diff --git a/test/L1/BalanceTracker.t.sol b/test/L1/BalanceTracker.t.sol index 94af8c1c..3d77df12 100644 --- a/test/L1/BalanceTracker.t.sol +++ b/test/L1/BalanceTracker.t.sol @@ -17,25 +17,24 @@ contract BalanceTrackerTest is Test { uint256 constant MAX_SYSTEM_ADDRESS_COUNT = 20; uint256 constant INITIAL_BALANCE_TRACKER_BALANCE = 2_000 ether; + uint256 constant BATCH_SENDER_TARGET_BALANCE = 1_000 ether; + uint256 constant L2_OUTPUT_PROPOSER_TARGET_BALANCE = 100 ether; + + address payable constant L1_STANDARD_BRIDGE = payable(address(1000)); + address payable constant PROFIT_WALLET = payable(address(1001)); + address payable constant BATCH_SENDER = payable(address(1002)); + address payable constant L2_OUTPUT_PROPOSER = payable(address(1003)); + address constant PROXY_ADMIN_OWNER = address(2048); - Proxy balanceTrackerProxy; - BalanceTracker balanceTrackerImplementation; BalanceTracker balanceTracker; - address payable l1StandardBridge = payable(address(1000)); - address payable profitWallet = payable(address(1001)); - address payable batchSender = payable(address(1002)); - address payable l2OutputProposer = payable(address(1003)); - uint256 batchSenderTargetBalance = 1_000 ether; - uint256 l2OutputProposerTargetBalance = 100 ether; - address payable[] systemAddresses = [batchSender, l2OutputProposer]; - uint256[] targetBalances = [batchSenderTargetBalance, l2OutputProposerTargetBalance]; - address proxyAdminOwner = address(2048); + address payable[] systemAddresses = [BATCH_SENDER, L2_OUTPUT_PROPOSER]; + uint256[] targetBalances = [BATCH_SENDER_TARGET_BALANCE, L2_OUTPUT_PROPOSER_TARGET_BALANCE]; function setUp() public { - balanceTrackerImplementation = new BalanceTracker(profitWallet); - balanceTrackerProxy = new Proxy(proxyAdminOwner); - vm.prank(proxyAdminOwner); + BalanceTracker balanceTrackerImplementation = new BalanceTracker(PROFIT_WALLET); + Proxy balanceTrackerProxy = new Proxy(PROXY_ADMIN_OWNER); + vm.prank(PROXY_ADMIN_OWNER); balanceTrackerProxy.upgradeTo(address(balanceTrackerImplementation)); balanceTracker = BalanceTracker(payable(address(balanceTrackerProxy))); } @@ -46,10 +45,10 @@ contract BalanceTrackerTest is Test { } function test_constructor_success() external { - balanceTracker = new BalanceTracker(profitWallet); + BalanceTracker tracker = new BalanceTracker(PROFIT_WALLET); - assertEq(balanceTracker.MAX_SYSTEM_ADDRESS_COUNT(), MAX_SYSTEM_ADDRESS_COUNT); - assertEq(balanceTracker.PROFIT_WALLET(), profitWallet); + assertEq(tracker.MAX_SYSTEM_ADDRESS_COUNT(), MAX_SYSTEM_ADDRESS_COUNT); + assertEq(tracker.PROFIT_WALLET(), PROFIT_WALLET); } function test_initializer_fail_systemAddresses_zeroLength() external { @@ -59,12 +58,11 @@ contract BalanceTrackerTest is Test { } function test_initializer_fail_systemAddresses_greaterThanMaxLength() external { - for (; systemAddresses.length <= balanceTracker.MAX_SYSTEM_ADDRESS_COUNT();) { - systemAddresses.push(payable(address(0))); - } + address payable[] memory oversizedSystemAddresses = new address payable[](MAX_SYSTEM_ADDRESS_COUNT + 1); + uint256[] memory oversizedTargetBalances = new uint256[](MAX_SYSTEM_ADDRESS_COUNT + 1); vm.expectRevert("BalanceTracker: systemAddresses cannot have a length greater than 20"); - balanceTracker.initialize(systemAddresses, targetBalances); + balanceTracker.initialize(oversizedSystemAddresses, oversizedTargetBalances); } function test_initializer_fail_systemAddresses_lengthNotEqualToTargetBalancesLength() external { @@ -99,24 +97,20 @@ contract BalanceTrackerTest is Test { function test_processFees_success_cannotBeReentered() external { vm.deal(address(balanceTracker), INITIAL_BALANCE_TRACKER_BALANCE); - uint256 expectedProfitWalletBalance = INITIAL_BALANCE_TRACKER_BALANCE - l2OutputProposerTargetBalance; + uint256 expectedProfitWalletBalance = INITIAL_BALANCE_TRACKER_BALANCE - L2_OUTPUT_PROPOSER_TARGET_BALANCE; address payable reentrancySystemAddress = payable(address(new ReenterProcessFees())); systemAddresses[0] = reentrancySystemAddress; balanceTracker.initialize(systemAddresses, targetBalances); - vm.expectEmit(true, true, true, true, address(balanceTracker)); - emit ProcessedFunds(reentrancySystemAddress, false, batchSenderTargetBalance, batchSenderTargetBalance); - vm.expectEmit(true, true, true, true, address(balanceTracker)); - emit ProcessedFunds(l2OutputProposer, true, l2OutputProposerTargetBalance, l2OutputProposerTargetBalance); - vm.expectEmit(true, true, true, true, address(balanceTracker)); - emit SentProfit(profitWallet, true, expectedProfitWalletBalance); + _expectProcessedFunds(reentrancySystemAddress, false, BATCH_SENDER_TARGET_BALANCE, BATCH_SENDER_TARGET_BALANCE); + _expectProcessedFunds( + L2_OUTPUT_PROPOSER, true, L2_OUTPUT_PROPOSER_TARGET_BALANCE, L2_OUTPUT_PROPOSER_TARGET_BALANCE + ); + _expectSentProfit(expectedProfitWalletBalance); balanceTracker.processFees(); - assertEq(address(balanceTracker).balance, 0); - assertEq(profitWallet.balance, expectedProfitWalletBalance); - assertEq(batchSender.balance, 0); - assertEq(l2OutputProposer.balance, l2OutputProposerTargetBalance); + _assertBalances(0, expectedProfitWalletBalance, 0, L2_OUTPUT_PROPOSER_TARGET_BALANCE); } function test_processFees_fail_whenNotInitialized() external { @@ -127,131 +121,138 @@ contract BalanceTrackerTest is Test { function test_processFees_success_continuesWhenSystemAddressReverts() external { vm.deal(address(balanceTracker), INITIAL_BALANCE_TRACKER_BALANCE); - uint256 expectedProfitWalletBalance = INITIAL_BALANCE_TRACKER_BALANCE - l2OutputProposerTargetBalance; + uint256 expectedProfitWalletBalance = INITIAL_BALANCE_TRACKER_BALANCE - L2_OUTPUT_PROPOSER_TARGET_BALANCE; balanceTracker.initialize(systemAddresses, targetBalances); - vm.mockCallRevert(batchSender, bytes(""), abi.encode("revert message")); - vm.expectEmit(true, true, true, true, address(balanceTracker)); - emit ProcessedFunds(batchSender, false, batchSenderTargetBalance, batchSenderTargetBalance); - vm.expectEmit(true, true, true, true, address(balanceTracker)); - emit ProcessedFunds(l2OutputProposer, true, l2OutputProposerTargetBalance, l2OutputProposerTargetBalance); - vm.expectEmit(true, true, true, true, address(balanceTracker)); - emit SentProfit(profitWallet, true, expectedProfitWalletBalance); + vm.mockCallRevert(BATCH_SENDER, bytes(""), abi.encode("revert message")); + _expectProcessedFunds(BATCH_SENDER, false, BATCH_SENDER_TARGET_BALANCE, BATCH_SENDER_TARGET_BALANCE); + _expectProcessedFunds( + L2_OUTPUT_PROPOSER, true, L2_OUTPUT_PROPOSER_TARGET_BALANCE, L2_OUTPUT_PROPOSER_TARGET_BALANCE + ); + _expectSentProfit(expectedProfitWalletBalance); balanceTracker.processFees(); - assertEq(address(balanceTracker).balance, 0); - assertEq(profitWallet.balance, expectedProfitWalletBalance); - assertEq(batchSender.balance, 0); - assertEq(l2OutputProposer.balance, l2OutputProposerTargetBalance); + _assertBalances(0, expectedProfitWalletBalance, 0, L2_OUTPUT_PROPOSER_TARGET_BALANCE); } function test_processFees_success_fundsSystemAddresses() external { vm.deal(address(balanceTracker), INITIAL_BALANCE_TRACKER_BALANCE); uint256 expectedProfitWalletBalance = - INITIAL_BALANCE_TRACKER_BALANCE - batchSenderTargetBalance - l2OutputProposerTargetBalance; + INITIAL_BALANCE_TRACKER_BALANCE - BATCH_SENDER_TARGET_BALANCE - L2_OUTPUT_PROPOSER_TARGET_BALANCE; balanceTracker.initialize(systemAddresses, targetBalances); - vm.expectEmit(true, true, true, true, address(balanceTracker)); - emit ProcessedFunds(batchSender, true, batchSenderTargetBalance, batchSenderTargetBalance); - vm.expectEmit(true, true, true, true, address(balanceTracker)); - emit ProcessedFunds(l2OutputProposer, true, l2OutputProposerTargetBalance, l2OutputProposerTargetBalance); - vm.expectEmit(true, true, true, true, address(balanceTracker)); - emit SentProfit(profitWallet, true, expectedProfitWalletBalance); + _expectProcessedFunds(BATCH_SENDER, true, BATCH_SENDER_TARGET_BALANCE, BATCH_SENDER_TARGET_BALANCE); + _expectProcessedFunds( + L2_OUTPUT_PROPOSER, true, L2_OUTPUT_PROPOSER_TARGET_BALANCE, L2_OUTPUT_PROPOSER_TARGET_BALANCE + ); + _expectSentProfit(expectedProfitWalletBalance); balanceTracker.processFees(); - assertEq(address(balanceTracker).balance, 0); - assertEq(profitWallet.balance, expectedProfitWalletBalance); - assertEq(batchSender.balance, batchSenderTargetBalance); - assertEq(l2OutputProposer.balance, l2OutputProposerTargetBalance); + _assertBalances(0, expectedProfitWalletBalance, BATCH_SENDER_TARGET_BALANCE, L2_OUTPUT_PROPOSER_TARGET_BALANCE); } function test_processFees_success_noFunds() external { balanceTracker.initialize(systemAddresses, targetBalances); - vm.expectEmit(true, true, true, true, address(balanceTracker)); - emit ProcessedFunds(batchSender, true, batchSenderTargetBalance, 0); - vm.expectEmit(true, true, true, true, address(balanceTracker)); - emit ProcessedFunds(l2OutputProposer, true, l2OutputProposerTargetBalance, 0); - vm.expectEmit(true, true, true, true, address(balanceTracker)); - emit SentProfit(profitWallet, true, 0); + _expectProcessedFunds(BATCH_SENDER, true, BATCH_SENDER_TARGET_BALANCE, 0); + _expectProcessedFunds(L2_OUTPUT_PROPOSER, true, L2_OUTPUT_PROPOSER_TARGET_BALANCE, 0); + _expectSentProfit(0); balanceTracker.processFees(); - assertEq(address(balanceTracker).balance, 0); - assertEq(profitWallet.balance, 0); - assertEq(batchSender.balance, 0); - assertEq(l2OutputProposer.balance, 0); + _assertBalances(0, 0, 0, 0); } function test_processFees_success_partialFunds() external { uint256 partialBalanceTrackerBalance = INITIAL_BALANCE_TRACKER_BALANCE / 3; vm.deal(address(balanceTracker), partialBalanceTrackerBalance); balanceTracker.initialize(systemAddresses, targetBalances); - vm.expectEmit(true, true, true, true, address(balanceTracker)); - emit ProcessedFunds(batchSender, true, batchSenderTargetBalance, partialBalanceTrackerBalance); - vm.expectEmit(true, true, true, true, address(balanceTracker)); - emit ProcessedFunds(l2OutputProposer, true, l2OutputProposerTargetBalance, 0); - vm.expectEmit(true, true, true, true, address(balanceTracker)); - emit SentProfit(profitWallet, true, 0); + _expectProcessedFunds(BATCH_SENDER, true, BATCH_SENDER_TARGET_BALANCE, partialBalanceTrackerBalance); + _expectProcessedFunds(L2_OUTPUT_PROPOSER, true, L2_OUTPUT_PROPOSER_TARGET_BALANCE, 0); + _expectSentProfit(0); balanceTracker.processFees(); - assertEq(address(balanceTracker).balance, 0); - assertEq(profitWallet.balance, 0); - assertEq(batchSender.balance, partialBalanceTrackerBalance); - assertEq(l2OutputProposer.balance, 0); + _assertBalances(0, 0, partialBalanceTrackerBalance, 0); } function test_processFees_success_skipsAddressesAtTargetBalance() external { vm.deal(address(balanceTracker), INITIAL_BALANCE_TRACKER_BALANCE); - vm.deal(batchSender, batchSenderTargetBalance); - vm.deal(l2OutputProposer, l2OutputProposerTargetBalance); + vm.deal(BATCH_SENDER, BATCH_SENDER_TARGET_BALANCE); + vm.deal(L2_OUTPUT_PROPOSER, L2_OUTPUT_PROPOSER_TARGET_BALANCE); balanceTracker.initialize(systemAddresses, targetBalances); - vm.expectEmit(true, true, true, true, address(balanceTracker)); - emit ProcessedFunds(batchSender, false, 0, 0); - vm.expectEmit(true, true, true, true, address(balanceTracker)); - emit ProcessedFunds(l2OutputProposer, false, 0, 0); - vm.expectEmit(true, true, true, true, address(balanceTracker)); - emit SentProfit(profitWallet, true, INITIAL_BALANCE_TRACKER_BALANCE); + _expectProcessedFunds(BATCH_SENDER, false, 0, 0); + _expectProcessedFunds(L2_OUTPUT_PROPOSER, false, 0, 0); + _expectSentProfit(INITIAL_BALANCE_TRACKER_BALANCE); balanceTracker.processFees(); - assertEq(address(balanceTracker).balance, 0); - assertEq(profitWallet.balance, INITIAL_BALANCE_TRACKER_BALANCE); - assertEq(batchSender.balance, batchSenderTargetBalance); - assertEq(l2OutputProposer.balance, l2OutputProposerTargetBalance); + _assertBalances( + 0, INITIAL_BALANCE_TRACKER_BALANCE, BATCH_SENDER_TARGET_BALANCE, L2_OUTPUT_PROPOSER_TARGET_BALANCE + ); } function test_processFees_success_maximumSystemAddresses() external { vm.deal(address(balanceTracker), INITIAL_BALANCE_TRACKER_BALANCE); - delete systemAddresses; - delete targetBalances; - for (uint256 i = 0; i < balanceTracker.MAX_SYSTEM_ADDRESS_COUNT(); i++) { - // forge-lint: disable-next-line(unsafe-typecast) - systemAddresses.push(payable(address(uint160(i + 100)))); - targetBalances.push(l2OutputProposerTargetBalance); + address payable[] memory maxSystemAddresses = new address payable[](MAX_SYSTEM_ADDRESS_COUNT); + uint256[] memory maxTargetBalances = new uint256[](MAX_SYSTEM_ADDRESS_COUNT); + for (uint256 i = 0; i < MAX_SYSTEM_ADDRESS_COUNT; i++) { + maxSystemAddresses[i] = payable(vm.addr(i + 100)); + maxTargetBalances[i] = L2_OUTPUT_PROPOSER_TARGET_BALANCE; } - balanceTracker.initialize(systemAddresses, targetBalances); + balanceTracker.initialize(maxSystemAddresses, maxTargetBalances); balanceTracker.processFees(); assertEq(address(balanceTracker).balance, 0); - for (uint256 i = 0; i < balanceTracker.MAX_SYSTEM_ADDRESS_COUNT(); i++) { - assertEq(systemAddresses[i].balance, l2OutputProposerTargetBalance); + for (uint256 i = 0; i < MAX_SYSTEM_ADDRESS_COUNT; i++) { + assertEq(maxSystemAddresses[i].balance, L2_OUTPUT_PROPOSER_TARGET_BALANCE); } - assertEq(profitWallet.balance, 0); + assertEq(PROFIT_WALLET.balance, 0); } function test_receive_success() external { uint256 value = 100; - vm.deal(l1StandardBridge, value); + vm.deal(L1_STANDARD_BRIDGE, value); - vm.prank(l1StandardBridge); - vm.expectEmit(true, true, true, true, address(balanceTracker)); - emit ReceivedFunds(l1StandardBridge, value); + vm.prank(L1_STANDARD_BRIDGE); + vm.expectEmit(address(balanceTracker)); + emit ReceivedFunds(L1_STANDARD_BRIDGE, value); (bool success,) = payable(address(balanceTracker)).call{ value: value }(""); assertTrue(success); assertEq(address(balanceTracker).balance, value); } + + function _expectProcessedFunds( + address systemAddress, + bool success, + uint256 balanceNeeded, + uint256 balanceSent + ) + internal + { + vm.expectEmit(address(balanceTracker)); + emit ProcessedFunds(systemAddress, success, balanceNeeded, balanceSent); + } + + function _expectSentProfit(uint256 balanceSent) internal { + vm.expectEmit(address(balanceTracker)); + emit SentProfit(PROFIT_WALLET, true, balanceSent); + } + + function _assertBalances( + uint256 trackerBalance, + uint256 profitWalletBalance, + uint256 batchSenderBalance, + uint256 l2OutputProposerBalance + ) + internal + view + { + assertEq(address(balanceTracker).balance, trackerBalance); + assertEq(PROFIT_WALLET.balance, profitWalletBalance); + assertEq(BATCH_SENDER.balance, batchSenderBalance); + assertEq(L2_OUTPUT_PROPOSER.balance, l2OutputProposerBalance); + } } diff --git a/test/L1/ETHLockbox.t.sol b/test/L1/ETHLockbox.t.sol index 40c042a2..1f96d859 100644 --- a/test/L1/ETHLockbox.t.sol +++ b/test/L1/ETHLockbox.t.sol @@ -21,8 +21,6 @@ import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; /// @title ETHLockbox_TestInit /// @notice Base contract that sets up the testing environment for ETHLockbox tests. abstract contract ETHLockbox_TestInit is CommonTest { - error InvalidInitialization(); - event ETHLocked(IOptimismPortal2 indexed portal, uint256 amount); event ETHUnlocked(IOptimismPortal2 indexed portal, uint256 amount); event PortalAuthorized(IOptimismPortal2 indexed portal); @@ -41,6 +39,28 @@ abstract contract ETHLockbox_TestInit is CommonTest { // If the ETHLockbox system feature is not enabled, skip these tests. skipIfSysFeatureDisabled(Features.ETH_LOCKBOX); } + + function _mockPortalSharedOwnerAndSuperchainConfig(IOptimismPortal2 _portal) internal { + vm.mockCall( + address(_portal), abi.encodeCall(IProxyAdminOwnedBase.proxyAdminOwner, ()), abi.encode(proxyAdminOwner) + ); + vm.mockCall( + address(_portal), abi.encodeCall(IOptimismPortal2.superchainConfig, ()), abi.encode(superchainConfig) + ); + } + + function _authorizePortalIfNeeded(IOptimismPortal2 _portal) internal { + if (!ethLockbox.authorizedPortals(_portal)) { + vm.prank(proxyAdminOwner); + ethLockbox.authorizePortal(_portal); + } + } + + function _mockLockboxSharedOwner(address _lockbox) internal { + vm.mockCall( + address(_lockbox), abi.encodeCall(IProxyAdminOwnedBase.proxyAdminOwner, ()), abi.encode(proxyAdminOwner) + ); + } } /// @title ETHLockbox_Version_Test @@ -56,6 +76,14 @@ contract ETHLockbox_Version_Test is ETHLockbox_TestInit { /// @title ETHLockbox_Initialize_Test /// @notice Test contract for the initialize function. contract ETHLockbox_Initialize_Test is ETHLockbox_TestInit { + StorageSlot internal initializedSlot; + + function setUp() public override { + super.setUp(); + + initializedSlot = ForgeArtifacts.getSlot("ETHLockbox", "_initialized"); + } + /// @notice Tests the superchain config was correctly set during initialization. function test_initialize_succeeds() public view { assertEq(address(ethLockbox.systemConfig().superchainConfig()), address(superchainConfig)); @@ -67,14 +95,9 @@ contract ETHLockbox_Initialize_Test is ETHLockbox_TestInit { /// but confirms that the initValue is not incremented incorrectly if an upgrade /// function is not present. function test_initialize_correctInitializerValue_succeeds() public view { - // Get the slot for _initialized. - StorageSlot memory slot = ForgeArtifacts.getSlot("ETHLockbox", "_initialized"); - - // Get the initializer value. - bytes32 slotVal = vm.load(address(ethLockbox), bytes32(slot.slot)); + bytes32 slotVal = vm.load(address(ethLockbox), bytes32(initializedSlot.slot)); uint8 val = uint8(uint256(slotVal) & 0xFF); - // Assert that the initializer value matches the expected value. assertEq(val, ethLockbox.initVersion()); } @@ -85,11 +108,8 @@ contract ETHLockbox_Initialize_Test is ETHLockbox_TestInit { // Prank as the not ProxyAdmin or ProxyAdmin owner. vm.assume(_sender != address(proxyAdmin) && _sender != proxyAdminOwner); - // Get the slot for _initialized. - StorageSlot memory slot = ForgeArtifacts.getSlot("ETHLockbox", "_initialized"); - // Set the initialized slot to 0. - vm.store(address(ethLockbox), bytes32(slot.slot), bytes32(0)); + vm.store(address(ethLockbox), bytes32(initializedSlot.slot), bytes32(0)); // Expect the revert with `ProxyAdminOwnedBase_NotProxyAdminOrProxyAdminOwner` selector vm.expectRevert(IProxyAdminOwnedBase.ProxyAdminOwnedBase_NotProxyAdminOrProxyAdminOwner.selector); @@ -183,10 +203,9 @@ contract ETHLockbox_AuthorizePortal_Test is ETHLockbox_TestInit { /// @notice Tests the `authorizePortal` function succeeds using the `optimismPortal2` address /// as the portal. function test_authorizePortal_succeeds() public { - // Calculate the correct storage slot for the mapping value - bytes32 mappingSlot = bytes32(uint256(1)); // position on the layout + StorageSlot memory authorizedPortalsSlot = ForgeArtifacts.getSlot("ETHLockbox", "authorizedPortals"); address key = address(optimismPortal2); - bytes32 slot = keccak256(abi.encode(key, mappingSlot)); + bytes32 slot = keccak256(abi.encode(key, bytes32(authorizedPortalsSlot.slot))); // Reset the authorization status to false vm.store(address(ethLockbox), slot, bytes32(0)); @@ -203,21 +222,11 @@ contract ETHLockbox_AuthorizePortal_Test is ETHLockbox_TestInit { assertTrue(ethLockbox.authorizedPortals(optimismPortal2)); } - /// @notice Tests the `authorizeLockbox` function succeeds + /// @notice Tests the `authorizePortal` function succeeds function testFuzz_authorizePortal_succeeds(IOptimismPortal2 _portal) public { assumeNotForgeAddress(address(_portal)); - // Mock the admin owner of the portal to be the same as the current lockbox proxy admin - // owner - vm.mockCall( - address(_portal), abi.encodeCall(IProxyAdminOwnedBase.proxyAdminOwner, ()), abi.encode(proxyAdminOwner) - ); - - // Mock the SuperchainConfig on the portal to be the same as the SuperchainConfig on the - // Lockbox. - vm.mockCall( - address(_portal), abi.encodeCall(IOptimismPortal2.superchainConfig, ()), abi.encode(superchainConfig) - ); + _mockPortalSharedOwnerAndSuperchainConfig(_portal); // Expect the `PortalAuthorized` event to be emitted vm.expectEmit(address(ethLockbox)); @@ -245,11 +254,7 @@ contract ETHLockbox_ReceiveLiquidity_Test is ETHLockbox_TestInit { // Deal the value to the lockbox deal(address(_lockbox), _value); - // Mock the admin owner of the lockbox to be the same as the current lockbox proxy admin - // owner - vm.mockCall( - address(_lockbox), abi.encodeCall(IProxyAdminOwnedBase.proxyAdminOwner, ()), abi.encode(proxyAdminOwner) - ); + _mockLockboxSharedOwner(_lockbox); // Authorize the lockbox if needed if (!ethLockbox.authorizedLockboxes(IETHLockbox(_lockbox))) { @@ -323,23 +328,10 @@ contract ETHLockbox_LockETH_Test is ETHLockbox_TestInit { assumeNotForgeAddress(address(_portal)); vm.assume(address(_portal) != address(ethLockbox)); - // Mock the admin owner of the portal to be the same as the current lockbox proxy admin - // owner - vm.mockCall( - address(_portal), abi.encodeCall(IProxyAdminOwnedBase.proxyAdminOwner, ()), abi.encode(proxyAdminOwner) - ); - - // Mock the SuperchainConfig on the portal to be the same as the SuperchainConfig on the - // lockbox. - vm.mockCall( - address(_portal), abi.encodeCall(IOptimismPortal2.superchainConfig, ()), abi.encode(superchainConfig) - ); + _mockPortalSharedOwnerAndSuperchainConfig(_portal); // Set the portal as an authorized portal if needed - if (!ethLockbox.authorizedPortals(_portal)) { - vm.prank(proxyAdminOwner); - ethLockbox.authorizePortal(_portal); - } + _authorizePortalIfNeeded(_portal); // Deal the ETH amount to the portal vm.deal(address(_portal), _amount); @@ -450,49 +442,41 @@ contract ETHLockbox_UnlockETH_Test is ETHLockbox_TestInit { /// @notice Tests the ETH is correctly unlocked when the caller is an authorized portal. function testFuzz_unlockETH_multiplePortals_succeeds(IOptimismPortal2 _portal, uint256 _value) public { + // Since on the fork the `_portal` fuzzed address doesn't exist, we skip the test + if (isForkTest()) vm.skip(true); assumeNotForgeAddress(address(_portal)); + vm.assume(address(_portal) != address(ethLockbox)); - // Mock the admin owner of the portal to be the same as the current lockbox proxy admin - // owner - vm.mockCall( - address(_portal), abi.encodeCall(IProxyAdminOwnedBase.proxyAdminOwner, ()), abi.encode(proxyAdminOwner) - ); - - // Mock the SuperchainConfig on the portal to be the same as the SuperchainConfig on the - // lockbox. - + _mockPortalSharedOwnerAndSuperchainConfig(_portal); vm.mockCall( - address(_portal), abi.encodeCall(IOptimismPortal2.superchainConfig, ()), abi.encode(superchainConfig) + address(_portal), abi.encodeCall(IOptimismPortal2.l2Sender, ()), abi.encode(Constants.DEFAULT_L2_SENDER) ); // Set the portal as an authorized portal if needed - if (!ethLockbox.authorizedPortals(_portal)) { - vm.prank(proxyAdminOwner); - ethLockbox.authorizePortal(_portal); - } + _authorizePortalIfNeeded(_portal); // Deal the ETH amount to the lockbox vm.deal(address(ethLockbox), _value); // Get the balance of the portal and lockbox before the unlock to compare later on the // assertions - uint256 portalBalanceBefore = address(optimismPortal2).balance; + uint256 portalBalanceBefore = address(_portal).balance; uint256 lockboxBalanceBefore = address(ethLockbox).balance; // Expect `donateETH` function to be called on Portal - vm.expectCall(address(optimismPortal2), abi.encodeCall(IOptimismPortal2.donateETH, ())); + vm.expectCall(address(_portal), abi.encodeCall(IOptimismPortal2.donateETH, ())); // Look for the emit of the `ETHUnlocked` event vm.expectEmit(address(ethLockbox)); - emit ETHUnlocked(optimismPortal2, _value); + emit ETHUnlocked(_portal, _value); // Call the `unlockETH` function with the portal - vm.prank(address(optimismPortal2)); + vm.prank(address(_portal)); ethLockbox.unlockETH(_value); // Assert the portal's balance increased and the lockbox's balance decreased by the amount // unlocked - assertEq(address(optimismPortal2).balance, portalBalanceBefore + _value); + assertEq(address(_portal).balance, portalBalanceBefore + _value); assertEq(address(ethLockbox).balance, lockboxBalanceBefore - _value); } } @@ -532,11 +516,7 @@ contract ETHLockbox_AuthorizeLockbox_Test is ETHLockbox_TestInit { function testFuzz_authorizeLockbox_succeeds(address _lockbox) public { assumeNotForgeAddress(_lockbox); - // Mock the admin owner of the lockbox to be the same as the current lockbox proxy admin - // owner - vm.mockCall( - address(_lockbox), abi.encodeCall(IProxyAdminOwnedBase.proxyAdminOwner, ()), abi.encode(proxyAdminOwner) - ); + _mockLockboxSharedOwner(_lockbox); // Expect the `LockboxAuthorized` event to be emitted vm.expectEmit(address(ethLockbox)); @@ -611,16 +591,6 @@ contract ETHLockbox_MigrateLiquidity_Test is ETHLockbox_TestInit { vm.prank(proxyAdminOwner); IETHLockbox(destinationLockbox).authorizeLockbox(ethLockbox); - // Mock the calls for checks on the destination lockbox so it can receive the migration - vm.mockCall( - address(destinationLockbox), - abi.encodeCall(IProxyAdminOwnedBase.proxyAdminOwner, ()), - abi.encode(proxyAdminOwner) - ); - vm.mockCall( - address(destinationLockbox), abi.encodeCall(IETHLockbox.authorizedLockboxes, (ethLockbox)), abi.encode(true) - ); - // Deal the balance to both lockboxes deal(address(ethLockbox), _originLockboxBalance); deal(address(destinationLockbox), _destinationLockboxBalance); diff --git a/test/L1/L1CrossDomainMessenger.t.sol b/test/L1/L1CrossDomainMessenger.t.sol index 2ce66cd5..29ac08d4 100644 --- a/test/L1/L1CrossDomainMessenger.t.sol +++ b/test/L1/L1CrossDomainMessenger.t.sol @@ -19,42 +19,10 @@ import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; import { IProxyAdminOwnedBase } from "interfaces/L1/IProxyAdminOwnedBase.sol"; -/// @title L1CrossDomainMessenger_Encoding_Harness -/// @notice A harness contract for testing internal functions of the Encoding library. -contract L1CrossDomainMessenger_Encoding_Harness { - function encodeCrossDomainMessage( - uint256 nonce, - address sender, - address target, - uint256 value, - uint256 gasLimit, - bytes memory data - ) - external - pure - returns (bytes memory) - { - return Encoding.encodeCrossDomainMessage(nonce, sender, target, value, gasLimit, data); - } -} - /// @title L1CrossDomainMessenger_TestInit /// @notice Reusable test initialization for L1CrossDomainMessenger tests. abstract contract L1CrossDomainMessenger_TestInit is CommonTest { - /// @dev The receiver address - address recipient = address(0xabbaacdc); - - /// @dev The storage slot of the l2Sender - uint256 senderSlotIndex; - - /// @dev Encoding library harness. - L1CrossDomainMessenger_Encoding_Harness encoding; - - function setUp() public virtual override { - super.setUp(); - senderSlotIndex = ForgeArtifacts.getSlot("OptimismPortal2", "l2Sender").slot; - encoding = new L1CrossDomainMessenger_Encoding_Harness(); - } + address internal constant recipient = address(0xabbaacdc); } /// @title L1CrossDomainMessenger_Constructor_Test @@ -80,6 +48,17 @@ contract L1CrossDomainMessenger_Constructor_Test is L1CrossDomainMessenger_TestI /// @title L1CrossDomainMessenger_Initialize_Test /// @notice Tests for the `initialize` function of the L1CrossDomainMessenger. contract L1CrossDomainMessenger_Initialize_Test is L1CrossDomainMessenger_TestInit { + function _resetInitialized() internal { + StorageSlot memory initializedSlot = ForgeArtifacts.getSlot("L1CrossDomainMessenger", "_initialized"); + vm.store(address(l1CrossDomainMessenger), bytes32(initializedSlot.slot), bytes32(0)); + } + + function _initializedValue() internal view returns (uint8) { + StorageSlot memory initializedSlot = ForgeArtifacts.getSlot("L1CrossDomainMessenger", "_initialized"); + bytes32 slotVal = vm.load(address(l1CrossDomainMessenger), bytes32(initializedSlot.slot)); + return uint8((uint256(slotVal) >> (initializedSlot.offset * 8)) & 0xFF); + } + /// @notice Tests that the proxy is initialized correctly. function test_initialize_succeeds() external view { assertEq(address(l1CrossDomainMessenger.systemConfig()), address(systemConfig)); @@ -94,17 +73,7 @@ contract L1CrossDomainMessenger_Initialize_Test is L1CrossDomainMessenger_TestIn /// initialization but confirms that the initValue is not incremented incorrectly if /// an upgrade function is not present. function test_initialize_correctInitializerValue_succeeds() public view { - // Get the slot for _initialized. - StorageSlot memory slot = ForgeArtifacts.getSlot("L1CrossDomainMessenger", "_initialized"); - - // Get the initializer value. - // Note that for L1CrossDomainMessenger the initialized value is stored at offset 20 so - // this test is slightly different from other similar tests. - bytes32 slotVal = vm.load(address(l1CrossDomainMessenger), bytes32(slot.slot)); - uint8 val = uint8((uint256(slotVal) >> 20 * 8) & 0xFF); - - // Assert that the initializer value matches the expected value. - assertEq(val, l1CrossDomainMessenger.initVersion()); + assertEq(_initializedValue(), l1CrossDomainMessenger.initVersion()); } /// @notice Tests that the initialize function reverts if called by a non-proxy admin or owner. @@ -113,11 +82,7 @@ contract L1CrossDomainMessenger_Initialize_Test is L1CrossDomainMessenger_TestIn // Prank as the not ProxyAdmin or ProxyAdmin owner. vm.assume(_sender != address(proxyAdmin) && _sender != proxyAdminOwner); - // Get the slot for _initialized. - StorageSlot memory slot = ForgeArtifacts.getSlot("L1CrossDomainMessenger", "_initialized"); - - // Set the initialized slot to 0. - vm.store(address(l1CrossDomainMessenger), bytes32(slot.slot), bytes32(0)); + _resetInitialized(); // Expect the revert with `ProxyAdminOwnedBase_NotProxyAdminOrProxyAdminOwner` selector vm.expectRevert(IProxyAdminOwnedBase.ProxyAdminOwnedBase_NotProxyAdminOrProxyAdminOwner.selector); @@ -130,11 +95,7 @@ contract L1CrossDomainMessenger_Initialize_Test is L1CrossDomainMessenger_TestIn /// @notice Fuzz test for initialize with any system config address. /// @param _systemConfig The system config address to test. function testFuzz_initialize_anySystemConfig_succeeds(address _systemConfig) external { - // Get the slot for _initialized. - StorageSlot memory slot = ForgeArtifacts.getSlot("L1CrossDomainMessenger", "_initialized"); - - // Set the initialized slot to 0. - vm.store(address(l1CrossDomainMessenger), bytes32(slot.slot), bytes32(0)); + _resetInitialized(); // Initialize with the fuzzed system config address vm.prank(address(proxyAdmin)); @@ -148,11 +109,7 @@ contract L1CrossDomainMessenger_Initialize_Test is L1CrossDomainMessenger_TestIn /// @notice Fuzz test for initialize with any portal address. /// @param _portal The portal address to test. function testFuzz_initialize_anyPortal_succeeds(address _portal) external { - // Get the slot for _initialized. - StorageSlot memory slot = ForgeArtifacts.getSlot("L1CrossDomainMessenger", "_initialized"); - - // Set the initialized slot to 0. - vm.store(address(l1CrossDomainMessenger), bytes32(slot.slot), bytes32(0)); + _resetInitialized(); // Initialize with the fuzzed portal address vm.prank(address(proxyAdmin)); @@ -219,20 +176,19 @@ contract L1CrossDomainMessenger_SendMessage_Test is L1CrossDomainMessenger_TestI /// TODO: this same test needs to be done with the legacy message type /// by setting the message version to 0 function test_sendMessage_succeeds() external { + uint32 minGasLimit = 100; + bytes memory message = hex"ff"; + uint256 nonce = l1CrossDomainMessenger.messageNonce(); + uint64 baseGas = l1CrossDomainMessenger.baseGas(message, minGasLimit); + bytes memory xDomainCalldata = + Encoding.encodeCrossDomainMessage(nonce, alice, recipient, 0, minGasLimit, message); + // deposit transaction on the optimism portal should be called vm.expectCall( address(optimismPortal2), abi.encodeCall( IOptimismPortal2.depositTransaction, - ( - Predeploys.L2_CROSS_DOMAIN_MESSENGER, - 0, - l1CrossDomainMessenger.baseGas(hex"ff", 100), - false, - Encoding.encodeCrossDomainMessage( - l1CrossDomainMessenger.messageNonce(), alice, recipient, 0, 100, hex"ff" - ) - ) + (Predeploys.L2_CROSS_DOMAIN_MESSENGER, 0, baseGas, false, xDomainCalldata) ) ); @@ -243,21 +199,21 @@ contract L1CrossDomainMessenger_SendMessage_Test is L1CrossDomainMessenger_TestI Predeploys.L2_CROSS_DOMAIN_MESSENGER, 0, 0, - l1CrossDomainMessenger.baseGas(hex"ff", 100), + baseGas, false, - Encoding.encodeCrossDomainMessage(l1CrossDomainMessenger.messageNonce(), alice, recipient, 0, 100, hex"ff") + xDomainCalldata ); // SentMessage event vm.expectEmit(address(l1CrossDomainMessenger)); - emit SentMessage(recipient, alice, hex"ff", l1CrossDomainMessenger.messageNonce(), 100); + emit SentMessage(recipient, alice, message, nonce, minGasLimit); // SentMessageExtension1 event vm.expectEmit(address(l1CrossDomainMessenger)); emit SentMessageExtension1(alice, 0); vm.prank(alice); - l1CrossDomainMessenger.sendMessage(recipient, hex"ff", uint32(100)); + l1CrossDomainMessenger.sendMessage(recipient, message, minGasLimit); } /// @notice Fuzz test for sendMessage with various gas limits and message data. @@ -273,24 +229,22 @@ contract L1CrossDomainMessenger_SendMessage_Test is L1CrossDomainMessenger_TestI { // Bound gas limit to reasonable range to avoid OutOfGas errors _gasLimit = uint32(bound(uint256(_gasLimit), 21000, 1_000_000)); - // Bound message length to avoid excessive gas costs - vm.assume(_message.length <= 1000); + bytes calldata message = _message[:_message.length > 1000 ? 1000 : _message.length]; vm.assume(_sender != address(0)); uint256 nonceBefore = l1CrossDomainMessenger.messageNonce(); vm.prank(_sender); - l1CrossDomainMessenger.sendMessage(recipient, _message, _gasLimit); + l1CrossDomainMessenger.sendMessage(recipient, message, _gasLimit); - // Verify nonce incremented assertEq(l1CrossDomainMessenger.messageNonce(), nonceBefore + 1); } /// @notice Tests that the sendMessage function is able to send the same message twice. function test_sendMessage_twice_succeeds() external { uint256 nonce = l1CrossDomainMessenger.messageNonce(); - l1CrossDomainMessenger.sendMessage(recipient, hex"aa", uint32(500_000)); - l1CrossDomainMessenger.sendMessage(recipient, hex"aa", uint32(500_000)); + l1CrossDomainMessenger.sendMessage(recipient, hex"aa", 500_000); + l1CrossDomainMessenger.sendMessage(recipient, hex"aa", 500_000); // the nonce increments for each message sent assertEq(nonce + 2, l1CrossDomainMessenger.messageNonce()); } @@ -306,7 +260,6 @@ contract L1CrossDomainMessenger_SendMessage_Test is L1CrossDomainMessenger_TestI vm.prank(alice); l1CrossDomainMessenger.sendMessage(recipient, hex"1234", 0); - // Verify nonce incremented assertEq(l1CrossDomainMessenger.messageNonce(), nonce + 1); } @@ -326,32 +279,44 @@ contract L1CrossDomainMessenger_SendMessage_Test is L1CrossDomainMessenger_TestI /// but are testing functionality of the CrossDomainMessenger contract that is inherited /// from. contract L1CrossDomainMessenger_Uncategorized_Test is L1CrossDomainMessenger_TestInit { - // Variables for reentrancy test bool reentrancyAttacked; uint256 constant reentrancyMessageValue = 50; - bytes reentrancySelector; - address reentrancySender; - bytes32 reentrancyHash; - address reentrancyTarget; - - function setUp() public virtual override { - super.setUp(); - // Setup for reentrancy test variables (balance setup moved to specific test) - reentrancyTarget = address(this); - reentrancySender = Predeploys.L2_CROSS_DOMAIN_MESSENGER; - reentrancySelector = abi.encodeCall(this.reinitAndReenter, ()); - reentrancyHash = Hashing.hashCrossDomainMessage( - Encoding.encodeVersionedNonce({ _nonce: 0, _version: 1 }), - reentrancySender, - reentrancyTarget, + + function _versionedNonce(uint16 _version) internal pure returns (uint256) { + return Encoding.encodeVersionedNonce({ _nonce: 0, _version: _version }); + } + + function _reentrancyMessage() internal view returns (bytes memory) { + return abi.encodeCall(this.reinitAndReenter, ()); + } + + function _reentrancyHash() internal view returns (bytes32) { + return Hashing.hashCrossDomainMessage( + _versionedNonce(1), + Predeploys.L2_CROSS_DOMAIN_MESSENGER, + address(this), reentrancyMessageValue, 0, - reentrancySelector + _reentrancyMessage() ); } + function _setPortalL2Sender(address _sender) internal { + StorageSlot memory senderSlot = ForgeArtifacts.getSlot("OptimismPortal2", "l2Sender"); + vm.store(address(optimismPortal2), bytes32(senderSlot.slot), bytes32(abi.encode(_sender))); + } + + function _assertMessageStatus(bytes32 _hash, bool _successful, bool _failed) internal view { + assertEq(l1CrossDomainMessenger.successfulMessages(_hash), _successful); + assertEq(l1CrossDomainMessenger.failedMessages(_hash), _failed); + } + + function _assertMessageRecorded(bytes32 _hash) internal view { + assertTrue(l1CrossDomainMessenger.successfulMessages(_hash) || l1CrossDomainMessenger.failedMessages(_hash)); + } + /// @notice This method will be called by the relayed message, and will attempt to reenter the - /// `relayMessage` function exactly one. + /// `relayMessage` function once. function reinitAndReenter() external payable { // only attempt the attack once if (!reentrancyAttacked) { @@ -364,14 +329,16 @@ contract L1CrossDomainMessenger_Uncategorized_Test is L1CrossDomainMessenger_Tes // attempt to re-replay the withdrawal vm.expectEmit(address(l1CrossDomainMessenger)); - emit FailedRelayedMessage(reentrancyHash); + bytes32 hash = _reentrancyHash(); + bytes memory message = _reentrancyMessage(); + emit FailedRelayedMessage(hash); l1CrossDomainMessenger.relayMessage( - Encoding.encodeVersionedNonce({ _nonce: 0, _version: 1 }), // nonce - reentrancySender, - reentrancyTarget, + _versionedNonce(1), + Predeploys.L2_CROSS_DOMAIN_MESSENGER, + address(this), reentrancyMessageValue, 0, - reentrancySelector + message ); } } @@ -394,8 +361,7 @@ contract L1CrossDomainMessenger_Uncategorized_Test is L1CrossDomainMessenger_Tes address target = address(0xabcd); address sender = Predeploys.L2_CROSS_DOMAIN_MESSENGER; - // Set the value of op.l2Sender() to be the L2 Cross Domain Messenger. - vm.store(address(optimismPortal2), bytes32(senderSlotIndex), bytes32(abi.encode(sender))); + _setPortalL2Sender(sender); // Expect a revert. vm.expectRevert("CrossDomainMessenger: only version 0 or 1 messages are supported at this time"); @@ -403,7 +369,7 @@ contract L1CrossDomainMessenger_Uncategorized_Test is L1CrossDomainMessenger_Tes // Try to relay a v2 message. vm.prank(address(optimismPortal2)); l1CrossDomainMessenger.relayMessage( - Encoding.encodeVersionedNonce({ _nonce: 0, _version: 2 }), // nonce + _versionedNonce(2), sender, target, 0, // value @@ -420,31 +386,19 @@ contract L1CrossDomainMessenger_Uncategorized_Test is L1CrossDomainMessenger_Tes vm.expectCall(target, hex"1111"); - // set the value of op.l2Sender() to be the L2 Cross Domain Messenger. - vm.store(address(optimismPortal2), bytes32(senderSlotIndex), bytes32(abi.encode(sender))); + _setPortalL2Sender(sender); vm.prank(address(optimismPortal2)); vm.expectEmit(address(l1CrossDomainMessenger)); - bytes32 hash = Hashing.hashCrossDomainMessage( - Encoding.encodeVersionedNonce({ _nonce: 0, _version: 1 }), sender, target, 0, 0, hex"1111" - ); + uint256 nonce = _versionedNonce(1); + bytes32 hash = Hashing.hashCrossDomainMessage(nonce, sender, target, 0, 0, hex"1111"); emit RelayedMessage(hash); - l1CrossDomainMessenger.relayMessage( - Encoding.encodeVersionedNonce({ _nonce: 0, _version: 1 }), // nonce - sender, - target, - 0, // value - 0, - hex"1111" - ); + l1CrossDomainMessenger.relayMessage(nonce, sender, target, 0, 0, hex"1111"); - // the message hash is in the successfulMessages mapping - assert(l1CrossDomainMessenger.successfulMessages(hash)); - // it is not in the received messages mapping - assertEq(l1CrossDomainMessenger.failedMessages(hash), false); + _assertMessageStatus(hash, true, false); } /// @notice Fuzz test for relaying messages with various parameters. @@ -463,26 +417,20 @@ contract L1CrossDomainMessenger_Uncategorized_Test is L1CrossDomainMessenger_Tes vm.assume(_target != address(optimismPortal2)); vm.assume(_target != address(0)); - // Bound gas limit and message size to avoid OutOfGas errors _minGasLimit = uint32(bound(uint256(_minGasLimit), 0, 100_000)); - vm.assume(_message.length <= 100); + bytes calldata message = _message[:_message.length > 100 ? 100 : _message.length]; address sender = Predeploys.L2_CROSS_DOMAIN_MESSENGER; - // set the value of op.l2Sender() to be the L2 Cross Domain Messenger. - vm.store(address(optimismPortal2), bytes32(senderSlotIndex), bytes32(abi.encode(sender))); + _setPortalL2Sender(sender); - bytes32 hash = Hashing.hashCrossDomainMessage( - Encoding.encodeVersionedNonce({ _nonce: 0, _version: 1 }), sender, _target, 0, _minGasLimit, _message - ); + uint256 nonce = _versionedNonce(1); + bytes32 hash = Hashing.hashCrossDomainMessage(nonce, sender, _target, 0, _minGasLimit, message); vm.prank(address(optimismPortal2)); - l1CrossDomainMessenger.relayMessage( - Encoding.encodeVersionedNonce({ _nonce: 0, _version: 1 }), sender, _target, 0, _minGasLimit, _message - ); + l1CrossDomainMessenger.relayMessage(nonce, sender, _target, 0, _minGasLimit, message); - // Verify message was relayed (either successfully or failed) - assertTrue(l1CrossDomainMessenger.successfulMessages(hash) || l1CrossDomainMessenger.failedMessages(hash)); + _assertMessageRecorded(hash); } /// @notice Tests that `relayMessage` reverts if the caller is optimismPortal2 and the value @@ -492,8 +440,7 @@ contract L1CrossDomainMessenger_Uncategorized_Test is L1CrossDomainMessenger_Tes address sender = Predeploys.L2_CROSS_DOMAIN_MESSENGER; bytes memory message = hex"1111"; - // set the value of op.l2Sender() to be the L2CrossDomainMessenger. - vm.store(address(optimismPortal2), bytes32(senderSlotIndex), bytes32(abi.encode(sender))); + _setPortalL2Sender(sender); // correctly sending as OptimismPortal but amount does not match msg.value vm.deal(address(optimismPortal2), 10 ether); @@ -511,8 +458,7 @@ contract L1CrossDomainMessenger_Uncategorized_Test is L1CrossDomainMessenger_Tes address sender = Predeploys.L2_CROSS_DOMAIN_MESSENGER; bytes memory message = hex"1111"; - // set the value of op.l2Sender() to be the L2 Cross Domain Messenger. - vm.store(address(optimismPortal2), bytes32(senderSlotIndex), bytes32(abi.encode(sender))); + _setPortalL2Sender(sender); // make a failed message vm.etch(target, hex"fe"); @@ -535,7 +481,7 @@ contract L1CrossDomainMessenger_Uncategorized_Test is L1CrossDomainMessenger_Tes address sender = Predeploys.L2_CROSS_DOMAIN_MESSENGER; bytes memory message = hex"1111"; - vm.store(address(optimismPortal2), bytes32(senderSlotIndex), bytes32(abi.encode(sender))); + _setPortalL2Sender(sender); vm.prank(address(optimismPortal2)); vm.expectRevert("CrossDomainMessenger: cannot send message to blocked system address"); @@ -555,7 +501,7 @@ contract L1CrossDomainMessenger_Uncategorized_Test is L1CrossDomainMessenger_Tes address sender = Predeploys.L2_CROSS_DOMAIN_MESSENGER; bytes memory message = hex"1111"; - vm.store(address(optimismPortal2), bytes32(senderSlotIndex), bytes32(abi.encode(sender))); + _setPortalL2Sender(sender); vm.prank(address(optimismPortal2)); vm.expectRevert("CrossDomainMessenger: cannot send message to blocked system address"); @@ -567,11 +513,11 @@ contract L1CrossDomainMessenger_Uncategorized_Test is L1CrossDomainMessenger_Tes /// @notice Tests that `relayMessage` reverts if the message is called by a non-optimismPortal2 /// address and is not a failed message eligible for replay. function test_relayMessage_relayingNewMessageByExternalUser_reverts() external { - address target = address(alice); + address target = alice; address sender = Predeploys.L2_CROSS_DOMAIN_MESSENGER; bytes memory message = hex"1111"; - vm.store(address(optimismPortal2), bytes32(senderSlotIndex), bytes32(abi.encode(sender))); + _setPortalL2Sender(sender); vm.prank(bob); vm.expectRevert("CrossDomainMessenger: message cannot be replayed"); @@ -632,25 +578,29 @@ contract L1CrossDomainMessenger_Uncategorized_Test is L1CrossDomainMessenger_Tes _message ); - // Count the number of non-zero bytes in the message. - uint256 zeroBytesInCalldata = 0; + // Count calldata bytes so the EIP-7623 floor can be checked against actual encoded data. uint256 nonzeroBytesInCalldata = 0; for (uint256 i = 0; i < encoded.length; i++) { if (encoded[i] != bytes1(0)) { nonzeroBytesInCalldata++; - } else { - zeroBytesInCalldata++; } } + uint256 zeroBytesInCalldata = encoded.length - nonzeroBytesInCalldata; + uint256 calldataTokens = zeroBytesInCalldata + nonzeroBytesInCalldata * 4; + uint256 floorCost = l1CrossDomainMessenger.TX_BASE_GAS() + calldataTokens + * (l1CrossDomainMessenger.FLOOR_CALLDATA_OVERHEAD() / 4); + // Base gas must always be sufficient to cover the floor cost from EIP-7623. - assertGt(baseGas, 21000 + ((zeroBytesInCalldata + nonzeroBytesInCalldata * 4) * 10)); + assertGt(baseGas, floorCost); // Actual gas on L2 will be the base gas minus the intrinsic gas cost. Note that even after // EIP-7623, we still deduct 21k + 16 gas per calldata token from the gas limit before // execution happens. After execution, if the message didn't spend enough in execution gas, // the EVM will floor the cost of the transaction to 21k + 40 gas per calldata token. - uint256 gasSupplied = baseGas - (21000 + ((zeroBytesInCalldata + nonzeroBytesInCalldata * 4) * 4)); + uint256 intrinsicCost = l1CrossDomainMessenger.TX_BASE_GAS() + calldataTokens + * (l1CrossDomainMessenger.MIN_GAS_CALLDATA_OVERHEAD() / 4); + uint256 gasSupplied = baseGas - intrinsicCost; // We'll trigger the L2CrossDomainMessenger as if we're the L1CrossDomainMessenger address caller = AddressAliasHelper.applyL1ToL2Alias(address(l1CrossDomainMessenger)); @@ -662,8 +612,9 @@ contract L1CrossDomainMessenger_Uncategorized_Test is L1CrossDomainMessenger_Tes assertTrue(success, "L2CrossDomainMessenger call should not fail"); // Message should either be in the failed or successful messages mapping. - bool inFailedMessages = l2CrossDomainMessenger.failedMessages(keccak256(encoded)); - bool inSuccessfulMessages = l2CrossDomainMessenger.successfulMessages(keccak256(encoded)); + bytes32 encodedHash = keccak256(encoded); + bool inFailedMessages = l2CrossDomainMessenger.failedMessages(encodedHash); + bool inSuccessfulMessages = l2CrossDomainMessenger.successfulMessages(encodedHash); assertTrue( inFailedMessages || inSuccessfulMessages, "message should be in either failed or successful messages" ); @@ -705,26 +656,13 @@ contract L1CrossDomainMessenger_Uncategorized_Test is L1CrossDomainMessenger_Tes bytes memory _message ) external + view { - // Make sure that unexpected nonces aren't being used right now. - // Prevents people from forgetting to update this test if a new version is ever used. - if (_version > 2) { - vm.expectRevert("Encoding: unknown cross domain message version"); - encoding.encodeCrossDomainMessage( - Encoding.encodeVersionedNonce({ _nonce: 0, _version: _version }), - _sender, - _target, - _value, - _minGasLimit, - _message - ); - } - // Clamp the version to 0 or 1. _version = _version % 2; // Encode the message. - bytes memory encoded = encoding.encodeCrossDomainMessage( + bytes memory encoded = Encoding.encodeCrossDomainMessage( Encoding.encodeVersionedNonce({ _nonce: _nonce, _version: _version }), _sender, _target, @@ -745,7 +683,7 @@ contract L1CrossDomainMessenger_Uncategorized_Test is L1CrossDomainMessenger_Tes address sender = Predeploys.L2_CROSS_DOMAIN_MESSENGER; - vm.store(address(optimismPortal2), bytes32(senderSlotIndex), bytes32(abi.encode(sender))); + _setPortalL2Sender(sender); vm.prank(address(optimismPortal2)); l1CrossDomainMessenger.relayMessage( Encoding.encodeVersionedNonce({ _nonce: 0, _version: 1 }), address(0), address(0), 0, 0, hex"" @@ -755,9 +693,11 @@ contract L1CrossDomainMessenger_Uncategorized_Test is L1CrossDomainMessenger_Tes l1CrossDomainMessenger.xDomainMessageSender(); } - /// @notice Tests that xDomainMessageSender is never set during sendMessage. - function test_xDomainMessageSender_duringSend_reverts() external { - // XDomainMessageSender is only set during relayMessage, not sendMessage + /// @notice Tests that xDomainMessageSender remains unset after sendMessage. + function test_xDomainMessageSender_afterSend_reverts() external { + vm.prank(alice); + l1CrossDomainMessenger.sendMessage(recipient, hex"1234", 100); + vm.expectRevert("CrossDomainMessenger: xDomainMessageSender is not set"); l1CrossDomainMessenger.xDomainMessageSender(); } @@ -776,8 +716,8 @@ contract L1CrossDomainMessenger_Uncategorized_Test is L1CrossDomainMessenger_Tes Encoding.encodeVersionedNonce({ _nonce: 0, _version: 1 }), sender, target, value, 0, hex"1111" ); - vm.store(address(optimismPortal2), bytes32(senderSlotIndex), bytes32(abi.encode(sender))); - vm.etch(target, address(new Reverter()).code); + _setPortalL2Sender(sender); + vm.etch(target, type(Reverter).runtimeCode); vm.deal(address(optimismPortal2), value); vm.prank(address(optimismPortal2)); l1CrossDomainMessenger.relayMessage{ value: value }( @@ -791,8 +731,7 @@ contract L1CrossDomainMessenger_Uncategorized_Test is L1CrossDomainMessenger_Tes assertEq(address(l1CrossDomainMessenger).balance, value); assertEq(address(target).balance, 0); - assertEq(l1CrossDomainMessenger.successfulMessages(hash), false); - assertEq(l1CrossDomainMessenger.failedMessages(hash), true); + _assertMessageStatus(hash, false, true); vm.expectEmit(address(l1CrossDomainMessenger)); @@ -811,8 +750,7 @@ contract L1CrossDomainMessenger_Uncategorized_Test is L1CrossDomainMessenger_Tes assertEq(address(l1CrossDomainMessenger).balance, 0); assertEq(address(target).balance, value); - assertEq(l1CrossDomainMessenger.successfulMessages(hash), true); - assertEq(l1CrossDomainMessenger.failedMessages(hash), true); + _assertMessageStatus(hash, true, true); } /// @notice Tests that `relayMessage` should successfully call the target contract after the @@ -833,8 +771,7 @@ contract L1CrossDomainMessenger_Uncategorized_Test is L1CrossDomainMessenger_Tes hex"1111" ); - // Set the value of op.l2Sender() to be the L2 Cross Domain Messenger. - vm.store(address(optimismPortal2), bytes32(senderSlotIndex), bytes32(abi.encode(sender))); + _setPortalL2Sender(sender); // Target should be called with expected data. vm.expectCall(target, hex"1111"); @@ -854,33 +791,24 @@ contract L1CrossDomainMessenger_Uncategorized_Test is L1CrossDomainMessenger_Tes hex"1111" ); - // Message was successfully relayed. - assertEq(l1CrossDomainMessenger.successfulMessages(hash), true); - assertEq(l1CrossDomainMessenger.failedMessages(hash), false); + _assertMessageStatus(hash, true, false); } /// @notice Tests that `relayMessage` should revert if the message is already replayed. function test_relayMessage_legacyOldReplay_reverts() external { address target = address(0xabcd); address sender = Predeploys.L2_CROSS_DOMAIN_MESSENGER; + uint256 nonce = _versionedNonce(0); // Compute the message hash. - bytes32 hash = Hashing.hashCrossDomainMessageV1( - // Using a legacy nonce with version 0. - Encoding.encodeVersionedNonce({ _nonce: 0, _version: 0 }), - sender, - target, - 0, - 0, - hex"1111" - ); + bytes32 hash = Hashing.hashCrossDomainMessageV1(nonce, sender, target, 0, 0, hex"1111"); - // Set the value of op.l2Sender() to be the L2 Cross Domain Messenger. - vm.store(address(optimismPortal2), bytes32(senderSlotIndex), bytes32(abi.encode(sender))); + _setPortalL2Sender(sender); // Mark legacy message as already relayed. - uint256 successfulMessagesSlot = 203; bytes32 oldHash = Hashing.hashCrossDomainMessageV0(target, sender, hex"1111", 0); - bytes32 slot = keccak256(abi.encode(oldHash, successfulMessagesSlot)); + StorageSlot memory successfulMessagesSlot = + ForgeArtifacts.getSlot("L1CrossDomainMessenger", "successfulMessages"); + bytes32 slot = keccak256(abi.encode(oldHash, successfulMessagesSlot.slot)); vm.store(address(l1CrossDomainMessenger), slot, bytes32(uint256(1))); // Expect revert. @@ -888,18 +816,9 @@ contract L1CrossDomainMessenger_Uncategorized_Test is L1CrossDomainMessenger_Tes // Relay the message. vm.prank(address(optimismPortal2)); - l1CrossDomainMessenger.relayMessage( - Encoding.encodeVersionedNonce({ _nonce: 0, _version: 0 }), // nonce - sender, - target, - 0, // value - 0, - hex"1111" - ); + l1CrossDomainMessenger.relayMessage(nonce, sender, target, 0, 0, hex"1111"); - // Message was not relayed. - assertEq(l1CrossDomainMessenger.successfulMessages(hash), false); - assertEq(l1CrossDomainMessenger.failedMessages(hash), false); + _assertMessageStatus(hash, false, false); } /// @notice Tests that `relayMessage` can be retried after a failure with a legacy message. @@ -919,11 +838,9 @@ contract L1CrossDomainMessenger_Uncategorized_Test is L1CrossDomainMessenger_Tes hex"1111" ); - // Set the value of op.l2Sender() to be the L2 Cross Domain Messenger. - vm.store(address(optimismPortal2), bytes32(senderSlotIndex), bytes32(abi.encode(sender))); + _setPortalL2Sender(sender); - // Turn the target into a Reverter. - vm.etch(target, address(new Reverter()).code); + vm.etch(target, type(Reverter).runtimeCode); // Target should be called with expected data. vm.expectCall(target, hex"1111"); @@ -947,8 +864,7 @@ contract L1CrossDomainMessenger_Uncategorized_Test is L1CrossDomainMessenger_Tes // Message failed. assertEq(address(l1CrossDomainMessenger).balance, value); assertEq(address(target).balance, 0); - assertEq(l1CrossDomainMessenger.successfulMessages(hash), false); - assertEq(l1CrossDomainMessenger.failedMessages(hash), true); + _assertMessageStatus(hash, false, true); // Make the target not revert anymore. vm.etch(target, address(0).code); @@ -974,8 +890,7 @@ contract L1CrossDomainMessenger_Uncategorized_Test is L1CrossDomainMessenger_Tes // Message was successfully relayed. assertEq(address(l1CrossDomainMessenger).balance, 0); assertEq(address(target).balance, value); - assertEq(l1CrossDomainMessenger.successfulMessages(hash), true); - assertEq(l1CrossDomainMessenger.failedMessages(hash), true); + _assertMessageStatus(hash, true, true); } /// @notice Tests that `relayMessage` cannot be retried after success with a legacy message. @@ -995,8 +910,7 @@ contract L1CrossDomainMessenger_Uncategorized_Test is L1CrossDomainMessenger_Tes hex"1111" ); - // Set the value of op.l2Sender() to be the L2 Cross Domain Messenger. - vm.store(address(optimismPortal2), bytes32(senderSlotIndex), bytes32(abi.encode(sender))); + _setPortalL2Sender(sender); // Target should be called with expected data. vm.expectCall(target, hex"1111"); @@ -1020,8 +934,7 @@ contract L1CrossDomainMessenger_Uncategorized_Test is L1CrossDomainMessenger_Tes // Message was successfully relayed. assertEq(address(l1CrossDomainMessenger).balance, 0); assertEq(address(target).balance, value); - assertEq(l1CrossDomainMessenger.successfulMessages(hash), true); - assertEq(l1CrossDomainMessenger.failedMessages(hash), false); + _assertMessageStatus(hash, true, false); // Expect a revert. vm.expectRevert("CrossDomainMessenger: message cannot be replayed"); @@ -1055,11 +968,9 @@ contract L1CrossDomainMessenger_Uncategorized_Test is L1CrossDomainMessenger_Tes hex"1111" ); - // Set the value of op.l2Sender() to be the L2 Cross Domain Messenger. - vm.store(address(optimismPortal2), bytes32(senderSlotIndex), bytes32(abi.encode(sender))); + _setPortalL2Sender(sender); - // Turn the target into a Reverter. - vm.etch(target, address(new Reverter()).code); + vm.etch(target, type(Reverter).runtimeCode); // Target should be called with expected data. vm.expectCall(target, hex"1111"); @@ -1079,8 +990,7 @@ contract L1CrossDomainMessenger_Uncategorized_Test is L1CrossDomainMessenger_Tes // Message failed. assertEq(address(l1CrossDomainMessenger).balance, value); assertEq(address(target).balance, 0); - assertEq(l1CrossDomainMessenger.successfulMessages(hash), false); - assertEq(l1CrossDomainMessenger.failedMessages(hash), true); + _assertMessageStatus(hash, false, true); // Make the target not revert anymore. vm.etch(target, address(0).code); @@ -1106,8 +1016,7 @@ contract L1CrossDomainMessenger_Uncategorized_Test is L1CrossDomainMessenger_Tes // Message was successfully relayed. assertEq(address(l1CrossDomainMessenger).balance, 0); assertEq(address(target).balance, value); - assertEq(l1CrossDomainMessenger.successfulMessages(hash), true); - assertEq(l1CrossDomainMessenger.failedMessages(hash), true); + _assertMessageStatus(hash, true, true); // Expect a revert. vm.expectRevert("CrossDomainMessenger: message has already been relayed"); @@ -1124,8 +1033,7 @@ contract L1CrossDomainMessenger_Uncategorized_Test is L1CrossDomainMessenger_Tes ); } - /// @notice Tests that the relayMessage function is able to relay a message successfully by - /// calling the target contract. + /// @notice Tests that relayMessage reverts while the messenger is paused. function test_relayMessage_paused_reverts() external { vm.prank(superchainConfig.guardian()); superchainConfig.pause(address(0)); @@ -1151,25 +1059,25 @@ contract L1CrossDomainMessenger_Uncategorized_Test is L1CrossDomainMessenger_Tes uint256 balanceBeforeThis = address(this).balance; uint256 balanceBeforeMessenger = address(l1CrossDomainMessenger).balance; + bytes32 hash = _reentrancyHash(); + bytes memory message = _reentrancyMessage(); + StorageSlot memory failedMessagesSlot = ForgeArtifacts.getSlot("L1CrossDomainMessenger", "failedMessages"); // A requisite for the attack is that the message has already been attempted and written // to the failedMessages mapping, so that it can be replayed. - vm.store(address(l1CrossDomainMessenger), keccak256(abi.encode(reentrancyHash, 206)), bytes32(uint256(1))); - assertTrue(l1CrossDomainMessenger.failedMessages(reentrancyHash)); + vm.store( + address(l1CrossDomainMessenger), keccak256(abi.encode(hash, failedMessagesSlot.slot)), bytes32(uint256(1)) + ); + assertTrue(l1CrossDomainMessenger.failedMessages(hash)); vm.expectEmit(address(l1CrossDomainMessenger)); - emit FailedRelayedMessage(reentrancyHash); + emit FailedRelayedMessage(hash); l1CrossDomainMessenger.relayMessage( - Encoding.encodeVersionedNonce({ _nonce: 0, _version: 1 }), // nonce - reentrancySender, - reentrancyTarget, - reentrancyMessageValue, - 0, - reentrancySelector + _versionedNonce(1), Predeploys.L2_CROSS_DOMAIN_MESSENGER, address(this), reentrancyMessageValue, 0, message ); // The message hash is not in the successfulMessages mapping. - assertFalse(l1CrossDomainMessenger.successfulMessages(reentrancyHash)); + assertFalse(l1CrossDomainMessenger.successfulMessages(hash)); // The balance of this contract is unchanged. assertEq(address(this).balance, balanceBeforeThis); // The balance of the messenger contract is unchanged. diff --git a/test/L1/L1ERC721Bridge.t.sol b/test/L1/L1ERC721Bridge.t.sol index 6caf0337..1a59d608 100644 --- a/test/L1/L1ERC721Bridge.t.sol +++ b/test/L1/L1ERC721Bridge.t.sol @@ -34,6 +34,8 @@ abstract contract L1ERC721Bridge_TestInit is CommonTest { L1ERC721Bridge_TestERC721_Harness internal localToken; L1ERC721Bridge_TestERC721_Harness internal remoteToken; uint256 internal constant tokenId = 1; + uint32 internal constant DEFAULT_MIN_GAS_LIMIT = 1234; + bytes internal constant EXTRA_DATA = hex"5678"; event ERC721BridgeInitiated( address indexed localToken, @@ -53,17 +55,65 @@ abstract contract L1ERC721Bridge_TestInit is CommonTest { bytes extraData ); - /// @notice Sets up the testing environment. - function setUp() public override { + function _expectBridgeMessage(address _to) internal { + bytes memory message = abi.encodeCall( + IL2ERC721Bridge.finalizeBridgeERC721, + (address(remoteToken), address(localToken), alice, _to, tokenId, EXTRA_DATA) + ); + + vm.expectCall( + address(l1ERC721Bridge.messenger()), + abi.encodeCall( + ICrossDomainMessenger.sendMessage, + (address(l1ERC721Bridge.otherBridge()), message, DEFAULT_MIN_GAS_LIMIT) + ) + ); + } + + function _expectBridgeInitiated(address _to) internal { + vm.expectEmit(address(l1ERC721Bridge)); + emit ERC721BridgeInitiated(address(localToken), address(remoteToken), alice, _to, tokenId, EXTRA_DATA); + } + + function _mockXDomainMessageSender(address _sender) internal { + vm.mockCall( + address(l1ERC721Bridge.messenger()), + abi.encodeCall(ICrossDomainMessenger.xDomainMessageSender, ()), + abi.encode(_sender) + ); + } + + function _mockOtherBridge() internal { + _mockXDomainMessageSender(address(l1ERC721Bridge.otherBridge())); + } + + function _bridgeToken() internal { + vm.prank(alice, alice); + l1ERC721Bridge.bridgeERC721( + address(localToken), address(remoteToken), tokenId, DEFAULT_MIN_GAS_LIMIT, EXTRA_DATA + ); + } + + function _assertTokenEscrowed() internal view { + assertTrue(l1ERC721Bridge.deposits(address(localToken), address(remoteToken), tokenId)); + assertEq(localToken.ownerOf(tokenId), address(l1ERC721Bridge)); + } + + function _assertTokenNotEscrowed(address _owner) internal view { + assertFalse(l1ERC721Bridge.deposits(address(localToken), address(remoteToken), tokenId)); + assertEq(localToken.ownerOf(tokenId), _owner); + } +} + +abstract contract L1ERC721Bridge_Bridge_TestInit is L1ERC721Bridge_TestInit { + function setUp() public virtual override { super.setUp(); localToken = new L1ERC721Bridge_TestERC721_Harness(); remoteToken = new L1ERC721Bridge_TestERC721_Harness(); - // Mint alice a token. localToken.mint(alice, tokenId); - // Approve the bridge to transfer the token. vm.prank(alice); localToken.approve(address(l1ERC721Bridge), tokenId); } @@ -89,6 +139,23 @@ contract L1ERC721Bridge_Constructor_Test is L1ERC721Bridge_TestInit { /// @title L1ERC721Bridge_Initialize_Test /// @notice Test contract for L1ERC721Bridge `initialize` function. contract L1ERC721Bridge_Initialize_Test is L1ERC721Bridge_TestInit { + StorageSlot internal initializedSlot; + + function setUp() public override { + super.setUp(); + + initializedSlot = ForgeArtifacts.getSlot("L1ERC721Bridge", "_initialized"); + } + + function _resetInitialized() internal { + vm.store(address(l1ERC721Bridge), bytes32(initializedSlot.slot), bytes32(0)); + } + + function _initializedValue() internal view returns (uint8) { + bytes32 slotVal = vm.load(address(l1ERC721Bridge), bytes32(initializedSlot.slot)); + return uint8((uint256(slotVal) >> (initializedSlot.offset * 8)) & 0xFF); + } + /// @notice Tests that the proxy is initialized with the correct values. function test_initialize_succeeds() public view { assertEq(address(l1ERC721Bridge.MESSENGER()), address(l1CrossDomainMessenger)); @@ -103,15 +170,7 @@ contract L1ERC721Bridge_Initialize_Test is L1ERC721Bridge_TestInit { /// initialization but confirms that the initValue is not incremented incorrectly if /// an upgrade function is not present. function test_initialize_correctInitializerValue_succeeds() public view { - // Get the slot for _initialized. - StorageSlot memory slot = ForgeArtifacts.getSlot("L1ERC721Bridge", "_initialized"); - - // Get the initializer value. - bytes32 slotVal = vm.load(address(l1ERC721Bridge), bytes32(slot.slot)); - uint8 val = uint8(uint256(slotVal) & 0xFF); - - // Assert that the initializer value matches the expected value. - assertEq(val, l1ERC721Bridge.initVersion()); + assertEq(_initializedValue(), l1ERC721Bridge.initVersion()); } /// @notice Tests that the initialize function reverts if called by a non-proxy admin or owner. @@ -120,11 +179,7 @@ contract L1ERC721Bridge_Initialize_Test is L1ERC721Bridge_TestInit { // Prank as the not ProxyAdmin or ProxyAdmin owner. vm.assume(_sender != address(proxyAdmin) && _sender != proxyAdminOwner); - // Get the slot for _initialized. - StorageSlot memory slot = ForgeArtifacts.getSlot("L1ERC721Bridge", "_initialized"); - - // Set the initialized slot to 0. - vm.store(address(l1ERC721Bridge), bytes32(slot.slot), bytes32(0)); + _resetInitialized(); // Expect the revert with `ProxyAdminOwnedBase_NotProxyAdminOrProxyAdminOwner` selector vm.expectRevert(IProxyAdminOwnedBase.ProxyAdminOwnedBase_NotProxyAdminOrProxyAdminOwner.selector); @@ -185,154 +240,111 @@ contract L1ERC721Bridge_Paused_Test is L1ERC721Bridge_TestInit { /// @title L1ERC721Bridge_FinalizeBridgeERC721_Test /// @notice Test contract for L1ERC721Bridge `finalizeBridgeERC721` function. -contract L1ERC721Bridge_FinalizeBridgeERC721_Test is L1ERC721Bridge_TestInit { +contract L1ERC721Bridge_FinalizeBridgeERC721_Test is L1ERC721Bridge_Bridge_TestInit { /// @notice Tests that the ERC721 bridge successfully finalizes a withdrawal. function test_finalizeBridgeERC721_succeeds() external { - // Bridge the token. - vm.prank(alice, alice); - l1ERC721Bridge.bridgeERC721(address(localToken), address(remoteToken), tokenId, 1234, hex"5678"); + _bridgeToken(); + vm.expectEmit(address(l1ERC721Bridge)); + emit ERC721BridgeFinalized(address(localToken), address(remoteToken), alice, alice, tokenId, EXTRA_DATA); - // Expect an event to be emitted. - vm.expectEmit(true, true, true, true); - emit ERC721BridgeFinalized(address(localToken), address(remoteToken), alice, alice, tokenId, hex"5678"); - - // Finalize a withdrawal. - vm.mockCall( - address(l1CrossDomainMessenger), - abi.encodeCall(l1CrossDomainMessenger.xDomainMessageSender, ()), - abi.encode(Predeploys.L2_ERC721_BRIDGE) + _mockOtherBridge(); + vm.prank(address(l1ERC721Bridge.messenger())); + l1ERC721Bridge.finalizeBridgeERC721( + address(localToken), address(remoteToken), alice, alice, tokenId, EXTRA_DATA ); - vm.prank(address(l1CrossDomainMessenger)); - l1ERC721Bridge.finalizeBridgeERC721(address(localToken), address(remoteToken), alice, alice, tokenId, hex"5678"); - // Token is not locked in the bridge. - assertEq(l1ERC721Bridge.deposits(address(localToken), address(remoteToken), tokenId), false); - assertEq(localToken.ownerOf(tokenId), alice); + _assertTokenNotEscrowed(alice); } /// @notice Tests that the ERC721 bridge finalize reverts when not called by the remote bridge. function test_finalizeBridgeERC721_notViaLocalMessenger_reverts() external { - // Finalize a withdrawal. vm.prank(alice); vm.expectRevert("ERC721Bridge: function can only be called from the other bridge"); - l1ERC721Bridge.finalizeBridgeERC721(address(localToken), address(remoteToken), alice, alice, tokenId, hex"5678"); + l1ERC721Bridge.finalizeBridgeERC721( + address(localToken), address(remoteToken), alice, alice, tokenId, EXTRA_DATA + ); } /// @notice Tests that the ERC721 bridge finalize reverts when not called from the remote /// messenger. function test_finalizeBridgeERC721_notFromRemoteMessenger_reverts() external { - // Finalize a withdrawal. - vm.mockCall( - address(l1CrossDomainMessenger), - abi.encodeCall(l1CrossDomainMessenger.xDomainMessageSender, ()), - abi.encode(alice) - ); - vm.prank(address(l1CrossDomainMessenger)); + _mockXDomainMessageSender(alice); + vm.prank(address(l1ERC721Bridge.messenger())); vm.expectRevert("ERC721Bridge: function can only be called from the other bridge"); - l1ERC721Bridge.finalizeBridgeERC721(address(localToken), address(remoteToken), alice, alice, tokenId, hex"5678"); + l1ERC721Bridge.finalizeBridgeERC721( + address(localToken), address(remoteToken), alice, alice, tokenId, EXTRA_DATA + ); } /// @notice Tests finalizeBridgeERC721 reverts with invalid caller address. /// @param _caller Random address that is not the messenger. function testFuzz_finalizeBridgeERC721_invalidCaller_reverts(address _caller) external { - vm.assume(_caller != address(l1CrossDomainMessenger)); + vm.assume(_caller != address(l1ERC721Bridge.messenger())); vm.prank(_caller); vm.expectRevert("ERC721Bridge: function can only be called from the other bridge"); - l1ERC721Bridge.finalizeBridgeERC721(address(localToken), address(remoteToken), alice, alice, tokenId, hex"5678"); + l1ERC721Bridge.finalizeBridgeERC721( + address(localToken), address(remoteToken), alice, alice, tokenId, EXTRA_DATA + ); } /// @notice Tests that the ERC721 bridge finalize reverts when the local token is set as the /// bridge itself. function test_finalizeBridgeERC721_selfToken_reverts() external { - // Finalize a withdrawal. - vm.mockCall( - address(l1CrossDomainMessenger), - abi.encodeCall(l1CrossDomainMessenger.xDomainMessageSender, ()), - abi.encode(Predeploys.L2_ERC721_BRIDGE) - ); - vm.prank(address(l1CrossDomainMessenger)); + _mockOtherBridge(); + vm.prank(address(l1ERC721Bridge.messenger())); vm.expectRevert("L1ERC721Bridge: local token cannot be self"); l1ERC721Bridge.finalizeBridgeERC721( - address(l1ERC721Bridge), address(remoteToken), alice, alice, tokenId, hex"5678" + address(l1ERC721Bridge), address(remoteToken), alice, alice, tokenId, EXTRA_DATA ); } /// @notice Tests that the ERC721 bridge finalize reverts when the remote token is not escrowed /// in the L1 bridge. function test_finalizeBridgeERC721_notEscrowed_reverts() external { - // Finalize a withdrawal. - vm.mockCall( - address(l1CrossDomainMessenger), - abi.encodeCall(l1CrossDomainMessenger.xDomainMessageSender, ()), - abi.encode(Predeploys.L2_ERC721_BRIDGE) - ); - vm.prank(address(l1CrossDomainMessenger)); + _mockOtherBridge(); + vm.prank(address(l1ERC721Bridge.messenger())); vm.expectRevert("L1ERC721Bridge: Token ID is not escrowed in the L1 Bridge"); - l1ERC721Bridge.finalizeBridgeERC721(address(localToken), address(remoteToken), alice, alice, tokenId, hex"5678"); + l1ERC721Bridge.finalizeBridgeERC721( + address(localToken), address(remoteToken), alice, alice, tokenId, EXTRA_DATA + ); } /// @notice Tests finalizeBridgeERC721 with random valid addresses succeeds after bridging. /// @param _from Random from address for testing. /// @param _to Random to address for testing. function testFuzz_finalizeBridgeERC721_randomAddresses_succeeds(address _from, address _to) external { - // Avoid special predeploy addresses that might cause initialization issues vm.assume(_from != address(0) && _to != address(0)); - vm.assume( - uint160(_from) < uint160(0x4200000000000000000000000000000000000000) - || uint160(_from) > uint160(0x4200000000000000000000000000000000001000) - ); - vm.assume( - uint160(_to) < uint160(0x4200000000000000000000000000000000000000) - || uint160(_to) > uint160(0x4200000000000000000000000000000000001000) - ); + vm.assume(!Predeploys.isPredeployNamespace(_from)); + vm.assume(!Predeploys.isPredeployNamespace(_to)); - // Ensure the to address is an EOA. vm.assume(_to.code.length == 0); - // Bridge the token first. - vm.prank(alice, alice); - l1ERC721Bridge.bridgeERC721(address(localToken), address(remoteToken), tokenId, 1234, hex"5678"); - - // Mock the cross domain messenger. - vm.mockCall( - address(l1CrossDomainMessenger), - abi.encodeCall(l1CrossDomainMessenger.xDomainMessageSender, ()), - abi.encode(Predeploys.L2_ERC721_BRIDGE) - ); + _bridgeToken(); + _mockOtherBridge(); - // Finalize with random addresses. - vm.prank(address(l1CrossDomainMessenger)); - l1ERC721Bridge.finalizeBridgeERC721(address(localToken), address(remoteToken), _from, _to, tokenId, hex"5678"); + vm.prank(address(l1ERC721Bridge.messenger())); + l1ERC721Bridge.finalizeBridgeERC721(address(localToken), address(remoteToken), _from, _to, tokenId, EXTRA_DATA); - // Verify token was transferred to _to. - assertEq(localToken.ownerOf(tokenId), _to); - assertEq(l1ERC721Bridge.deposits(address(localToken), address(remoteToken), tokenId), false); + _assertTokenNotEscrowed(_to); } - /// @notice Ensures that the `bridgeERC721` function reverts when the bridge is paused. + /// @notice Ensures that the `finalizeBridgeERC721` function reverts when the bridge is paused. function test_finalizeBridgeERC721_paused_reverts() external { - /// Sets up the test by pausing the bridge, giving ether to the bridge and mocking the - /// calls to the xDomainMessageSender so that it returns the correct value. - vm.startPrank(systemConfig.superchainConfig().guardian()); - systemConfig.superchainConfig().pause(address(0)); - vm.stopPrank(); + vm.prank(superchainConfig.guardian()); + superchainConfig.pause(address(0)); assertTrue(l1ERC721Bridge.paused()); - vm.mockCall( - address(l1ERC721Bridge.messenger()), - abi.encodeCall(ICrossDomainMessenger.xDomainMessageSender, ()), - abi.encode(address(l1ERC721Bridge.otherBridge())) - ); + _mockOtherBridge(); vm.prank(address(l1ERC721Bridge.messenger())); vm.expectRevert("L1ERC721Bridge: paused"); l1ERC721Bridge.finalizeBridgeERC721({ - _localToken: address(0), - _remoteToken: address(0), - _from: address(0), - _to: address(0), - _tokenId: 0, - _extraData: hex"" + _localToken: address(localToken), + _remoteToken: address(remoteToken), + _from: alice, + _to: alice, + _tokenId: tokenId, + _extraData: EXTRA_DATA }); } } @@ -340,214 +352,139 @@ contract L1ERC721Bridge_FinalizeBridgeERC721_Test is L1ERC721Bridge_TestInit { /// @title L1ERC721Bridge_Uncategorized_Test /// @notice General tests that are not testing any function directly of the `L1ERC721Bridge` /// contract or are testing multiple functions at once. -contract L1ERC721Bridge_Uncategorized_Test is L1ERC721Bridge_TestInit { +contract L1ERC721Bridge_Uncategorized_Test is L1ERC721Bridge_Bridge_TestInit { /// @notice Tests that the ERC721 can be bridged successfully. function test_bridgeERC721_fromEOA_succeeds() public { - // Expect a call to the messenger. - vm.expectCall( - address(l1CrossDomainMessenger), - abi.encodeCall( - l1CrossDomainMessenger.sendMessage, - ( - address(l2ERC721Bridge), - abi.encodeCall( - IL2ERC721Bridge.finalizeBridgeERC721, - (address(remoteToken), address(localToken), alice, alice, tokenId, hex"5678") - ), - 1234 - ) - ) - ); + _expectBridgeMessage(alice); + _expectBridgeInitiated(alice); - // Expect an event to be emitted. - vm.expectEmit(true, true, true, true); - emit ERC721BridgeInitiated(address(localToken), address(remoteToken), alice, alice, tokenId, hex"5678"); + _bridgeToken(); - // Bridge the token. - vm.prank(alice, alice); - l1ERC721Bridge.bridgeERC721(address(localToken), address(remoteToken), tokenId, 1234, hex"5678"); - - // Token is locked in the bridge. - assertEq(l1ERC721Bridge.deposits(address(localToken), address(remoteToken), tokenId), true); - assertEq(localToken.ownerOf(tokenId), address(l1ERC721Bridge)); + _assertTokenEscrowed(); } /// @notice Tests that the ERC721 can be bridged successfully. function test_bridgeERC721_fromEOA7702_succeeds() public { - // Expect a call to the messenger. - vm.expectCall( - address(l1CrossDomainMessenger), - abi.encodeCall( - l1CrossDomainMessenger.sendMessage, - ( - address(l2ERC721Bridge), - abi.encodeCall( - IL2ERC721Bridge.finalizeBridgeERC721, - (address(remoteToken), address(localToken), alice, alice, tokenId, hex"5678") - ), - 1234 - ) - ) - ); - - // Expect an event to be emitted. - vm.expectEmit(true, true, true, true); - emit ERC721BridgeInitiated(address(localToken), address(remoteToken), alice, alice, tokenId, hex"5678"); + _expectBridgeMessage(alice); + _expectBridgeInitiated(alice); // Set alice to have 7702 code (delegation target must be non-zero; address(0) is treated as revocation per // EIP-7702). vm.etch(alice, abi.encodePacked(hex"EF0100", address(1))); - // Bridge the token. vm.prank(alice); - l1ERC721Bridge.bridgeERC721(address(localToken), address(remoteToken), tokenId, 1234, hex"5678"); + l1ERC721Bridge.bridgeERC721( + address(localToken), address(remoteToken), tokenId, DEFAULT_MIN_GAS_LIMIT, EXTRA_DATA + ); - // Token is locked in the bridge. - assertEq(l1ERC721Bridge.deposits(address(localToken), address(remoteToken), tokenId), true); - assertEq(localToken.ownerOf(tokenId), address(l1ERC721Bridge)); + _assertTokenEscrowed(); } /// @notice Tests that the ERC721 bridge reverts for non externally owned accounts. function test_bridgeERC721_fromContract_reverts() external { - // Bridge the token. vm.etch(alice, hex"01"); vm.prank(alice); vm.expectRevert("ERC721Bridge: account is not externally owned"); - l1ERC721Bridge.bridgeERC721(address(localToken), address(remoteToken), tokenId, 1234, hex"5678"); + l1ERC721Bridge.bridgeERC721( + address(localToken), address(remoteToken), tokenId, DEFAULT_MIN_GAS_LIMIT, EXTRA_DATA + ); - // Token is not locked in the bridge. - assertEq(l1ERC721Bridge.deposits(address(localToken), address(remoteToken), tokenId), false); - assertEq(localToken.ownerOf(tokenId), alice); + _assertTokenNotEscrowed(alice); } /// @notice Tests that the ERC721 bridge reverts for a zero address local token. function test_bridgeERC721_localTokenZeroAddress_reverts() external { - // Bridge the token. vm.prank(alice, alice); vm.expectRevert(); // nosemgrep: sol-safety-expectrevert-no-args - l1ERC721Bridge.bridgeERC721(address(0), address(remoteToken), tokenId, 1234, hex"5678"); + l1ERC721Bridge.bridgeERC721(address(0), address(remoteToken), tokenId, DEFAULT_MIN_GAS_LIMIT, EXTRA_DATA); - // Token is not locked in the bridge. - assertEq(l1ERC721Bridge.deposits(address(localToken), address(remoteToken), tokenId), false); - assertEq(localToken.ownerOf(tokenId), alice); + _assertTokenNotEscrowed(alice); } /// @notice Tests that the ERC721 bridge reverts for a zero address remote token. function test_bridgeERC721_remoteTokenZeroAddress_reverts() external { - // Bridge the token. vm.prank(alice, alice); vm.expectRevert("L1ERC721Bridge: remote token cannot be address(0)"); - l1ERC721Bridge.bridgeERC721(address(localToken), address(0), tokenId, 1234, hex"5678"); + l1ERC721Bridge.bridgeERC721(address(localToken), address(0), tokenId, DEFAULT_MIN_GAS_LIMIT, EXTRA_DATA); - // Token is not locked in the bridge. - assertEq(l1ERC721Bridge.deposits(address(localToken), address(remoteToken), tokenId), false); - assertEq(localToken.ownerOf(tokenId), alice); + _assertTokenNotEscrowed(alice); } /// @notice Tests bridgeERC721 with various gas limits succeeds. /// @param _minGasLimit Random gas limit for testing. function testFuzz_bridgeERC721_variousGasLimits_succeeds(uint32 _minGasLimit) external { - // Bound gas limit to avoid out of gas errors _minGasLimit = uint32(bound(uint256(_minGasLimit), 21000, 10000000)); - // Bridge the token. vm.prank(alice, alice); - l1ERC721Bridge.bridgeERC721(address(localToken), address(remoteToken), tokenId, _minGasLimit, hex"5678"); + l1ERC721Bridge.bridgeERC721(address(localToken), address(remoteToken), tokenId, _minGasLimit, EXTRA_DATA); - // Token is locked in the bridge. - assertEq(l1ERC721Bridge.deposits(address(localToken), address(remoteToken), tokenId), true); - assertEq(localToken.ownerOf(tokenId), address(l1ERC721Bridge)); + _assertTokenEscrowed(); } /// @notice Tests that the ERC721 bridge reverts for an incorrect owner. function test_bridgeERC721_wrongOwner_reverts() external { - // Bridge the token. vm.prank(bob, bob); vm.expectRevert("ERC721: transfer from incorrect owner"); - l1ERC721Bridge.bridgeERC721(address(localToken), address(remoteToken), tokenId, 1234, hex"5678"); + l1ERC721Bridge.bridgeERC721( + address(localToken), address(remoteToken), tokenId, DEFAULT_MIN_GAS_LIMIT, EXTRA_DATA + ); - // Token is not locked in the bridge. - assertEq(l1ERC721Bridge.deposits(address(localToken), address(remoteToken), tokenId), false); - assertEq(localToken.ownerOf(tokenId), alice); + _assertTokenNotEscrowed(alice); } /// @notice Tests that the ERC721 bridge successfully sends a token to a different address than /// the owner. function test_bridgeERC721To_succeeds() external { - // Expect a call to the messenger. - vm.expectCall( - address(l1CrossDomainMessenger), - abi.encodeCall( - l1CrossDomainMessenger.sendMessage, - ( - address(Predeploys.L2_ERC721_BRIDGE), - abi.encodeCall( - IL2ERC721Bridge.finalizeBridgeERC721, - (address(remoteToken), address(localToken), alice, bob, tokenId, hex"5678") - ), - 1234 - ) - ) - ); + _expectBridgeMessage(bob); + _expectBridgeInitiated(bob); - // Expect an event to be emitted. - vm.expectEmit(true, true, true, true); - emit ERC721BridgeInitiated(address(localToken), address(remoteToken), alice, bob, tokenId, hex"5678"); - - // Bridge the token. vm.prank(alice); - l1ERC721Bridge.bridgeERC721To(address(localToken), address(remoteToken), bob, tokenId, 1234, hex"5678"); + l1ERC721Bridge.bridgeERC721To( + address(localToken), address(remoteToken), bob, tokenId, DEFAULT_MIN_GAS_LIMIT, EXTRA_DATA + ); - // Token is locked in the bridge. - assertEq(l1ERC721Bridge.deposits(address(localToken), address(remoteToken), tokenId), true); - assertEq(localToken.ownerOf(tokenId), address(l1ERC721Bridge)); + _assertTokenEscrowed(); } /// @notice Tests that the ERC721 bridge reverts for non externally owned accounts when sending /// to a different address than the owner. function test_bridgeERC721To_localTokenZeroAddress_reverts() external { - // Bridge the token. vm.prank(alice); vm.expectRevert(); // nosemgrep: sol-safety-expectrevert-no-args - l1ERC721Bridge.bridgeERC721To(address(0), address(remoteToken), bob, tokenId, 1234, hex"5678"); + l1ERC721Bridge.bridgeERC721To(address(0), address(remoteToken), bob, tokenId, DEFAULT_MIN_GAS_LIMIT, EXTRA_DATA); - // Token is not locked in the bridge. - assertEq(l1ERC721Bridge.deposits(address(localToken), address(remoteToken), tokenId), false); - assertEq(localToken.ownerOf(tokenId), alice); + _assertTokenNotEscrowed(alice); } /// @notice Tests that the ERC721 bridge reverts for a zero address remote token when sending /// to a different address than the owner. function test_bridgeERC721To_remoteTokenZeroAddress_reverts() external { - // Bridge the token. vm.prank(alice); vm.expectRevert("L1ERC721Bridge: remote token cannot be address(0)"); - l1ERC721Bridge.bridgeERC721To(address(localToken), address(0), bob, tokenId, 1234, hex"5678"); + l1ERC721Bridge.bridgeERC721To(address(localToken), address(0), bob, tokenId, DEFAULT_MIN_GAS_LIMIT, EXTRA_DATA); - // Token is not locked in the bridge. - assertEq(l1ERC721Bridge.deposits(address(localToken), address(remoteToken), tokenId), false); - assertEq(localToken.ownerOf(tokenId), alice); + _assertTokenNotEscrowed(alice); } /// @notice Tests that the ERC721 bridge reverts for an incorrect owner when sending to a /// different address than the owner. function test_bridgeERC721To_wrongOwner_reverts() external { - // Bridge the token. vm.prank(bob); vm.expectRevert("ERC721: transfer from incorrect owner"); - l1ERC721Bridge.bridgeERC721To(address(localToken), address(remoteToken), bob, tokenId, 1234, hex"5678"); + l1ERC721Bridge.bridgeERC721To( + address(localToken), address(remoteToken), bob, tokenId, DEFAULT_MIN_GAS_LIMIT, EXTRA_DATA + ); - // Token is not locked in the bridge. - assertEq(l1ERC721Bridge.deposits(address(localToken), address(remoteToken), tokenId), false); - assertEq(localToken.ownerOf(tokenId), alice); + _assertTokenNotEscrowed(alice); } /// @notice Tests that `bridgeERC721To` reverts if the to address is the zero address. function test_bridgeERC721To_toZeroAddress_reverts() external { - // Bridge the token. vm.prank(bob); vm.expectRevert("ERC721Bridge: nft recipient cannot be address(0)"); - l1ERC721Bridge.bridgeERC721To(address(localToken), address(remoteToken), address(0), tokenId, 1234, hex"5678"); + l1ERC721Bridge.bridgeERC721To( + address(localToken), address(remoteToken), address(0), tokenId, DEFAULT_MIN_GAS_LIMIT, EXTRA_DATA + ); } /// @notice Tests bridgeERC721To with random valid recipient addresses. @@ -555,28 +492,13 @@ contract L1ERC721Bridge_Uncategorized_Test is L1ERC721Bridge_TestInit { function testFuzz_bridgeERC721To_validRecipient_succeeds(address _to) external { vm.assume(_to != address(0)); - // Expect a call to the messenger. - vm.expectCall( - address(l1CrossDomainMessenger), - abi.encodeCall( - l1CrossDomainMessenger.sendMessage, - ( - address(Predeploys.L2_ERC721_BRIDGE), - abi.encodeCall( - IL2ERC721Bridge.finalizeBridgeERC721, - (address(remoteToken), address(localToken), alice, _to, tokenId, hex"5678") - ), - 1234 - ) - ) - ); + _expectBridgeMessage(_to); - // Bridge the token. vm.prank(alice); - l1ERC721Bridge.bridgeERC721To(address(localToken), address(remoteToken), _to, tokenId, 1234, hex"5678"); + l1ERC721Bridge.bridgeERC721To( + address(localToken), address(remoteToken), _to, tokenId, DEFAULT_MIN_GAS_LIMIT, EXTRA_DATA + ); - // Token is locked in the bridge. - assertEq(l1ERC721Bridge.deposits(address(localToken), address(remoteToken), tokenId), true); - assertEq(localToken.ownerOf(tokenId), address(l1ERC721Bridge)); + _assertTokenEscrowed(); } } diff --git a/test/L1/L1StandardBridge.t.sol b/test/L1/L1StandardBridge.t.sol index b0b414b3..20608777 100644 --- a/test/L1/L1StandardBridge.t.sol +++ b/test/L1/L1StandardBridge.t.sol @@ -2,7 +2,6 @@ pragma solidity 0.8.15; // Testing -import { stdStorage, StdStorage } from "lib/forge-std/src/Test.sol"; import { CommonTest } from "test/setup/CommonTest.sol"; import { ForgeArtifacts, StorageSlot } from "scripts/libraries/ForgeArtifacts.sol"; @@ -26,6 +25,40 @@ import { IProxyAdminOwnedBase } from "interfaces/L1/IProxyAdminOwnedBase.sol"; /// @title L1StandardBridge_TestInit /// @notice Reusable test initialization for `L1StandardBridge` tests. abstract contract L1StandardBridge_TestInit is CommonTest { + function _assertETHBridgeCustody( + uint256 _portalBalanceBefore, + uint256 _ethLockboxBalanceBefore, + uint256 _amount + ) + internal + view + { + if (isSysFeatureEnabled(Features.ETH_LOCKBOX)) { + assertEq(address(optimismPortal2).balance, _portalBalanceBefore); + assertEq(address(ethLockbox).balance, _ethLockboxBalanceBefore + _amount); + } else { + assertEq(address(optimismPortal2).balance, _portalBalanceBefore + _amount); + } + } + + function _mockXDomainMessageSender(address _sender) internal { + vm.mockCall( + address(l1StandardBridge.messenger()), + abi.encodeCall(ICrossDomainMessenger.xDomainMessageSender, ()), + abi.encode(_sender) + ); + } + + function _mockOtherBridge() internal { + _mockXDomainMessageSender(address(l1StandardBridge.OTHER_BRIDGE())); + } + + function _setupFinalizeBridgeETH() internal returns (address messenger) { + messenger = address(l1StandardBridge.messenger()); + _mockOtherBridge(); + vm.deal(messenger, 100); + } + /// @notice Asserts the expected calls and events for bridging ETH depending on whether the /// bridge call is legacy or not. function _preBridgeETH(bool isLegacy, uint256 value) internal { @@ -33,7 +66,6 @@ abstract contract L1StandardBridge_TestInit is CommonTest { assertEq(address(optimismPortal2).balance, 0, "OptimismPortal2 balance should be 0"); } uint256 nonce = l1CrossDomainMessenger.messageNonce(); - uint256 version = 0; // Internal constant in the OptimismPortal: DEPOSIT_VERSION address l1MessengerAliased = AddressAliasHelper.applyL1ToL2Alias(address(l1CrossDomainMessenger)); bytes memory message = abi.encodeCall(StandardBridge.finalizeBridgeETH, (alice, alice, value, hex"dead")); @@ -68,23 +100,20 @@ abstract contract L1StandardBridge_TestInit is CommonTest { ) ); - bytes memory opaqueData = abi.encodePacked(uint256(value), uint256(value), baseGas, false, innerMessage); - vm.expectEmit(address(l1StandardBridge)); emit ETHDepositInitiated(alice, alice, value, hex"dead"); vm.expectEmit(address(l1StandardBridge)); emit ETHBridgeInitiated(alice, alice, value, hex"dead"); - // OptimismPortal emits a TransactionDeposited event on `depositTransaction` call vm.expectEmit(address(optimismPortal2)); - emit TransactionDeposited(l1MessengerAliased, address(l2CrossDomainMessenger), version, opaqueData); + emitTransactionDeposited( + l1MessengerAliased, address(l2CrossDomainMessenger), value, value, baseGas, false, innerMessage + ); - // SentMessage event emitted by the CrossDomainMessenger vm.expectEmit(address(l1CrossDomainMessenger)); emit SentMessage(address(l2StandardBridge), address(l1StandardBridge), message, nonce, 50000); - // SentMessageExtension1 event emitted by the CrossDomainMessenger vm.expectEmit(address(l1CrossDomainMessenger)); emit SentMessageExtension1(address(l1StandardBridge), value); @@ -95,7 +124,6 @@ abstract contract L1StandardBridge_TestInit is CommonTest { /// depending on whether the bridge call is legacy or not. function _preBridgeETHTo(bool isLegacy, uint256 value) internal { uint256 nonce = l1CrossDomainMessenger.messageNonce(); - uint256 version = 0; // Internal constant in the OptimismPortal: DEPOSIT_VERSION address l1MessengerAliased = AddressAliasHelper.applyL1ToL2Alias(address(l1CrossDomainMessenger)); if (isLegacy) { @@ -110,8 +138,6 @@ abstract contract L1StandardBridge_TestInit is CommonTest { bytes memory message = abi.encodeCall(StandardBridge.finalizeBridgeETH, (alice, bob, value, hex"dead")); - // the L1 bridge should call - // L1CrossDomainMessenger.sendMessage vm.expectCall( address(l1CrossDomainMessenger), abi.encodeCall(ICrossDomainMessenger.sendMessage, (address(l2StandardBridge), message, 60000)) @@ -131,27 +157,23 @@ abstract contract L1StandardBridge_TestInit is CommonTest { ) ); - bytes memory opaqueData = abi.encodePacked(uint256(value), uint256(value), baseGas, false, innerMessage); - vm.expectEmit(address(l1StandardBridge)); emit ETHDepositInitiated(alice, bob, value, hex"dead"); vm.expectEmit(address(l1StandardBridge)); emit ETHBridgeInitiated(alice, bob, value, hex"dead"); - // OptimismPortal emits a TransactionDeposited event on `depositTransaction` call vm.expectEmit(address(optimismPortal2)); - emit TransactionDeposited(l1MessengerAliased, address(l2CrossDomainMessenger), version, opaqueData); + emitTransactionDeposited( + l1MessengerAliased, address(l2CrossDomainMessenger), value, value, baseGas, false, innerMessage + ); - // SentMessage event emitted by the CrossDomainMessenger vm.expectEmit(address(l1CrossDomainMessenger)); emit SentMessage(address(l2StandardBridge), address(l1StandardBridge), message, nonce, 60000); - // SentMessageExtension1 event emitted by the CrossDomainMessenger vm.expectEmit(address(l1CrossDomainMessenger)); emit SentMessageExtension1(address(l1StandardBridge), value); - // deposit eth to bob vm.prank(alice, alice); } } @@ -178,6 +200,23 @@ contract L1StandardBridge_Constructor_Test is CommonTest { /// @title L1StandardBridge_Initialize_Test /// @notice Tests the `initialize` function of the `L1StandardBridge` contract. contract L1StandardBridge_Initialize_Test is CommonTest { + StorageSlot internal initializedSlot; + + function setUp() public override { + super.setUp(); + + initializedSlot = ForgeArtifacts.getSlot("L1StandardBridge", "_initialized"); + } + + function _resetInitialized() internal { + vm.store(address(l1StandardBridge), bytes32(initializedSlot.slot), bytes32(0)); + } + + function _initializedValue() internal view returns (uint8) { + bytes32 slotVal = vm.load(address(l1StandardBridge), bytes32(initializedSlot.slot)); + return uint8((uint256(slotVal) >> (initializedSlot.offset * 8)) & 0xFF); + } + /// @notice Test that the initialize function sets the correct values. function test_initialize_succeeds() external view { assertEq(address(l1StandardBridge.systemConfig()), address(systemConfig)); @@ -193,8 +232,7 @@ contract L1StandardBridge_Initialize_Test is CommonTest { function testFuzz_initialize_notProxyAdminOrProxyAdminOwner_reverts(address _sender) public { vm.assume(_sender != address(proxyAdmin) && _sender != proxyAdminOwner); - StorageSlot memory slot = ForgeArtifacts.getSlot("L1StandardBridge", "_initialized"); - vm.store(address(l1StandardBridge), bytes32(slot.slot), bytes32(0)); + _resetInitialized(); vm.expectRevert(IProxyAdminOwnedBase.ProxyAdminOwnedBase_NotProxyAdminOrProxyAdminOwner.selector); vm.prank(_sender); @@ -205,36 +243,27 @@ contract L1StandardBridge_Initialize_Test is CommonTest { /// but confirms that the initValue is not incremented incorrectly if an upgrade /// function is not present. function test_initialize_correctInitializerValue_succeeds() public view { - // Get the slot for _initialized. - StorageSlot memory slot = ForgeArtifacts.getSlot("L1StandardBridge", "_initialized"); - - // Get the initializer value. - bytes32 slotVal = vm.load(address(l1StandardBridge), bytes32(slot.slot)); - uint8 val = uint8(uint256(slotVal) & 0xFF); - - // Assert that the initializer value matches the expected value. - assertEq(val, l1StandardBridge.initVersion()); + assertEq(_initializedValue(), l1StandardBridge.initVersion()); } } /// @title L1StandardBridge_Paused_Test /// @notice Tests the `paused` function of the `L1StandardBridge` contract. -contract L1StandardBridge_Paused_Test is CommonTest { - /// @notice Sets up the test by pausing the bridge, giving ether to the bridge and mocking the - /// calls to the xDomainMessageSender so that it returns the correct value. - function _setupPausedBridge() internal { +contract L1StandardBridge_Paused_Test is L1StandardBridge_TestInit { + /// @notice Pauses the bridge and mocks the xDomainMessageSender to return the other bridge. + function _pauseBridge() internal { vm.startPrank(systemConfig.guardian()); systemConfig.superchainConfig().pause(address(0)); vm.stopPrank(); assertTrue(l1StandardBridge.paused()); - vm.deal(address(l1StandardBridge.messenger()), 1 ether); + _mockOtherBridge(); + } - vm.mockCall( - address(l1StandardBridge.messenger()), - abi.encodeCall(ICrossDomainMessenger.xDomainMessageSender, ()), - abi.encode(address(l1StandardBridge.otherBridge())) - ); + /// @notice Pauses the bridge and funds the messenger for payable ETH finalization tests. + function _setupPausedBridge() internal { + _pauseBridge(); + vm.deal(address(l1StandardBridge.messenger()), 1 ether); } /// @notice Verifies that the `paused` accessor returns the same value as the `paused` function @@ -289,7 +318,7 @@ contract L1StandardBridge_Paused_Test is CommonTest { /// @notice Confirms that the `finalizeERC20Withdrawal` function reverts when the bridge is /// paused. function test_paused_finalizeERC20Withdrawal_reverts() external { - _setupPausedBridge(); + _pauseBridge(); vm.prank(address(l1StandardBridge.messenger())); vm.expectRevert("StandardBridge: paused"); @@ -305,7 +334,7 @@ contract L1StandardBridge_Paused_Test is CommonTest { /// @notice Confirms that the `finalizeBridgeERC20` function reverts when the bridge is paused. function test_paused_finalizeBridgeERC20_reverts() external { - _setupPausedBridge(); + _pauseBridge(); vm.prank(address(l1StandardBridge.messenger())); vm.expectRevert("StandardBridge: paused"); @@ -322,7 +351,7 @@ contract L1StandardBridge_Paused_Test is CommonTest { /// @title L1StandardBridge_Receive_Test /// @notice Tests the `receive` function of the `L1StandardBridge` contract. -contract L1StandardBridge_Receive_Test is CommonTest { +contract L1StandardBridge_Receive_Test is L1StandardBridge_TestInit { /// @notice Tests receive bridges ETH successfully. function test_receive_succeeds() external { skipIfSysFeatureEnabled(Features.CUSTOM_GAS_TOKEN); @@ -350,14 +379,9 @@ contract L1StandardBridge_Receive_Test is CommonTest { vm.prank(alice, alice); (bool success,) = address(l1StandardBridge).call{ value: 100 }(hex""); - assertEq(success, true); + assertTrue(success); - if (isSysFeatureEnabled(Features.ETH_LOCKBOX)) { - assertEq(address(optimismPortal2).balance, portalBalanceBefore); - assertEq(address(ethLockbox).balance, ethLockboxBalanceBefore + 100); - } else { - assertEq(address(optimismPortal2).balance, portalBalanceBefore + 100); - } + _assertETHBridgeCustody(portalBalanceBefore, ethLockboxBalanceBefore, 100); } /// @notice Verifies receive function reverts when called by contracts @@ -365,7 +389,7 @@ contract L1StandardBridge_Receive_Test is CommonTest { vm.etch(alice, hex"ffff"); vm.deal(alice, 100); vm.prank(alice); - vm.expectRevert(bytes("StandardBridge: function can only be called from an EOA")); + vm.expectRevert("StandardBridge: function can only be called from an EOA"); (bool revertsAsExpected,) = address(l1StandardBridge).call{ value: 100 }(hex""); assertTrue(revertsAsExpected, "expectRevert: call did not revert"); } @@ -386,31 +410,21 @@ contract L1StandardBridge_DepositETH_Test is L1StandardBridge_TestInit { uint256 ethLockboxBalanceBefore = address(ethLockbox).balance; l1StandardBridge.depositETH{ value: 500 }(50000, hex"dead"); - if (isSysFeatureEnabled(Features.ETH_LOCKBOX)) { - assertEq(address(optimismPortal2).balance, portalBalanceBefore); - assertEq(address(ethLockbox).balance, ethLockboxBalanceBefore + 500); - } else { - assertEq(address(optimismPortal2).balance, portalBalanceBefore + 500); - } + _assertETHBridgeCustody(portalBalanceBefore, ethLockboxBalanceBefore, 500); } /// @notice Tests that depositing ETH succeeds for an EOA using 7702 delegation. function test_depositETH_fromEOA7702_succeeds() external { skipIfSysFeatureEnabled(Features.CUSTOM_GAS_TOKEN); - // Set alice to have 7702 code. - vm.etch(alice, abi.encodePacked(hex"EF0100", address(0))); + // EIP-7702 treats address(0) as revocation, so use a non-zero delegation target. + vm.etch(alice, abi.encodePacked(hex"EF0100", address(1))); _preBridgeETH({ isLegacy: true, value: 500 }); uint256 portalBalanceBefore = address(optimismPortal2).balance; uint256 ethLockboxBalanceBefore = address(ethLockbox).balance; l1StandardBridge.depositETH{ value: 500 }(50000, hex"dead"); - if (isSysFeatureEnabled(Features.ETH_LOCKBOX)) { - assertEq(address(optimismPortal2).balance, portalBalanceBefore); - assertEq(address(ethLockbox).balance, ethLockboxBalanceBefore + 500); - } else { - assertEq(address(optimismPortal2).balance, portalBalanceBefore + 500); - } + _assertETHBridgeCustody(portalBalanceBefore, ethLockboxBalanceBefore, 500); } /// @notice Tests that depositing ETH reverts if the call is not from an EOA. @@ -437,12 +451,7 @@ contract L1StandardBridge_DepositETHTo_Test is L1StandardBridge_TestInit { uint256 ethLockboxBalanceBefore = address(ethLockbox).balance; l1StandardBridge.depositETHTo{ value: 600 }(bob, 60000, hex"dead"); - if (isSysFeatureEnabled(Features.ETH_LOCKBOX)) { - assertEq(address(optimismPortal2).balance, portalBalanceBefore); - assertEq(address(ethLockbox).balance, ethLockboxBalanceBefore + 600); - } else { - assertEq(address(optimismPortal2).balance, portalBalanceBefore + 600); - } + _assertETHBridgeCustody(portalBalanceBefore, ethLockboxBalanceBefore, 600); } /// @notice Verifies depositETHTo succeeds with various recipients and amounts @@ -461,25 +470,13 @@ contract L1StandardBridge_DepositETHTo_Test is L1StandardBridge_TestInit { vm.prank(alice); l1StandardBridge.depositETHTo{ value: _amount }(_to, 60000, hex"dead"); - if (isSysFeatureEnabled(Features.ETH_LOCKBOX)) { - assertEq(address(ethLockbox).balance, ethLockboxBalanceBefore + _amount); - } else { - assertEq(address(optimismPortal2).balance, portalBalanceBefore + _amount); - } + _assertETHBridgeCustody(portalBalanceBefore, ethLockboxBalanceBefore, _amount); } } /// @title L1StandardBridge_DepositERC20_Test /// @notice Tests the `depositERC20` function of the `L1StandardBridge` contract. contract L1StandardBridge_DepositERC20_Test is CommonTest { - using stdStorage for StdStorage; - - // depositERC20 - // - updates bridge.deposits - // - emits ERC20DepositInitiated - // - calls optimismPortal.depositTransaction - // - only callable by EOA - /// @notice Tests that depositing ERC20 to the bridge succeeds. /// Bridge deposits are updated. /// Emits ERC20DepositInitiated event. @@ -487,13 +484,11 @@ contract L1StandardBridge_DepositERC20_Test is CommonTest { /// Only EOA can call depositERC20. function test_depositERC20_succeeds() external { uint256 nonce = l1CrossDomainMessenger.messageNonce(); - uint256 version = 0; // Internal constant in the OptimismPortal: DEPOSIT_VERSION address l1MessengerAliased = AddressAliasHelper.applyL1ToL2Alias(address(l1CrossDomainMessenger)); - // Deal Alice's ERC20 State - deal(address(L1Token), alice, 100000, true); + deal(address(L1Token), alice, 100000); vm.prank(alice); - L1Token.approve(address(l1StandardBridge), type(uint256).max); + L1Token.approve(address(l1StandardBridge), 100); // The l1StandardBridge should transfer alice's tokens to itself vm.expectCall(address(L1Token), abi.encodeCall(ERC20.transferFrom, (alice, address(l1StandardBridge), 100))); @@ -521,24 +516,20 @@ contract L1StandardBridge_DepositERC20_Test is CommonTest { ) ); - bytes memory opaqueData = abi.encodePacked(uint256(0), uint256(0), baseGas, false, innerMessage); - - // Should emit both the bedrock and legacy events vm.expectEmit(address(l1StandardBridge)); emit ERC20DepositInitiated(address(L1Token), address(L2Token), alice, alice, 100, hex""); vm.expectEmit(address(l1StandardBridge)); emit ERC20BridgeInitiated(address(L1Token), address(L2Token), alice, alice, 100, hex""); - // OptimismPortal emits a TransactionDeposited event on `depositTransaction` call vm.expectEmit(address(optimismPortal2)); - emit TransactionDeposited(l1MessengerAliased, address(l2CrossDomainMessenger), version, opaqueData); + emitTransactionDeposited( + l1MessengerAliased, address(l2CrossDomainMessenger), 0, 0, baseGas, false, innerMessage + ); - // SentMessage event emitted by the CrossDomainMessenger vm.expectEmit(address(l1CrossDomainMessenger)); emit SentMessage(address(l2StandardBridge), address(l1StandardBridge), message, nonce, 10000); - // SentMessageExtension1 event emitted by the CrossDomainMessenger vm.expectEmit(address(l1CrossDomainMessenger)); emit SentMessageExtension1(address(l1StandardBridge), 0); @@ -564,7 +555,7 @@ contract L1StandardBridge_DepositERC20_Test is CommonTest { _amount = bound(_amount, 1, 1000000); _gasLimit = uint32(bound(uint256(_gasLimit), 21000, 10000000)); - deal(address(L1Token), alice, _amount, true); + deal(address(L1Token), alice, _amount); vm.prank(alice); L1Token.approve(address(l1StandardBridge), _amount); @@ -584,7 +575,6 @@ contract L1StandardBridge_DepositERC20To_Test is CommonTest { /// Contracts can call depositERC20. function test_depositERC20To_succeeds() external { uint256 nonce = l1CrossDomainMessenger.messageNonce(); - uint256 version = 0; // Internal constant in the OptimismPortal: DEPOSIT_VERSION address l1MessengerAliased = AddressAliasHelper.applyL1ToL2Alias(address(l1CrossDomainMessenger)); bytes memory message = abi.encodeCall( @@ -597,29 +587,25 @@ contract L1StandardBridge_DepositERC20To_Test is CommonTest { ); uint64 baseGas = l1CrossDomainMessenger.baseGas(message, 10000); - bytes memory opaqueData = abi.encodePacked(uint256(0), uint256(0), baseGas, false, innerMessage); - - deal(address(L1Token), alice, 100000, true); + deal(address(L1Token), alice, 100000); vm.prank(alice); - L1Token.approve(address(l1StandardBridge), type(uint256).max); + L1Token.approve(address(l1StandardBridge), 1000); - // Should emit both the bedrock and legacy events vm.expectEmit(address(l1StandardBridge)); emit ERC20DepositInitiated(address(L1Token), address(L2Token), alice, bob, 1000, hex""); vm.expectEmit(address(l1StandardBridge)); emit ERC20BridgeInitiated(address(L1Token), address(L2Token), alice, bob, 1000, hex""); - // OptimismPortal emits a TransactionDeposited event on `depositTransaction` call vm.expectEmit(address(optimismPortal2)); - emit TransactionDeposited(l1MessengerAliased, address(l2CrossDomainMessenger), version, opaqueData); + emitTransactionDeposited( + l1MessengerAliased, address(l2CrossDomainMessenger), 0, 0, baseGas, false, innerMessage + ); - // SentMessage event emitted by the CrossDomainMessenger vm.expectEmit(address(l1CrossDomainMessenger)); emit SentMessage(address(l2StandardBridge), address(l1StandardBridge), message, nonce, 10000); - // SentMessageExtension1 event emitted by the CrossDomainMessenger vm.expectEmit(address(l1CrossDomainMessenger)); emit SentMessageExtension1(address(l1StandardBridge), 0); @@ -645,10 +631,6 @@ contract L1StandardBridge_DepositERC20To_Test is CommonTest { /// @notice Verifies depositERC20To succeeds with zero amount function test_depositERC20To_zeroAmount_succeeds() external { - deal(address(L1Token), alice, 1000, true); - vm.prank(alice); - L1Token.approve(address(l1StandardBridge), 0); - vm.prank(alice); l1StandardBridge.depositERC20To(address(L1Token), address(L2Token), bob, 0, 10000, hex""); assertEq(l1StandardBridge.deposits(address(L1Token), address(L2Token)), 0); @@ -657,9 +639,7 @@ contract L1StandardBridge_DepositERC20To_Test is CommonTest { /// @title L1StandardBridge_FinalizeETHWithdrawal_Test /// @notice Tests the `finalizeETHWithdrawal` function of the `L1StandardBridge` contract. -contract L1StandardBridge_FinalizeETHWithdrawal_Test is CommonTest { - using stdStorage for StdStorage; - +contract L1StandardBridge_FinalizeETHWithdrawal_Test is L1StandardBridge_TestInit { /// @notice Tests that finalizing an ETH withdrawal succeeds. /// Emits ETHWithdrawalFinalized event. /// Only callable by the L2 bridge. @@ -674,14 +654,8 @@ contract L1StandardBridge_FinalizeETHWithdrawal_Test is CommonTest { vm.expectCall(alice, hex""); - vm.mockCall( - address(l1StandardBridge.messenger()), - abi.encodeCall(ICrossDomainMessenger.xDomainMessageSender, ()), - abi.encode(address(l1StandardBridge.OTHER_BRIDGE())) - ); - // ensure that the messenger has ETH to call with - vm.deal(address(l1StandardBridge.messenger()), 100); - vm.prank(address(l1StandardBridge.messenger())); + address messenger = _setupFinalizeBridgeETH(); + vm.prank(messenger); l1StandardBridge.finalizeETHWithdrawal{ value: 100 }(alice, alice, 100, hex""); assertEq(address(l1StandardBridge.messenger()).balance, 0); @@ -691,21 +665,19 @@ contract L1StandardBridge_FinalizeETHWithdrawal_Test is CommonTest { /// @title L1StandardBridge_FinalizeERC20Withdrawal_Test /// @notice Tests the `finalizeERC20Withdrawal` function of the `L1StandardBridge` contract. -contract L1StandardBridge_FinalizeERC20Withdrawal_Test is CommonTest { - using stdStorage for StdStorage; - +contract L1StandardBridge_FinalizeERC20Withdrawal_Test is L1StandardBridge_TestInit { /// @notice Tests that finalizing an ERC20 withdrawal succeeds. /// Bridge deposits are updated. /// Emits ERC20WithdrawalFinalized event. /// Only callable by the L2 bridge. function test_finalizeERC20Withdrawal_succeeds() external { - deal(address(L1Token), address(l1StandardBridge), 100, true); + deal(address(L1Token), address(l1StandardBridge), 100); - uint256 slot = stdstore.target(address(l1StandardBridge)).sig("deposits(address,address)") - .with_key(address(L1Token)).with_key(address(L2Token)).find(); + StorageSlot memory depositsSlot = ForgeArtifacts.getSlot("L1StandardBridge", "deposits"); + bytes32 localTokenDepositsSlot = keccak256(abi.encode(address(L1Token), uint256(depositsSlot.slot))); + bytes32 depositSlot = keccak256(abi.encode(address(L2Token), localTokenDepositsSlot)); - // Give the L1 bridge some ERC20 tokens - vm.store(address(l1StandardBridge), bytes32(slot), bytes32(uint256(100))); + vm.store(address(l1StandardBridge), depositSlot, bytes32(uint256(100))); assertEq(l1StandardBridge.deposits(address(L1Token), address(L2Token)), 100); vm.expectEmit(address(l1StandardBridge)); @@ -716,11 +688,7 @@ contract L1StandardBridge_FinalizeERC20Withdrawal_Test is CommonTest { vm.expectCall(address(L1Token), abi.encodeCall(ERC20.transfer, (alice, 100))); - vm.mockCall( - address(l1StandardBridge.messenger()), - abi.encodeCall(ICrossDomainMessenger.xDomainMessageSender, ()), - abi.encode(address(l1StandardBridge.OTHER_BRIDGE())) - ); + _mockOtherBridge(); vm.prank(address(l1StandardBridge.messenger())); l1StandardBridge.finalizeERC20Withdrawal(address(L1Token), address(L2Token), alice, alice, 100, hex""); @@ -733,11 +701,6 @@ contract L1StandardBridge_FinalizeERC20Withdrawal_Test is CommonTest { function testFuzz_finalizeERC20Withdrawal_notMessenger_reverts(address _caller) external { vm.assume(_caller != address(l1StandardBridge.messenger())); - vm.mockCall( - address(l1StandardBridge.messenger()), - abi.encodeCall(ICrossDomainMessenger.xDomainMessageSender, ()), - abi.encode(address(l1StandardBridge.OTHER_BRIDGE())) - ); vm.expectRevert("StandardBridge: function can only be called from the other bridge"); vm.prank(_caller); l1StandardBridge.finalizeERC20Withdrawal(address(L1Token), address(L2Token), alice, alice, 100, hex""); @@ -746,11 +709,7 @@ contract L1StandardBridge_FinalizeERC20Withdrawal_Test is CommonTest { /// @notice Tests that finalizing an ERC20 withdrawal reverts if the caller is not the L2 /// bridge. function test_finalizeERC20Withdrawal_notOtherBridge_reverts() external { - vm.mockCall( - address(l1StandardBridge.messenger()), - abi.encodeCall(ICrossDomainMessenger.xDomainMessageSender, ()), - abi.encode(address(0)) - ); + _mockXDomainMessageSender(address(0)); vm.prank(address(l1StandardBridge.messenger())); vm.expectRevert("StandardBridge: function can only be called from the other bridge"); l1StandardBridge.finalizeERC20Withdrawal(address(L1Token), address(L2Token), alice, alice, 100, hex""); @@ -763,12 +722,12 @@ contract L1StandardBridge_FinalizeERC20Withdrawal_Test is CommonTest { contract L1StandardBridge_Uncategorized_Test is L1StandardBridge_TestInit { /// @notice Test that the accessors return the correct initialized values. function test_getters_succeeds() external view { - assert(l1StandardBridge.l2TokenBridge() == address(l2StandardBridge)); - assert(address(l1StandardBridge.OTHER_BRIDGE()) == address(l2StandardBridge)); - assert(address(l1StandardBridge.messenger()) == address(l1CrossDomainMessenger)); - assert(address(l1StandardBridge.MESSENGER()) == address(l1CrossDomainMessenger)); - assert(l1StandardBridge.systemConfig() == systemConfig); - assert(l1StandardBridge.superchainConfig() == systemConfig.superchainConfig()); + assertEq(l1StandardBridge.l2TokenBridge(), address(l2StandardBridge)); + assertEq(address(l1StandardBridge.OTHER_BRIDGE()), address(l2StandardBridge)); + assertEq(address(l1StandardBridge.messenger()), address(l1CrossDomainMessenger)); + assertEq(address(l1StandardBridge.MESSENGER()), address(l1CrossDomainMessenger)); + assertEq(address(l1StandardBridge.systemConfig()), address(systemConfig)); + assertEq(address(l1StandardBridge.superchainConfig()), address(systemConfig.superchainConfig())); } /// @notice Tests that bridging ETH succeeds. @@ -783,12 +742,7 @@ contract L1StandardBridge_Uncategorized_Test is L1StandardBridge_TestInit { uint256 ethLockboxBalanceBefore = address(ethLockbox).balance; l1StandardBridge.bridgeETH{ value: 500 }(50000, hex"dead"); - if (isSysFeatureEnabled(Features.ETH_LOCKBOX)) { - assertEq(address(optimismPortal2).balance, portalBalanceBefore); - assertEq(address(ethLockbox).balance, ethLockboxBalanceBefore + 500); - } else { - assertEq(address(optimismPortal2).balance, portalBalanceBefore + 500); - } + _assertETHBridgeCustody(portalBalanceBefore, ethLockboxBalanceBefore, 500); } /// @notice Tests that bridging ETH to a different address succeeds. @@ -803,23 +757,12 @@ contract L1StandardBridge_Uncategorized_Test is L1StandardBridge_TestInit { uint256 ethLockboxBalanceBefore = address(ethLockbox).balance; l1StandardBridge.bridgeETHTo{ value: 600 }(bob, 60000, hex"dead"); - if (isSysFeatureEnabled(Features.ETH_LOCKBOX)) { - assertEq(address(optimismPortal2).balance, portalBalanceBefore); - assertEq(address(ethLockbox).balance, ethLockboxBalanceBefore + 600); - } else { - assertEq(address(optimismPortal2).balance, portalBalanceBefore + 600); - } + _assertETHBridgeCustody(portalBalanceBefore, ethLockboxBalanceBefore, 600); } /// @notice Tests that finalizing bridged ETH succeeds. function test_finalizeBridgeETH_succeeds() external { - address messenger = address(l1StandardBridge.messenger()); - vm.mockCall( - messenger, - abi.encodeCall(ICrossDomainMessenger.xDomainMessageSender, ()), - abi.encode(address(l1StandardBridge.OTHER_BRIDGE())) - ); - vm.deal(messenger, 100); + address messenger = _setupFinalizeBridgeETH(); vm.prank(messenger); vm.expectEmit(address(l1StandardBridge)); @@ -830,13 +773,7 @@ contract L1StandardBridge_Uncategorized_Test is L1StandardBridge_TestInit { /// @notice Tests that finalizing bridged ETH reverts if the amount is incorrect. function test_finalizeBridgeETH_incorrectValue_reverts() external { - address messenger = address(l1StandardBridge.messenger()); - vm.mockCall( - messenger, - abi.encodeCall(ICrossDomainMessenger.xDomainMessageSender, ()), - abi.encode(address(l1StandardBridge.OTHER_BRIDGE())) - ); - vm.deal(messenger, 100); + address messenger = _setupFinalizeBridgeETH(); vm.prank(messenger); vm.expectRevert("StandardBridge: amount sent does not match amount required"); l1StandardBridge.finalizeBridgeETH{ value: 50 }(alice, alice, 100, hex""); @@ -844,13 +781,7 @@ contract L1StandardBridge_Uncategorized_Test is L1StandardBridge_TestInit { /// @notice Tests that finalizing bridged ETH reverts if the destination is the L1 bridge. function test_finalizeBridgeETH_sendToSelf_reverts() external { - address messenger = address(l1StandardBridge.messenger()); - vm.mockCall( - messenger, - abi.encodeCall(ICrossDomainMessenger.xDomainMessageSender, ()), - abi.encode(address(l1StandardBridge.OTHER_BRIDGE())) - ); - vm.deal(messenger, 100); + address messenger = _setupFinalizeBridgeETH(); vm.prank(messenger); vm.expectRevert("StandardBridge: cannot send to self"); l1StandardBridge.finalizeBridgeETH{ value: 100 }(alice, address(l1StandardBridge), 100, hex""); @@ -858,13 +789,7 @@ contract L1StandardBridge_Uncategorized_Test is L1StandardBridge_TestInit { /// @notice Tests that finalizing bridged ETH reverts if the destination is the messenger. function test_finalizeBridgeETH_sendToMessenger_reverts() external { - address messenger = address(l1StandardBridge.messenger()); - vm.mockCall( - messenger, - abi.encodeCall(ICrossDomainMessenger.xDomainMessageSender, ()), - abi.encode(address(l1StandardBridge.OTHER_BRIDGE())) - ); - vm.deal(messenger, 100); + address messenger = _setupFinalizeBridgeETH(); vm.prank(messenger); vm.expectRevert("StandardBridge: cannot send to messenger"); l1StandardBridge.finalizeBridgeETH{ value: 100 }(alice, messenger, 100, hex""); diff --git a/test/L1/OptimismPortal2.t.sol b/test/L1/OptimismPortal2.t.sol index 5b8120b5..bf93e25f 100644 --- a/test/L1/OptimismPortal2.t.sol +++ b/test/L1/OptimismPortal2.t.sol @@ -19,7 +19,6 @@ import { Types } from "src/libraries/Types.sol"; import { Hashing } from "src/libraries/Hashing.sol"; import { Constants } from "src/libraries/Constants.sol"; import { AddressAliasHelper } from "src/vendor/AddressAliasHelper.sol"; -import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; import { Features } from "src/libraries/Features.sol"; import { AggregateVerifier } from "src/L1/proofs/AggregateVerifier.sol"; import "src/libraries/bridge/Types.sol"; @@ -31,8 +30,6 @@ import { IOptimismPortal2 as IOptimismPortal } from "interfaces/L1/IOptimismPort import { IAggregateVerifier } from "interfaces/L1/proofs/IAggregateVerifier.sol"; import { IDisputeGame } from "interfaces/L1/proofs/IDisputeGame.sol"; import { IProxy } from "interfaces/universal/IProxy.sol"; -import { IAnchorStateRegistry } from "interfaces/L1/proofs/IAnchorStateRegistry.sol"; -import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; import { IProxyAdminOwnedBase } from "interfaces/L1/IProxyAdminOwnedBase.sol"; import { IVerifier } from "interfaces/L1/proofs/IVerifier.sol"; @@ -122,14 +119,10 @@ abstract contract OptimismPortal2_TestInit is DisputeGameFactory_TestInit { } function _createDisputeGame(Claim _rootClaim, uint256 _salt) internal returns (IAggregateVerifier game_) { - bytes memory intermediateRoots; AggregateVerifier gameImpl = AggregateVerifier(address(disputeGameFactory.gameImpls(respectedGameType))); - for (uint256 i = 1; i < gameImpl.intermediateOutputRootsCount(); i++) { - intermediateRoots = - abi.encodePacked(intermediateRoots, keccak256(abi.encode(_proposedBlockNumber, i, _salt))); - } - bytes memory extraData = - abi.encodePacked(_proposedBlockNumber, address(anchorStateRegistry), intermediateRoots, _rootClaim.raw()); + bytes memory intermediateRoots = + _generateIntermediateRoots(_rootClaim, _salt, gameImpl.intermediateOutputRootsCount()); + bytes memory extraData = abi.encodePacked(_proposedBlockNumber, address(anchorStateRegistry), intermediateRoots); bytes memory proof = abi.encodePacked( uint8(AggregateVerifier.ProofType.TEE), blockhash(block.number - 1), block.number - 1, bytes32(0) ); @@ -143,6 +136,97 @@ abstract contract OptimismPortal2_TestInit is DisputeGameFactory_TestInit { ); } + function _generateIntermediateRoots( + Claim _rootClaim, + uint256 _salt, + uint256 _intermediateRootsCount + ) + private + view + returns (bytes memory) + { + bytes32[] memory intermediateRoots = new bytes32[](_intermediateRootsCount); + for (uint256 i = 1; i < _intermediateRootsCount; i++) { + intermediateRoots[i - 1] = keccak256(abi.encode(_proposedBlockNumber, i, _salt)); + } + intermediateRoots[_intermediateRootsCount - 1] = _rootClaim.raw(); + + return abi.encodePacked(intermediateRoots); + } + + function _expectWithdrawalProven(bytes32 _withdrawalHash_, address _proofSubmitter) internal { + vm.expectEmit(true, true, true, true, address(optimismPortal2)); + emit WithdrawalProven(_withdrawalHash_, alice, bob); + vm.expectEmit(true, true, true, true, address(optimismPortal2)); + emit WithdrawalProvenExtension1(_withdrawalHash_, _proofSubmitter); + } + + function _proveDefaultWithdrawal() internal { + _expectWithdrawalProven(_withdrawalHash, address(this)); + optimismPortal2.proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameIndex: _proposedGameIndex, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + } + + function _resolveGameAndWarpPastProofMaturity(IDisputeGame _game) internal { + _game.resolve(); + vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1 seconds); + } + + function _proveFuzzedWithdrawal( + address _sender, + address _target, + uint256 _value, + uint256 _gasLimit, + bytes memory _data + ) + internal + returns (Types.WithdrawalTransaction memory withdrawalTx_, bytes32 withdrawalHash_) + { + uint256 value = bound(_value, 0, 200_000_000 ether); + vm.deal(address(optimismPortal2), value); + if (isUsingLockbox()) { + vm.deal(address(ethLockbox), value); + } + + uint256 gasLimit = bound(_gasLimit, 0, 50_000_000); + withdrawalTx_ = Types.WithdrawalTransaction({ + nonce: l2ToL1MessagePasser.messageNonce(), + sender: _sender, + target: _target, + value: value, + gasLimit: gasLimit, + data: _data + }); + + ( + bytes32 stateRoot, + bytes32 storageRoot, + bytes32 outputRoot, + bytes32 withdrawalHash, + bytes[] memory withdrawalProof + ) = ffi.getProveWithdrawalTransactionInputs(withdrawalTx_); + withdrawalHash_ = withdrawalHash; + + Types.OutputRootProof memory proof = Types.OutputRootProof({ + version: bytes32(uint256(0)), + stateRoot: stateRoot, + messagePasserStorageRoot: storageRoot, + latestBlockhash: bytes32(uint256(0)) + }); + + assertEq(outputRoot, Hashing.hashOutputRootProof(proof)); + assertEq(withdrawalHash_, Hashing.hashWithdrawal(withdrawalTx_)); + + vm.mockCall(address(game), abi.encodeCall(game.rootClaim, ()), abi.encode(outputRoot)); + optimismPortal2.proveWithdrawalTransaction(withdrawalTx_, _proposedGameIndex, proof, withdrawalProof); + (IDisputeGame provenGame,) = optimismPortal2.provenWithdrawals(withdrawalHash_, address(this)); + assertTrue(provenGame.rootClaim().raw() != bytes32(0)); + } + /// @notice Asserts that the reentrant call will revert. function callPortalAndExpectRevert() external payable { vm.expectRevert(IOptimismPortal.OptimismPortal_NoReentrancy.selector); @@ -157,14 +241,14 @@ abstract contract OptimismPortal2_TestInit is DisputeGameFactory_TestInit { /// @notice Checks if the ETHLockbox feature is enabled. /// @return bool True if the ETHLockbox feature is enabled. - function isUsingLockbox() public view returns (bool) { + function isUsingLockbox() internal view returns (bool) { return systemConfig.isFeatureEnabled(Features.ETH_LOCKBOX) && address(optimismPortal2.ethLockbox()) != address(0); } /// @notice Enables the ETHLockbox feature if not enabled. /// @param _lockbox Address of the lockbox to enable. - function forceEnableLockbox(address _lockbox) public { + function forceEnableLockbox(address _lockbox) internal { if (!isSysFeatureEnabled(Features.ETH_LOCKBOX)) { vm.prank(address(proxyAdmin)); systemConfig.setFeature(Features.ETH_LOCKBOX, true); @@ -200,7 +284,6 @@ contract OptimismPortal2_Constructor_Test is OptimismPortal2_TestInit { assertEq(address(opImpl.anchorStateRegistry()), address(0)); assertEq(address(opImpl.systemConfig()), address(0)); assertEq(opImpl.l2Sender(), address(0)); - assertEq(address(opImpl.anchorStateRegistry()), address(0)); assertEq(address(opImpl.ethLockbox()), address(0)); } } @@ -430,13 +513,7 @@ contract OptimismPortal2_NumProofSubmitters_Test is OptimismPortal2_TestInit { function test_numProofSubmitters_provenWithdrawal_succeeds() external { bytes32 withdrawalHash = Hashing.hashWithdrawal(_defaultTx); - // Prove the withdrawal - optimismPortal2.proveWithdrawalTransaction({ - _tx: _defaultTx, - _disputeGameIndex: _proposedGameIndex, - _outputRootProof: _outputRootProof, - _withdrawalProof: _withdrawalProof - }); + _proveDefaultWithdrawal(); assertEq(optimismPortal2.numProofSubmitters(withdrawalHash), 1); } @@ -446,13 +523,7 @@ contract OptimismPortal2_NumProofSubmitters_Test is OptimismPortal2_TestInit { vm.assume(_prover != address(0) && _prover != address(this)); bytes32 withdrawalHash = Hashing.hashWithdrawal(_defaultTx); - // First proof by this contract - optimismPortal2.proveWithdrawalTransaction({ - _tx: _defaultTx, - _disputeGameIndex: _proposedGameIndex, - _outputRootProof: _outputRootProof, - _withdrawalProof: _withdrawalProof - }); + _proveDefaultWithdrawal(); // Second proof by different prover vm.prank(_prover); @@ -565,10 +636,8 @@ contract OptimismPortal2_DonateETH_Test is OptimismPortal2_TestInit { optimismPortal2.donateETH{ value: _amount }(); VmSafe.AccountAccess[] memory accountAccesses = vm.stopAndReturnStateDiff(); - // not necessary since it's checked below assertEq(address(optimismPortal2).balance, preBalance + _amount); - // check that the ETHLockbox balance is unchanged assertEq(address(ethLockbox).balance, lockboxBalanceBefore); // 0 for extcodesize of proxy before being called by this test, @@ -578,28 +647,13 @@ contract OptimismPortal2_DonateETH_Test is OptimismPortal2_TestInit { assertEq(uint8(accountAccesses[1].kind), uint8(VmSafe.AccountAccessKind.Call)); assertEq(uint8(accountAccesses[2].kind), uint8(VmSafe.AccountAccessKind.DelegateCall)); - // to of 1 is the optimism portal proxy assertEq(accountAccesses[1].account, address(optimismPortal2)); - - // accessor is the pranked address assertEq(accountAccesses[1].accessor, alice); - - // value is the amount of ETH donated assertEq(accountAccesses[1].value, _amount); - - // old balance is the balance of the optimism portal before the donation assertEq(accountAccesses[1].oldBalance, preBalance); - - // new balance is the balance of the optimism portal after the donation assertEq(accountAccesses[1].newBalance, preBalance + _amount); - - // data is the selector of the donateETH function assertEq(accountAccesses[1].data, abi.encodePacked(optimismPortal2.donateETH.selector)); - - // reverted of alice call to proxy is false assertEq(accountAccesses[1].reverted, false); - - // reverted of delegate call of proxy to impl is false assertEq(accountAccesses[2].reverted, false); // storage accesses of delegate call of proxy to impl is empty (No storage read or write!) @@ -698,22 +752,13 @@ contract OptimismPortal2_ProveWithdrawalTransaction_Test is OptimismPortal2_Test /// @notice Tests that `proveWithdrawalTransaction` reverts when the withdrawal has already /// been proven, and the new game has the `CHALLENGER_WINS` status. function test_proveWithdrawalTransaction_replayProveDifferentGameChallengerWins_reverts() external { - vm.expectEmit(address(optimismPortal2)); - emit WithdrawalProven(_withdrawalHash, alice, bob); - vm.expectEmit(address(optimismPortal2)); - emit WithdrawalProvenExtension1(_withdrawalHash, address(this)); - optimismPortal2.proveWithdrawalTransaction({ - _tx: _defaultTx, - _disputeGameIndex: _proposedGameIndex, - _outputRootProof: _outputRootProof, - _withdrawalProof: _withdrawalProof - }); + _proveDefaultWithdrawal(); // Create a new dispute game, and mock both games to be CHALLENGER_WINS. IDisputeGame game2 = _createDisputeGame(Claim.wrap(_outputRoot), 1); _proposedGameIndex = disputeGameFactory.gameCount() - 1; vm.mockCall(address(game), abi.encodeCall(game.status, ()), abi.encode(GameStatus.CHALLENGER_WINS)); - vm.mockCall(address(game2), abi.encodeCall(game.status, ()), abi.encode(GameStatus.CHALLENGER_WINS)); + vm.mockCall(address(game2), abi.encodeCall(game2.status, ()), abi.encode(GameStatus.CHALLENGER_WINS)); vm.expectRevert(IOptimismPortal.OptimismPortal_InvalidDisputeGame.selector); optimismPortal2.proveWithdrawalTransaction({ @@ -741,7 +786,7 @@ contract OptimismPortal2_ProveWithdrawalTransaction_Test is OptimismPortal2_Test /// does not implement `wasRespectedGameTypeWhenCreated`. function test_proveWithdrawalTransaction_legacyGame_reverts() external { vm.mockCallRevert(address(game), abi.encodeCall(game.wasRespectedGameTypeWhenCreated, ()), ""); - vm.expectRevert(); // nosemgrep: sol-safety-expectrevert-no-args + vm.expectRevert(bytes("")); optimismPortal2.proveWithdrawalTransaction({ _tx: _defaultTx, _disputeGameIndex: _proposedGameIndex, @@ -783,16 +828,7 @@ contract OptimismPortal2_ProveWithdrawalTransaction_Test is OptimismPortal2_Test /// @notice Tests that `proveWithdrawalTransaction` can be re-executed if the dispute game /// proven against has resolved against the favor of the root claim. function test_proveWithdrawalTransaction_replayProveBadProposal_succeeds() external { - vm.expectEmit(true, true, true, true); - emit WithdrawalProven(_withdrawalHash, alice, bob); - vm.expectEmit(true, true, true, true); - emit WithdrawalProvenExtension1(_withdrawalHash, address(this)); - optimismPortal2.proveWithdrawalTransaction({ - _tx: _defaultTx, - _disputeGameIndex: _proposedGameIndex, - _outputRootProof: _outputRootProof, - _withdrawalProof: _withdrawalProof - }); + _proveDefaultWithdrawal(); // Mock the status of the dispute game we just proved against to be CHALLENGER_WINS. vm.mockCall(address(game), abi.encodeCall(game.status, ()), abi.encode(GameStatus.CHALLENGER_WINS)); @@ -804,32 +840,14 @@ contract OptimismPortal2_ProveWithdrawalTransaction_Test is OptimismPortal2_Test // Warp 1 second into the future so we're not in the same block as the dispute game. vm.warp(block.timestamp + 1 seconds); - vm.expectEmit(true, true, true, true); - emit WithdrawalProven(_withdrawalHash, alice, bob); - vm.expectEmit(true, true, true, true); - emit WithdrawalProvenExtension1(_withdrawalHash, address(this)); - optimismPortal2.proveWithdrawalTransaction({ - _tx: _defaultTx, - _disputeGameIndex: _proposedGameIndex, - _outputRootProof: _outputRootProof, - _withdrawalProof: _withdrawalProof - }); + _proveDefaultWithdrawal(); } /// @notice Tests that `proveWithdrawalTransaction` can be re-executed if the dispute game /// proven against is no longer of the respected game type. function test_proveWithdrawalTransaction_replayRespectedGameTypeChanged_succeeds() external { // Prove the withdrawal against a game with the current respected game type. - vm.expectEmit(true, true, true, true); - emit WithdrawalProven(_withdrawalHash, alice, bob); - vm.expectEmit(true, true, true, true); - emit WithdrawalProvenExtension1(_withdrawalHash, address(this)); - optimismPortal2.proveWithdrawalTransaction({ - _tx: _defaultTx, - _disputeGameIndex: _proposedGameIndex, - _outputRootProof: _outputRootProof, - _withdrawalProof: _withdrawalProof - }); + _proveDefaultWithdrawal(); // Create a new game. IDisputeGame newGame = _createDisputeGame(Claim.wrap(_outputRoot), 1); @@ -849,10 +867,7 @@ contract OptimismPortal2_ProveWithdrawalTransaction_Test is OptimismPortal2_Test vm.warp(block.timestamp + 1 seconds); // Re-proving should be successful against the new game. - vm.expectEmit(true, true, true, true); - emit WithdrawalProven(_withdrawalHash, alice, bob); - vm.expectEmit(true, true, true, true); - emit WithdrawalProvenExtension1(_withdrawalHash, address(this)); + _expectWithdrawalProven(_withdrawalHash, address(this)); optimismPortal2.proveWithdrawalTransaction({ _tx: _defaultTx, _disputeGameIndex: _proposedGameIndex + 1, @@ -863,16 +878,7 @@ contract OptimismPortal2_ProveWithdrawalTransaction_Test is OptimismPortal2_Test /// @notice Tests that `proveWithdrawalTransaction` succeeds. function test_proveWithdrawalTransaction_validWithdrawalProof_succeeds() external { - vm.expectEmit(true, true, true, true); - emit WithdrawalProven(_withdrawalHash, alice, bob); - vm.expectEmit(true, true, true, true); - emit WithdrawalProvenExtension1(_withdrawalHash, address(this)); - optimismPortal2.proveWithdrawalTransaction({ - _tx: _defaultTx, - _disputeGameIndex: _proposedGameIndex, - _outputRootProof: _outputRootProof, - _withdrawalProof: _withdrawalProof - }); + _proveDefaultWithdrawal(); } } @@ -903,9 +909,7 @@ contract OptimismPortal2_FinalizeWithdrawalTransaction_Test is OptimismPortal2_T emit WithdrawalProven(_withdrawalHash, alice, bob); optimismPortal2.proveWithdrawalTransaction(_defaultTx, _proposedGameIndex, _outputRootProof, _withdrawalProof); - // Warp and resolve the dispute game. - game.resolve(); - vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1 seconds); + _resolveGameAndWarpPastProofMaturity(game); vm.startPrank(alice, Constants.ESTIMATION_ADDRESS); vm.expectRevert(IOptimismPortal.OptimismPortal_GasEstimation.selector); @@ -918,30 +922,28 @@ contract OptimismPortal2_FinalizeWithdrawalTransaction_Test is OptimismPortal2_T // Get withdrawal proof data we can use for testing. ( - bytes32 _stateRoot_noData, - bytes32 _storageRoot_noData, - bytes32 _outputRoot_noData, - bytes32 _withdrawalHash_noData, - bytes[] memory _withdrawalProof_noData + bytes32 stateRootNoData, + bytes32 storageRootNoData, + bytes32 outputRootNoData, + bytes32 withdrawalHashNoData, + bytes[] memory withdrawalProofNoData ) = ffi.getProveWithdrawalTransactionInputs(_defaultTx); // Setup a dummy output root proof for reuse. - Types.OutputRootProof memory _outputRootProof_noData = Types.OutputRootProof({ + Types.OutputRootProof memory outputRootProofNoData = Types.OutputRootProof({ version: bytes32(uint256(0)), - stateRoot: _stateRoot_noData, - messagePasserStorageRoot: _storageRoot_noData, + stateRoot: stateRootNoData, + messagePasserStorageRoot: storageRootNoData, latestBlockhash: bytes32(uint256(0)) }); - IAggregateVerifier game_noData = _createDisputeGame(Claim.wrap(_outputRoot_noData), 0); + IAggregateVerifier gameNoData = _createDisputeGame(Claim.wrap(outputRootNoData), 0); - uint256 _proposedGameIndex_noData = disputeGameFactory.gameCount() - 1; + uint256 proposedGameIndexNoData = disputeGameFactory.gameCount() - 1; // Warp beyond the resolution delay. - vm.warp(game_noData.expectedResolution().raw() + 1 seconds); + vm.warp(gameNoData.expectedResolution().raw() + 1 seconds); - // Fund the portal so that we can withdraw ETH. - vm.store(address(optimismPortal2), bytes32(uint256(61)), bytes32(uint256(0xFFFFFFFF))); vm.deal(address(optimismPortal2), 0xFFFFFFFF); if (isUsingLockbox()) { vm.deal(address(ethLockbox), 0xFFFFFFFF); @@ -949,23 +951,18 @@ contract OptimismPortal2_FinalizeWithdrawalTransaction_Test is OptimismPortal2_T uint256 bobBalanceBefore = bob.balance; - vm.expectEmit(address(optimismPortal2)); - emit WithdrawalProven(_withdrawalHash_noData, alice, bob); - vm.expectEmit(address(optimismPortal2)); - emit WithdrawalProvenExtension1(_withdrawalHash_noData, address(this)); + _expectWithdrawalProven(withdrawalHashNoData, address(this)); optimismPortal2.proveWithdrawalTransaction({ _tx: _defaultTx, - _disputeGameIndex: _proposedGameIndex_noData, - _outputRootProof: _outputRootProof_noData, - _withdrawalProof: _withdrawalProof_noData + _disputeGameIndex: proposedGameIndexNoData, + _outputRootProof: outputRootProofNoData, + _withdrawalProof: withdrawalProofNoData }); - // Warp and resolve the dispute game. - game_noData.resolve(); - vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1 seconds); + _resolveGameAndWarpPastProofMaturity(gameNoData); vm.expectEmit(true, true, false, true); - emit WithdrawalFinalized(_withdrawalHash_noData, true); + emit WithdrawalFinalized(withdrawalHashNoData, true); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); assert(bob.balance == bobBalanceBefore + _defaultTx.value); @@ -975,20 +972,9 @@ contract OptimismPortal2_FinalizeWithdrawalTransaction_Test is OptimismPortal2_T function test_finalizeWithdrawalTransaction_provenWithdrawalHashEther_succeeds() external { uint256 bobBalanceBefore = address(bob).balance; - vm.expectEmit(address(optimismPortal2)); - emit WithdrawalProven(_withdrawalHash, alice, bob); - vm.expectEmit(address(optimismPortal2)); - emit WithdrawalProvenExtension1(_withdrawalHash, address(this)); - optimismPortal2.proveWithdrawalTransaction({ - _tx: _defaultTx, - _disputeGameIndex: _proposedGameIndex, - _outputRootProof: _outputRootProof, - _withdrawalProof: _withdrawalProof - }); + _proveDefaultWithdrawal(); - // Warp and resolve the dispute game. - game.resolve(); - vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1 seconds); + _resolveGameAndWarpPastProofMaturity(game); vm.expectEmit(true, true, false, true); emit WithdrawalFinalized(_withdrawalHash, true); @@ -1009,10 +995,7 @@ contract OptimismPortal2_FinalizeWithdrawalTransaction_Test is OptimismPortal2_T vm.warp(block.timestamp + 1); // Prove the withdrawal transaction against the invalid dispute game, as 0xb0b. - vm.expectEmit(true, true, true, true); - emit WithdrawalProven(_withdrawalHash, alice, bob); - vm.expectEmit(true, true, true, true); - emit WithdrawalProvenExtension1(_withdrawalHash, address(0xb0b)); + _expectWithdrawalProven(_withdrawalHash, address(0xb0b)); vm.prank(address(0xb0b)); optimismPortal2.proveWithdrawalTransaction({ _tx: _defaultTx, @@ -1022,24 +1005,13 @@ contract OptimismPortal2_FinalizeWithdrawalTransaction_Test is OptimismPortal2_T }); // Mock the status of the dispute game 0xb0b proves against to be CHALLENGER_WINS. - vm.mockCall(address(secondGame), abi.encodeCall(game.status, ()), abi.encode(GameStatus.CHALLENGER_WINS)); + vm.mockCall(address(secondGame), abi.encodeCall(secondGame.status, ()), abi.encode(GameStatus.CHALLENGER_WINS)); // Prove the withdrawal transaction against the invalid dispute game, as the test contract, against the original // game. - vm.expectEmit(true, true, true, true); - emit WithdrawalProven(_withdrawalHash, alice, bob); - vm.expectEmit(true, true, true, true); - emit WithdrawalProvenExtension1(_withdrawalHash, address(this)); - optimismPortal2.proveWithdrawalTransaction({ - _tx: _defaultTx, - _disputeGameIndex: _proposedGameIndex, - _outputRootProof: _outputRootProof, - _withdrawalProof: _withdrawalProof - }); + _proveDefaultWithdrawal(); - // Warp and resolve the original dispute game. - game.resolve(); - vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1 seconds); + _resolveGameAndWarpPastProofMaturity(game); // Ensure both proofs are registered successfully. assertEq(optimismPortal2.numProofSubmitters(_withdrawalHash), 2); @@ -1080,16 +1052,7 @@ contract OptimismPortal2_FinalizeWithdrawalTransaction_Test is OptimismPortal2_T function test_finalizeWithdrawalTransaction_ifWithdrawalProofNotOldEnough_reverts() external { uint256 bobBalanceBefore = address(bob).balance; - vm.expectEmit(address(optimismPortal2)); - emit WithdrawalProven(_withdrawalHash, alice, bob); - vm.expectEmit(address(optimismPortal2)); - emit WithdrawalProvenExtension1(_withdrawalHash, address(this)); - optimismPortal2.proveWithdrawalTransaction({ - _tx: _defaultTx, - _disputeGameIndex: _proposedGameIndex, - _outputRootProof: _outputRootProof, - _withdrawalProof: _withdrawalProof - }); + _proveDefaultWithdrawal(); vm.expectRevert(IOptimismPortal.OptimismPortal_ProofNotOldEnough.selector); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); @@ -1103,16 +1066,7 @@ contract OptimismPortal2_FinalizeWithdrawalTransaction_Test is OptimismPortal2_T uint256 bobBalanceBefore = address(bob).balance; // Prove our withdrawal - vm.expectEmit(true, true, true, true); - emit WithdrawalProven(_withdrawalHash, alice, bob); - vm.expectEmit(true, true, true, true); - emit WithdrawalProvenExtension1(_withdrawalHash, address(this)); - optimismPortal2.proveWithdrawalTransaction({ - _tx: _defaultTx, - _disputeGameIndex: _proposedGameIndex, - _outputRootProof: _outputRootProof, - _withdrawalProof: _withdrawalProof - }); + _proveDefaultWithdrawal(); // Warp to after the finalization period vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1); @@ -1134,16 +1088,7 @@ contract OptimismPortal2_FinalizeWithdrawalTransaction_Test is OptimismPortal2_T uint256 bobBalanceBefore = address(bob).balance; // Prove our withdrawal - vm.expectEmit(true, true, true, true); - emit WithdrawalProven(_withdrawalHash, alice, bob); - vm.expectEmit(true, true, true, true); - emit WithdrawalProvenExtension1(_withdrawalHash, address(this)); - optimismPortal2.proveWithdrawalTransaction({ - _tx: _defaultTx, - _disputeGameIndex: _proposedGameIndex, - _outputRootProof: _outputRootProof, - _withdrawalProof: _withdrawalProof - }); + _proveDefaultWithdrawal(); // Warp to after the finalization period vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1); @@ -1165,21 +1110,9 @@ contract OptimismPortal2_FinalizeWithdrawalTransaction_Test is OptimismPortal2_T uint256 bobBalanceBefore = address(bob).balance; vm.etch(bob, hex"fe"); // Contract with just the invalid opcode. - vm.expectEmit(true, true, true, true); - emit WithdrawalProven(_withdrawalHash, alice, bob); - vm.expectEmit(true, true, true, true); - emit WithdrawalProvenExtension1(_withdrawalHash, address(this)); - optimismPortal2.proveWithdrawalTransaction({ - _tx: _defaultTx, - _disputeGameIndex: _proposedGameIndex, - _outputRootProof: _outputRootProof, - _withdrawalProof: _withdrawalProof - }); - - // Resolve the dispute game. - game.resolve(); + _proveDefaultWithdrawal(); - vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1); + _resolveGameAndWarpPastProofMaturity(game); vm.expectEmit(true, true, true, true); emit WithdrawalFinalized(_withdrawalHash, false); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); @@ -1205,21 +1138,9 @@ contract OptimismPortal2_FinalizeWithdrawalTransaction_Test is OptimismPortal2_T uint256 bobBalanceBefore = address(bob).balance; vm.etch(bob, hex"fe"); // Contract with just the invalid opcode. - vm.expectEmit(true, true, true, true); - emit WithdrawalProven(_withdrawalHash, alice, bob); - vm.expectEmit(true, true, true, true); - emit WithdrawalProvenExtension1(_withdrawalHash, address(this)); - optimismPortal2.proveWithdrawalTransaction({ - _tx: _defaultTx, - _disputeGameIndex: _proposedGameIndex, - _outputRootProof: _outputRootProof, - _withdrawalProof: _withdrawalProof - }); + _proveDefaultWithdrawal(); - // Resolve the dispute game. - game.resolve(); - - vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1); + _resolveGameAndWarpPastProofMaturity(game); vm.expectEmit(true, true, true, true); emit WithdrawalFinalized(_withdrawalHash, false); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); @@ -1234,21 +1155,9 @@ contract OptimismPortal2_FinalizeWithdrawalTransaction_Test is OptimismPortal2_T /// @notice Tests that `finalizeWithdrawalTransaction` reverts if the withdrawal has already /// been finalized. function test_finalizeWithdrawalTransaction_onReplay_reverts() external { - vm.expectEmit(true, true, true, true); - emit WithdrawalProven(_withdrawalHash, alice, bob); - vm.expectEmit(true, true, true, true); - emit WithdrawalProvenExtension1(_withdrawalHash, address(this)); - optimismPortal2.proveWithdrawalTransaction({ - _tx: _defaultTx, - _disputeGameIndex: _proposedGameIndex, - _outputRootProof: _outputRootProof, - _withdrawalProof: _withdrawalProof - }); - - // Resolve the dispute game. - game.resolve(); + _proveDefaultWithdrawal(); - vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1); + _resolveGameAndWarpPastProofMaturity(game); vm.expectEmit(true, true, true, true); emit WithdrawalFinalized(_withdrawalHash, true); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); @@ -1285,10 +1194,7 @@ contract OptimismPortal2_FinalizeWithdrawalTransaction_Test is OptimismPortal2_T _withdrawalProof: withdrawalProof }); - // Resolve the dispute game. - game.resolve(); - - vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1); + _resolveGameAndWarpPastProofMaturity(game); vm.expectRevert("SafeCall: Not enough gas"); optimismPortal2.finalizeWithdrawalTransaction{ gas: _defaultTx.gasLimit }(_defaultTx); } @@ -1328,10 +1234,7 @@ contract OptimismPortal2_FinalizeWithdrawalTransaction_Test is OptimismPortal2_T emit WithdrawalProvenExtension1(withdrawalHash, address(this)); optimismPortal2.proveWithdrawalTransaction(_testTx, _proposedGameIndex, outputRootProof, withdrawalProof); - // Resolve the dispute game. - game.resolve(); - - vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1); + _resolveGameAndWarpPastProofMaturity(game); vm.expectCall(address(this), _testTx.data); vm.expectEmit(true, true, true, true); emit WithdrawalFinalized(withdrawalHash, true); @@ -1360,57 +1263,13 @@ contract OptimismPortal2_FinalizeWithdrawalTransaction_Test is OptimismPortal2_T && uint160(_target) > 9 // No precompiles (or zero address) ); - // Total ETH supply is currently about 120M ETH. - uint256 value = bound(_value, 0, 200_000_000 ether); - vm.deal(address(optimismPortal2), value); - if (isUsingLockbox()) { - vm.deal(address(ethLockbox), value); - } + (Types.WithdrawalTransaction memory withdrawalTx, bytes32 withdrawalHash) = + _proveFuzzedWithdrawal(_sender, _target, _value, _gasLimit, _data); - uint256 gasLimit = bound(_gasLimit, 0, 50_000_000); - uint256 nonce = l2ToL1MessagePasser.messageNonce(); + _resolveGameAndWarpPastProofMaturity(game); - // Get a withdrawal transaction and mock proof from the differential testing script. - Types.WithdrawalTransaction memory _tx = Types.WithdrawalTransaction({ - nonce: nonce, sender: _sender, target: _target, value: value, gasLimit: gasLimit, data: _data - }); - ( - bytes32 stateRoot, - bytes32 storageRoot, - bytes32 outputRoot, - bytes32 withdrawalHash, - bytes[] memory withdrawalProof - ) = ffi.getProveWithdrawalTransactionInputs(_tx); - - // Create the output root proof - Types.OutputRootProof memory proof = Types.OutputRootProof({ - version: bytes32(uint256(0)), - stateRoot: stateRoot, - messagePasserStorageRoot: storageRoot, - latestBlockhash: bytes32(uint256(0)) - }); - - // Ensure the values returned from ffi are correct - assertEq(outputRoot, Hashing.hashOutputRootProof(proof)); - assertEq(withdrawalHash, Hashing.hashWithdrawal(_tx)); - - // Setup the dispute game to return the output root - vm.mockCall(address(game), abi.encodeCall(game.rootClaim, ()), abi.encode(outputRoot)); - - // Prove the withdrawal transaction - optimismPortal2.proveWithdrawalTransaction(_tx, _proposedGameIndex, proof, withdrawalProof); - (IDisputeGame _game,) = optimismPortal2.provenWithdrawals(withdrawalHash, address(this)); - assertTrue(_game.rootClaim().raw() != bytes32(0)); - - // Resolve the dispute game - game.resolve(); - - // Warp past the finalization period - vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1); - - // Finalize the withdrawal transaction - vm.expectCallMinGas(_tx.target, _tx.value, uint64(_tx.gasLimit), _tx.data); - optimismPortal2.finalizeWithdrawalTransaction(_tx); + vm.expectCallMinGas(withdrawalTx.target, withdrawalTx.value, uint64(withdrawalTx.gasLimit), withdrawalTx.data); + optimismPortal2.finalizeWithdrawalTransaction(withdrawalTx); assertTrue(optimismPortal2.finalizedWithdrawals(withdrawalHash)); } @@ -1438,77 +1297,25 @@ contract OptimismPortal2_FinalizeWithdrawalTransaction_Test is OptimismPortal2_T // Bound to prevent changes in retirementTimestamp _newGameType = GameType.wrap(uint32(bound(_newGameType.raw(), 0, type(uint32).max - 1))); - // Total ETH supply is currently about 120M ETH. - uint256 value = bound(_value, 0, 200_000_000 ether); - vm.deal(address(optimismPortal2), value); - if (isUsingLockbox()) { - vm.deal(address(ethLockbox), value); - } - - uint256 gasLimit = bound(_gasLimit, 0, 50_000_000); - uint256 nonce = l2ToL1MessagePasser.messageNonce(); + (Types.WithdrawalTransaction memory withdrawalTx, bytes32 withdrawalHash) = + _proveFuzzedWithdrawal(_sender, _target, _value, _gasLimit, _data); - // Get a withdrawal transaction and mock proof from the differential testing script. - Types.WithdrawalTransaction memory _tx = Types.WithdrawalTransaction({ - nonce: nonce, sender: _sender, target: _target, value: value, gasLimit: gasLimit, data: _data - }); - ( - bytes32 stateRoot, - bytes32 storageRoot, - bytes32 outputRoot, - bytes32 withdrawalHash, - bytes[] memory withdrawalProof - ) = ffi.getProveWithdrawalTransactionInputs(_tx); - - // Create the output root proof - Types.OutputRootProof memory proof = Types.OutputRootProof({ - version: bytes32(uint256(0)), - stateRoot: stateRoot, - messagePasserStorageRoot: storageRoot, - latestBlockhash: bytes32(uint256(0)) - }); - - // Ensure the values returned from ffi are correct - assertEq(outputRoot, Hashing.hashOutputRootProof(proof)); - assertEq(withdrawalHash, Hashing.hashWithdrawal(_tx)); - - // Setup the dispute game to return the output root - vm.mockCall(address(game), abi.encodeCall(game.rootClaim, ()), abi.encode(outputRoot)); - - // Prove the withdrawal transaction - optimismPortal2.proveWithdrawalTransaction(_tx, _proposedGameIndex, proof, withdrawalProof); - (IDisputeGame _game,) = optimismPortal2.provenWithdrawals(withdrawalHash, address(this)); - assertTrue(_game.rootClaim().raw() != bytes32(0)); - - // Resolve the dispute game - game.resolve(); - - // Warp past the finalization period - vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1); + _resolveGameAndWarpPastProofMaturity(game); // Change the respectedGameType vm.prank(optimismPortal2.guardian()); anchorStateRegistry.setRespectedGameType(_newGameType); // Withdrawal transaction still finalizable - vm.expectCallMinGas(_tx.target, _tx.value, uint64(_tx.gasLimit), _tx.data); - optimismPortal2.finalizeWithdrawalTransaction(_tx); + vm.expectCallMinGas(withdrawalTx.target, withdrawalTx.value, uint64(withdrawalTx.gasLimit), withdrawalTx.data); + optimismPortal2.finalizeWithdrawalTransaction(withdrawalTx); assertTrue(optimismPortal2.finalizedWithdrawals(withdrawalHash)); } /// @notice Tests that `finalizeWithdrawalTransaction` reverts if the withdrawal's dispute game /// has been blacklisted. function test_finalizeWithdrawalTransaction_blacklisted_reverts() external { - vm.expectEmit(true, true, true, true); - emit WithdrawalProven(_withdrawalHash, alice, bob); - vm.expectEmit(true, true, true, true); - emit WithdrawalProvenExtension1(_withdrawalHash, address(this)); - optimismPortal2.proveWithdrawalTransaction({ - _tx: _defaultTx, - _disputeGameIndex: _proposedGameIndex, - _outputRootProof: _outputRootProof, - _withdrawalProof: _withdrawalProof - }); + _proveDefaultWithdrawal(); // Resolve the dispute game. game.resolve(); @@ -1525,16 +1332,7 @@ contract OptimismPortal2_FinalizeWithdrawalTransaction_Test is OptimismPortal2_T /// @notice Tests that `finalizeWithdrawalTransaction` reverts if the withdrawal's dispute game /// is still in the air gap. function test_finalizeWithdrawalTransaction_gameInAirGap_reverts() external { - vm.expectEmit(true, true, true, true); - emit WithdrawalProven(_withdrawalHash, alice, bob); - vm.expectEmit(true, true, true, true); - emit WithdrawalProvenExtension1(_withdrawalHash, address(this)); - optimismPortal2.proveWithdrawalTransaction({ - _tx: _defaultTx, - _disputeGameIndex: _proposedGameIndex, - _outputRootProof: _outputRootProof, - _withdrawalProof: _withdrawalProof - }); + _proveDefaultWithdrawal(); // Warp past the finalization period. vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1); @@ -1555,16 +1353,7 @@ contract OptimismPortal2_FinalizeWithdrawalTransaction_Test is OptimismPortal2_T /// @notice Tests that `finalizeWithdrawalTransaction` reverts if the respected game type was /// updated after the dispute game was created. function test_finalizeWithdrawalTransaction_gameOlderThanRespectedGameTypeUpdate_reverts() external { - vm.expectEmit(address(optimismPortal2)); - emit WithdrawalProven(_withdrawalHash, alice, bob); - vm.expectEmit(address(optimismPortal2)); - emit WithdrawalProvenExtension1(_withdrawalHash, address(this)); - optimismPortal2.proveWithdrawalTransaction({ - _tx: _defaultTx, - _disputeGameIndex: _proposedGameIndex, - _outputRootProof: _outputRootProof, - _withdrawalProof: _withdrawalProof - }); + _proveDefaultWithdrawal(); // Warp past the finalization period. vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1); @@ -1588,16 +1377,7 @@ contract OptimismPortal2_FinalizeWithdrawalTransaction_Test is OptimismPortal2_T /// respected game type when it was created. `proveWithdrawalTransaction` should /// already prevent this, but we remove that assumption here. function test_finalizeWithdrawalTransaction_gameWasNotRespectedGameType_reverts() external { - vm.expectEmit(address(optimismPortal2)); - emit WithdrawalProven(_withdrawalHash, alice, bob); - vm.expectEmit(address(optimismPortal2)); - emit WithdrawalProvenExtension1(_withdrawalHash, address(this)); - optimismPortal2.proveWithdrawalTransaction({ - _tx: _defaultTx, - _disputeGameIndex: _proposedGameIndex, - _outputRootProof: _outputRootProof, - _withdrawalProof: _withdrawalProof - }); + _proveDefaultWithdrawal(); // Warp past the finalization period. vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1); @@ -1619,16 +1399,7 @@ contract OptimismPortal2_FinalizeWithdrawalTransaction_Test is OptimismPortal2_T /// `proveWithdrawalTransaction` should already prevent this, but we remove that /// assumption here. function test_finalizeWithdrawalTransaction_legacyGame_reverts() external { - vm.expectEmit(address(optimismPortal2)); - emit WithdrawalProven(_withdrawalHash, alice, bob); - vm.expectEmit(address(optimismPortal2)); - emit WithdrawalProvenExtension1(_withdrawalHash, address(this)); - optimismPortal2.proveWithdrawalTransaction({ - _tx: _defaultTx, - _disputeGameIndex: _proposedGameIndex, - _outputRootProof: _outputRootProof, - _withdrawalProof: _withdrawalProof - }); + _proveDefaultWithdrawal(); // Warp past the finalization period. vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1); @@ -1642,8 +1413,7 @@ contract OptimismPortal2_FinalizeWithdrawalTransaction_Test is OptimismPortal2_T // Mock the wasRespectedGameTypeWhenCreated call to revert. vm.mockCallRevert(address(game), abi.encodeCall(game.wasRespectedGameTypeWhenCreated, ()), ""); - // Should revert. - vm.expectRevert(); // nosemgrep: sol-safety-expectrevert-no-args + vm.expectRevert(bytes("")); optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); } @@ -1651,16 +1421,7 @@ contract OptimismPortal2_FinalizeWithdrawalTransaction_Test is OptimismPortal2_T /// correctness. function test_finalizeWithdrawalTransaction_delayEdges_succeeds() external { // Prove the withdrawal transaction. - vm.expectEmit(true, true, true, true); - emit WithdrawalProven(_withdrawalHash, alice, bob); - vm.expectEmit(true, true, true, true); - emit WithdrawalProvenExtension1(_withdrawalHash, address(this)); - optimismPortal2.proveWithdrawalTransaction({ - _tx: _defaultTx, - _disputeGameIndex: _proposedGameIndex, - _outputRootProof: _outputRootProof, - _withdrawalProof: _withdrawalProof - }); + _proveDefaultWithdrawal(); // Attempt to finalize the withdrawal transaction 1 second before the proof has matured. // This should fail. @@ -1698,22 +1459,10 @@ contract OptimismPortal2_FinalizeWithdrawalTransactionExternalProof_Test is Opti uint256 bobBalanceBefore = address(bob).balance; // Submit the first proof for the withdrawal hash. - vm.expectEmit(true, true, true, true); - emit WithdrawalProven(_withdrawalHash, alice, bob); - vm.expectEmit(true, true, true, true); - emit WithdrawalProvenExtension1(_withdrawalHash, address(this)); - optimismPortal2.proveWithdrawalTransaction({ - _tx: _defaultTx, - _disputeGameIndex: _proposedGameIndex, - _outputRootProof: _outputRootProof, - _withdrawalProof: _withdrawalProof - }); + _proveDefaultWithdrawal(); // Submit a second proof for the same withdrawal hash. - vm.expectEmit(true, true, true, true); - emit WithdrawalProven(_withdrawalHash, alice, bob); - vm.expectEmit(true, true, true, true); - emit WithdrawalProvenExtension1(_withdrawalHash, address(0xb0b)); + _expectWithdrawalProven(_withdrawalHash, address(0xb0b)); vm.prank(address(0xb0b)); optimismPortal2.proveWithdrawalTransaction({ _tx: _defaultTx, @@ -1722,9 +1471,7 @@ contract OptimismPortal2_FinalizeWithdrawalTransactionExternalProof_Test is Opti _withdrawalProof: _withdrawalProof }); - // Warp and resolve the dispute game. - game.resolve(); - vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1 seconds); + _resolveGameAndWarpPastProofMaturity(game); vm.expectEmit(true, true, false, true); emit WithdrawalFinalized(_withdrawalHash, true); @@ -1744,16 +1491,7 @@ contract OptimismPortal2_CheckWithdrawal_Test is OptimismPortal2_TestInit { /// game has been finalized, and the root claim is valid. function test_checkWithdrawal_succeeds() external { // Prove the withdrawal transaction. - vm.expectEmit(true, true, true, true); - emit WithdrawalProven(_withdrawalHash, alice, bob); - vm.expectEmit(true, true, true, true); - emit WithdrawalProvenExtension1(_withdrawalHash, address(this)); - optimismPortal2.proveWithdrawalTransaction({ - _tx: _defaultTx, - _disputeGameIndex: _proposedGameIndex, - _outputRootProof: _outputRootProof, - _withdrawalProof: _withdrawalProof - }); + _proveDefaultWithdrawal(); // Warp past the finalization period. vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1); @@ -1773,20 +1511,9 @@ contract OptimismPortal2_CheckWithdrawal_Test is OptimismPortal2_TestInit { /// @notice Tests that checkWithdrawal reverts if the withdrawal has already been finalized. function test_checkWithdrawal_ifAlreadyFinalized_reverts() external { // Prove the withdrawal transaction. - vm.expectEmit(true, true, true, true); - emit WithdrawalProven(_withdrawalHash, alice, bob); - vm.expectEmit(true, true, true, true); - emit WithdrawalProvenExtension1(_withdrawalHash, address(this)); - optimismPortal2.proveWithdrawalTransaction({ - _tx: _defaultTx, - _disputeGameIndex: _proposedGameIndex, - _outputRootProof: _outputRootProof, - _withdrawalProof: _withdrawalProof - }); + _proveDefaultWithdrawal(); - // Warp and resolve the dispute game. - game.resolve(); - vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1); + _resolveGameAndWarpPastProofMaturity(game); // Finalize the withdrawal. optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); @@ -1807,13 +1534,7 @@ contract OptimismPortal2_CheckWithdrawal_Test is OptimismPortal2_TestInit { /// @notice Tests that checkWithdrawal reverts if the proof timestamp is greater than the game /// creation timestamp. function testFuzz_checkWithdrawal_ifInvalidProofTimestamp_reverts(uint64 _createdAt) external { - // Prove the withdrawal transaction. - optimismPortal2.proveWithdrawalTransaction({ - _tx: _defaultTx, - _disputeGameIndex: _proposedGameIndex, - _outputRootProof: _outputRootProof, - _withdrawalProof: _withdrawalProof - }); + _proveDefaultWithdrawal(); // Mock the game creation timestamp to be greater than the proof timestamp. _createdAt = uint64(bound(_createdAt, block.timestamp, type(uint64).max)); @@ -1838,13 +1559,7 @@ contract OptimismPortal2_CheckWithdrawal_Test is OptimismPortal2_TestInit { /// @notice Tests that checkWithdrawal reverts if the proof timestamp is less than the proof /// maturity delay. function test_checkWithdrawal_ifProofNotOldEnough_reverts() external { - // Prove but don't warp ahead past the proof maturity delay. - optimismPortal2.proveWithdrawalTransaction({ - _tx: _defaultTx, - _disputeGameIndex: _proposedGameIndex, - _outputRootProof: _outputRootProof, - _withdrawalProof: _withdrawalProof - }); + _proveDefaultWithdrawal(); // Should revert. vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() - 1); @@ -1854,13 +1569,7 @@ contract OptimismPortal2_CheckWithdrawal_Test is OptimismPortal2_TestInit { /// @notice Tests that checkWithdrawal reverts if the root claim is invalid. function test_checkWithdrawal_ifInvalidRootClaim_reverts() external { - // Prove the withdrawal. - optimismPortal2.proveWithdrawalTransaction({ - _tx: _defaultTx, - _disputeGameIndex: _proposedGameIndex, - _outputRootProof: _outputRootProof, - _withdrawalProof: _withdrawalProof - }); + _proveDefaultWithdrawal(); // Warp past the proof maturity delay. vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1); diff --git a/test/L1/ResourceMetering.t.sol b/test/L1/ResourceMetering.t.sol index bcf7afce..20e8cd92 100644 --- a/test/L1/ResourceMetering.t.sol +++ b/test/L1/ResourceMetering.t.sol @@ -1,32 +1,28 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -// Testing -import { Test } from "lib/forge-std/src/Test.sol"; - -// Contracts +import { Test, stdError } from "lib/forge-std/src/Test.sol"; import { ResourceMetering } from "src/L1/ResourceMetering.sol"; - -// Libraries import { Constants } from "src/libraries/Constants.sol"; - -// Interfaces import { IResourceMetering } from "interfaces/L1/IResourceMetering.sol"; +uint256 constant MAX_EMPTY_BLOCK_GAP = 433_576_281_058_164_217_753_225_238_677_900_874_458_690; + +function defaultResourceConfig() pure returns (ResourceMetering.ResourceConfig memory) { + IResourceMetering.ResourceConfig memory rcfg = Constants.DEFAULT_RESOURCE_CONFIG(); + ResourceMetering.ResourceConfig memory config; + assembly ("memory-safe") { + config := rcfg + } + return config; +} + contract MeterUser is ResourceMetering { ResourceMetering.ResourceConfig public innerConfig; constructor() { initialize(); - IResourceMetering.ResourceConfig memory rcfg = Constants.DEFAULT_RESOURCE_CONFIG(); - innerConfig = ResourceMetering.ResourceConfig({ - maxResourceLimit: rcfg.maxResourceLimit, - elasticityMultiplier: rcfg.elasticityMultiplier, - baseFeeMaxChangeDenominator: rcfg.baseFeeMaxChangeDenominator, - minimumBaseFee: rcfg.minimumBaseFee, - systemTxMaxGas: rcfg.systemTxMaxGas, - maximumBaseFee: rcfg.maximumBaseFee - }); + innerConfig = defaultResourceConfig(); } function initialize() public initializer { @@ -43,6 +39,12 @@ contract MeterUser is ResourceMetering { function use(uint64 _amount) public metered(_amount) { } + function measuredUse(uint64 _amount) public returns (uint256) { + uint256 initialGas = gasleft(); + _metered(_amount, initialGas); + return initialGas - gasleft(); + } + function set(uint128 _prevBaseFee, uint64 _prevBoughtGas, uint64 _prevBlockNum) public { params = ResourceMetering.ResourceParams({ prevBaseFee: _prevBaseFee, prevBoughtGas: _prevBoughtGas, prevBlockNum: _prevBlockNum @@ -54,48 +56,14 @@ contract MeterUser is ResourceMetering { } } -/// @title CustomMeterUser -/// @notice A simple wrapper around `ResourceMetering` that allows the initial params to be set in -/// the constructor. -contract CustomMeterUser is ResourceMetering { - uint256 public startGas; - uint256 public endGas; - - constructor(uint128 _prevBaseFee, uint64 _prevBoughtGas, uint64 _prevBlockNum) { - params = ResourceMetering.ResourceParams({ - prevBaseFee: _prevBaseFee, prevBoughtGas: _prevBoughtGas, prevBlockNum: _prevBlockNum - }); - } - - function _resourceConfig() internal pure override returns (ResourceMetering.ResourceConfig memory) { - IResourceMetering.ResourceConfig memory rcfg = Constants.DEFAULT_RESOURCE_CONFIG(); - return ResourceMetering.ResourceConfig({ - maxResourceLimit: rcfg.maxResourceLimit, - elasticityMultiplier: rcfg.elasticityMultiplier, - baseFeeMaxChangeDenominator: rcfg.baseFeeMaxChangeDenominator, - minimumBaseFee: rcfg.minimumBaseFee, - systemTxMaxGas: rcfg.systemTxMaxGas, - maximumBaseFee: rcfg.maximumBaseFee - }); - } - - function use(uint64 _amount) public returns (uint256) { - uint256 initialGas = gasleft(); - _metered(_amount, initialGas); - return initialGas - gasleft(); - } -} - /// @title ResourceMetering_TestInit /// @notice Reusable test initialization for `ResourceMetering` tests. abstract contract ResourceMetering_TestInit is Test { MeterUser internal meter; - uint64 initialBlockNum; /// @notice Sets up the test contract. function setUp() public { meter = new MeterUser(); - initialBlockNum = uint64(block.number); } } @@ -119,13 +87,14 @@ contract ResourceMetering_Metered_Test is ResourceMetering_TestInit { /// @notice Tests that updating after multiple empty blocks maintains correct base fee. function testFuzz_metered_emptyBlocks_succeeds(uint256 _blockDiff) external { _blockDiff = bound(_blockDiff, 1, 100); - vm.roll(initialBlockNum + _blockDiff); + uint256 startBlock = block.number; + vm.roll(startBlock + _blockDiff); meter.use(0); (uint128 prevBaseFee, uint64 prevBoughtGas, uint64 prevBlockNum) = meter.params(); assertEq(prevBaseFee, 1 gwei); assertEq(prevBoughtGas, 0); - assertEq(prevBlockNum, initialBlockNum + _blockDiff); + assertEq(prevBlockNum, startBlock + _blockDiff); } /// @notice Tests that updating the gas delta sets the meter params correctly. @@ -135,23 +104,22 @@ contract ResourceMetering_Metered_Test is ResourceMetering_TestInit { meter.use(uint64(target)); (uint128 prevBaseFee, uint64 prevBoughtGas, uint64 prevBlockNum) = meter.params(); - assertEq(prevBaseFee, 1000000000); + assertEq(prevBaseFee, rcfg.minimumBaseFee); assertEq(prevBoughtGas, target); - assertEq(prevBlockNum, initialBlockNum); + assertEq(prevBlockNum, block.number); } /// @notice Tests that the meter params are set correctly for the maximum gas delta. function test_metered_useMax_succeeds() external { ResourceMetering.ResourceConfig memory rcfg = meter.resourceConfig(); - uint64 target = uint64(rcfg.maxResourceLimit) / uint64(rcfg.elasticityMultiplier); - uint64 elasticityMultiplier = uint64(rcfg.elasticityMultiplier); + uint64 maxResourceLimit = uint64(rcfg.maxResourceLimit); - meter.use(target * elasticityMultiplier); + meter.use(maxResourceLimit); (, uint64 prevBoughtGas,) = meter.params(); - assertEq(prevBoughtGas, target * elasticityMultiplier); + assertEq(prevBoughtGas, maxResourceLimit); - vm.roll(initialBlockNum + 1); + vm.roll(block.number + 1); meter.use(0); (uint128 postBaseFee,,) = meter.params(); assertEq(postBaseFee, 2125000000); @@ -163,16 +131,15 @@ contract ResourceMetering_Metered_Test is ResourceMetering_TestInit { /// computed as 0. function test_metered_denominatorEq1_reverts() external { ResourceMetering.ResourceConfig memory rcfg = meter.resourceConfig(); - uint64 target = uint64(rcfg.maxResourceLimit) / uint64(rcfg.elasticityMultiplier); - uint64 elasticityMultiplier = uint64(rcfg.elasticityMultiplier); + uint64 maxResourceLimit = uint64(rcfg.maxResourceLimit); rcfg.baseFeeMaxChangeDenominator = 1; meter.setParams(rcfg); - meter.use(target * elasticityMultiplier); + meter.use(maxResourceLimit); (, uint64 prevBoughtGas,) = meter.params(); - assertEq(prevBoughtGas, target * elasticityMultiplier); + assertEq(prevBoughtGas, maxResourceLimit); - vm.roll(initialBlockNum + 2); + vm.roll(block.number + 2); vm.expectRevert("UNDEFINED"); meter.use(0); @@ -181,26 +148,21 @@ contract ResourceMetering_Metered_Test is ResourceMetering_TestInit { /// @notice Tests that the metered modifier reverts if the value is greater than allowed. function test_metered_useMoreThanMax_reverts() external { ResourceMetering.ResourceConfig memory rcfg = meter.resourceConfig(); - uint64 target = uint64(rcfg.maxResourceLimit) / uint64(rcfg.elasticityMultiplier); - uint64 elasticityMultiplier = uint64(rcfg.elasticityMultiplier); vm.expectRevert(ResourceMetering.OutOfGas.selector); - meter.use(target * elasticityMultiplier + 1); + meter.use(uint64(rcfg.maxResourceLimit) + 1); } /// @notice Tests that resource metering can handle large gaps between deposits. function testFuzz_metered_largeBlockDiff_succeeds(uint64 _amount, uint256 _blockDiff) external { - // This test fails if the following line is commented out. - // At 12 seconds per block, this number is effectively unreachable. - _blockDiff = uint256(bound(_blockDiff, 0, 433576281058164217753225238677900874458690)); + // Bounds the empty block gap to values that keep the base fee decay math in range. + _blockDiff = uint256(bound(_blockDiff, 0, MAX_EMPTY_BLOCK_GAP)); ResourceMetering.ResourceConfig memory rcfg = meter.resourceConfig(); - uint64 target = uint64(rcfg.maxResourceLimit) / uint64(rcfg.elasticityMultiplier); - uint64 elasticityMultiplier = uint64(rcfg.elasticityMultiplier); - _amount = uint64(bound(_amount, 0, target * elasticityMultiplier)); + _amount = uint64(bound(_amount, 0, rcfg.maxResourceLimit)); - vm.roll(initialBlockNum + _blockDiff); + vm.roll(block.number + _blockDiff); meter.use(_amount); } @@ -274,7 +236,7 @@ contract ResourceMetering_ResourceMeteringInit_Test is ResourceMetering_TestInit assertEq(prevBaseFee, rcfg.minimumBaseFee); assertEq(prevBoughtGas, 0); - assertEq(prevBlockNum, initialBlockNum); + assertEq(prevBlockNum, block.number); } /// @notice Tests that reinitializing the resource params are set correctly. @@ -298,36 +260,8 @@ contract ResourceMetering_ResourceMeteringInit_Test is ResourceMetering_TestInit /// information about how much gas is used and how expensive it is in USD terms to purchase /// the deposit gas. contract ArtifactResourceMetering_Metered_Test is Test { - uint128 internal minimumBaseFee; - uint128 internal maximumBaseFee; - uint64 internal maxResourceLimit; - uint64 internal targetResourceLimit; - string internal outfile; - // keccak256(abi.encodeWithSignature("Error(string)", "ResourceMetering: cannot buy more gas - // than available gas limit")) - bytes32 internal cannotBuyMoreGas = 0x84edc668cfd5e050b8999f43ff87a1faaa93e5f935b20bc1dd4d3ff157ccf429; - // keccak256(abi.encodeWithSignature("Panic(uint256)", 0x11)) - bytes32 internal overflowErr = 0x1ca389f2c8264faa4377de9ce8e14d6263ef29c68044a9272d405761bab2db27; - // keccak256(hex"") - bytes32 internal emptyReturnData = 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470; - - /// @notice Sets up the tests with constants from the ResourceMetering contract. - function setUp() public { - vm.roll(1_000_000); - - MeterUser base = new MeterUser(); - ResourceMetering.ResourceConfig memory rcfg = base.resourceConfig(); - minimumBaseFee = uint128(rcfg.minimumBaseFee); - maximumBaseFee = rcfg.maximumBaseFee; - maxResourceLimit = uint64(rcfg.maxResourceLimit); - targetResourceLimit = uint64(rcfg.maxResourceLimit) / uint64(rcfg.elasticityMultiplier); - - outfile = string.concat(vm.projectRoot(), "/.resource-metering.csv"); - try vm.removeFile(outfile) { } catch { } - } - /// @notice Generates a CSV file. No more than the L1 block gas limit should be supplied to the /// `meter` function to avoid long execution time. This test is skipped because there /// is no need to run it every time. It generates a CSV file on disk that can be used @@ -336,6 +270,16 @@ contract ArtifactResourceMetering_Metered_Test is Test { function test_meter_generateArtifact_succeeds() external { vm.skip({ skipTest: true }); + vm.roll(1_000_000); + + ResourceMetering.ResourceConfig memory rcfg = defaultResourceConfig(); + uint128 minimumBaseFee = uint128(rcfg.minimumBaseFee); + uint128 maximumBaseFee = rcfg.maximumBaseFee; + uint64 maxResourceLimit = uint64(rcfg.maxResourceLimit); + uint64 targetResourceLimit = uint64(rcfg.maxResourceLimit) / uint64(rcfg.elasticityMultiplier); + outfile = string.concat(vm.projectRoot(), "/.resource-metering.csv"); + try vm.removeFile(outfile) { } catch { } + vm.writeLine( outfile, "prevBaseFee,prevBoughtGas,prevBlockNumDiff,l1BaseFee,requestedGas,gasConsumed,ethPrice,usdCost,success" @@ -349,10 +293,6 @@ contract ArtifactResourceMetering_Metered_Test is Test { prevBaseFees[3] = uint128(100 gwei); prevBaseFees[4] = uint128(200 gwei); - // prevBoughtGas value in ResourceParams - uint64[] memory prevBoughtGases = new uint64[](1); - prevBoughtGases[0] = uint64(0); - // prevBlockNum diff, simulates blocks with no deposits when non zero uint64[] memory prevBlockNumDiffs = new uint64[](2); prevBlockNumDiffs[0] = 0; @@ -378,79 +318,77 @@ contract ArtifactResourceMetering_Metered_Test is Test { // Iterate over all of the test values and run a test for (uint256 i; i < prevBaseFees.length; i++) { - for (uint256 j; j < prevBoughtGases.length; j++) { - for (uint256 k; k < prevBlockNumDiffs.length; k++) { - for (uint256 l; l < requestedGases.length; l++) { - for (uint256 m; m < l1BaseFees.length; m++) { - for (uint256 n; n < ethPrices.length; n++) { - uint256 snapshotId = vm.snapshot(); - - uint128 prevBaseFee = prevBaseFees[i]; - uint64 prevBoughtGas = prevBoughtGases[j]; - uint64 prevBlockNumDiff = prevBlockNumDiffs[k]; - uint64 requestedGas = requestedGases[l]; - uint256 l1BaseFee = l1BaseFees[m]; - uint256 ethPrice = ethPrices[n]; - string memory result = "success"; - - vm.fee(l1BaseFee); - - CustomMeterUser meter = new CustomMeterUser({ - _prevBaseFee: prevBaseFee, - _prevBoughtGas: prevBoughtGas, - _prevBlockNum: uint64(block.number) - }); - - vm.roll(block.number + prevBlockNumDiff); - - // Call the metering code and catch the various - // types of errors. - uint256 gasConsumed = 0; - try meter.use{ gas: 30_000_000 }(requestedGas) returns (uint256 gasConsumed_) { - gasConsumed = gasConsumed_; - } catch (bytes memory err) { - bytes32 hash = keccak256(err); - if (hash == cannotBuyMoreGas) { - result = "ResourceMetering: cannot buy more gas than available gas limit"; - } else if (hash == overflowErr) { - result = "arithmetic overflow/underflow"; - } else if (hash == emptyReturnData) { - result = "out of gas"; - } else { - result = "UNKNOWN ERROR"; + for (uint256 k; k < prevBlockNumDiffs.length; k++) { + for (uint256 l; l < requestedGases.length; l++) { + for (uint256 m; m < l1BaseFees.length; m++) { + for (uint256 n; n < ethPrices.length; n++) { + uint256 snapshotId = vm.snapshot(); + + uint128 prevBaseFee = prevBaseFees[i]; + uint64 prevBoughtGas = 0; + uint64 prevBlockNumDiff = prevBlockNumDiffs[k]; + uint64 requestedGas = requestedGases[l]; + uint256 l1BaseFee = l1BaseFees[m]; + uint256 ethPrice = ethPrices[n]; + string memory result = "success"; + + vm.fee(l1BaseFee); + + MeterUser meter = new MeterUser(); + meter.set(prevBaseFee, prevBoughtGas, uint64(block.number)); + + vm.roll(block.number + prevBlockNumDiff); + + uint256 gasConsumed = 0; + try meter.measuredUse{ gas: 30_000_000 }(requestedGas) returns (uint256 gasConsumed_) { + gasConsumed = gasConsumed_; + } catch (bytes memory err) { + bytes4 selector; + if (err.length >= 4) { + assembly { + selector := mload(add(err, 0x20)) } } - // Compute the USD cost of the gas used - uint256 usdCost = (gasConsumed * l1BaseFee * ethPrice) / 1 ether; - - vm.writeLine( - outfile, - string.concat( - vm.toString(prevBaseFee), - ",", - vm.toString(prevBoughtGas), - ",", - vm.toString(prevBlockNumDiff), - ",", - vm.toString(l1BaseFee), - ",", - vm.toString(requestedGas), - ",", - vm.toString(gasConsumed), - ",", - "$", - vm.toString(ethPrice), - ",", - "$", - vm.toString(usdCost), - ",", - result - ) - ); - - assertTrue(vm.revertTo(snapshotId)); + if (selector == ResourceMetering.OutOfGas.selector) { + result = "ResourceMetering: cannot buy more gas than available gas limit"; + } else if (keccak256(err) == keccak256(stdError.arithmeticError)) { + result = "arithmetic overflow/underflow"; + } else if (err.length == 0) { + result = "out of gas"; + } else { + result = "UNKNOWN ERROR"; + } } + + uint256 usdCost = (gasConsumed * l1BaseFee * ethPrice) / 1 ether; + + vm.writeLine( + outfile, + string.concat( + vm.toString(prevBaseFee), + ",", + vm.toString(prevBoughtGas), + ",", + vm.toString(prevBlockNumDiff), + ",", + vm.toString(l1BaseFee), + ",", + vm.toString(requestedGas), + ",", + vm.toString(gasConsumed), + ",", + "$", + vm.toString(ethPrice), + ",", + "$", + vm.toString(usdCost), + ",", + result + ) + ); + + assertTrue(vm.revertTo(snapshotId)); } } } diff --git a/test/L1/SuperchainConfig.t.sol b/test/L1/SuperchainConfig.t.sol index 95f5b1bc..8b6ec06d 100644 --- a/test/L1/SuperchainConfig.t.sol +++ b/test/L1/SuperchainConfig.t.sol @@ -10,10 +10,17 @@ import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; /// @title SuperchainConfig_TestInit /// @notice Initialization contract for SuperchainConfig tests. abstract contract SuperchainConfig_TestInit is CommonTest { + uint256 internal constant PAUSE_EXPIRY = 7_884_000; + function setUp() public virtual override { super.setUp(); skipIfForkTest("SuperchainConfig_TestInit: cannot test initialization on forked network"); } + + function _pauseAsGuardian(address _identifier) internal { + vm.prank(superchainConfig.guardian()); + superchainConfig.pause(_identifier); + } } /// @title SuperchainConfig_Constructor_Test @@ -31,7 +38,7 @@ contract SuperchainConfig_Constructor_Test is SuperchainConfig_TestInit { contract SuperchainConfig_PauseExpiry_Test is SuperchainConfig_TestInit { /// @notice Tests that `pauseExpiry` returns the correct constant value. function test_pauseExpiry_succeeds() external view { - assertEq(superchainConfig.pauseExpiry(), 7_884_000); + assertEq(superchainConfig.pauseExpiry(), PAUSE_EXPIRY); } } @@ -40,67 +47,41 @@ contract SuperchainConfig_PauseExpiry_Test is SuperchainConfig_TestInit { contract SuperchainConfig_Paused_Test is SuperchainConfig_TestInit { /// @notice Tests that `paused` returns true when the specific identifier is paused. /// @param _identifier The identifier to test. - function testFuzz_paused_specificIdentifier_succeeds(address _identifier) external { - // Assume the identifier is not the zero address. + /// @param _other The unpaused identifier to test. + function testFuzz_paused_specificIdentifier_succeeds(address _identifier, address _other) external { vm.assume(_identifier != address(0)); + vm.assume(_other != _identifier); - // Pause with the specific identifier. - vm.prank(superchainConfig.guardian()); - superchainConfig.pause(_identifier); - - // Assert that the specific identifier is paused. + _pauseAsGuardian(_identifier); assertTrue(superchainConfig.paused(_identifier)); - - // Pick a random address that is not the identifier. - address other = vm.randomAddress(); - while (other == _identifier) { - other = vm.randomAddress(); - } - - // Assert that the other address is not paused. - assertFalse(superchainConfig.paused(other)); + assertFalse(superchainConfig.paused(_other)); } /// @notice Tests that `paused` returns true when the global superchain system is paused. function test_paused_global_succeeds() external { - // Pause the global superchain system. - vm.prank(superchainConfig.guardian()); - superchainConfig.pause(address(0)); + _pauseAsGuardian(address(0)); - // Assert that the global superchain system is paused. assertTrue(superchainConfig.paused()); assertTrue(superchainConfig.paused(address(0))); - - // Pick a random address that is not the zero address. - address other = vm.randomAddress(); - while (other == address(0)) { - other = vm.randomAddress(); - } - - // Assert that the other address is not paused. - assertFalse(superchainConfig.paused(other)); + assertFalse(superchainConfig.paused(address(1))); } /// @notice Tests that `paused` returns false after pause expires. /// @param _identifier The identifier to test. function testFuzz_paused_expired_succeeds(address _identifier) external { - vm.prank(superchainConfig.guardian()); - superchainConfig.pause(_identifier); + _pauseAsGuardian(_identifier); assertTrue(superchainConfig.paused(_identifier)); - // Warp past expiry - vm.warp(block.timestamp + superchainConfig.pauseExpiry() + 1); + vm.warp(block.timestamp + PAUSE_EXPIRY + 1); assertFalse(superchainConfig.paused(_identifier)); } /// @notice Tests that `paused` returns true just before expiry. /// @param _identifier The identifier to test. function testFuzz_paused_beforeExpiry_succeeds(address _identifier) external { - vm.prank(superchainConfig.guardian()); - superchainConfig.pause(_identifier); + _pauseAsGuardian(_identifier); - // Warp to just before expiry - vm.warp(block.timestamp + superchainConfig.pauseExpiry() - 1); + vm.warp(block.timestamp + PAUSE_EXPIRY - 1); assertTrue(superchainConfig.paused(_identifier)); } } @@ -130,18 +111,15 @@ contract SuperchainConfig_Pause_Test is SuperchainConfig_TestInit { vm.expectRevert(ISuperchainConfig.SuperchainConfig_OnlyGuardianOrIncidentResponder.selector); vm.prank(_caller); superchainConfig.pause(address(this)); - - assertFalse(superchainConfig.paused(address(this))); } /// @notice Tests that `pause` reverts when the identifier is already used. /// @param _identifier The identifier to test. function testFuzz_pause_alreadyUsed_reverts(address _identifier) external { - vm.startPrank(superchainConfig.guardian()); - superchainConfig.pause(_identifier); + _pauseAsGuardian(_identifier); + vm.prank(superchainConfig.guardian()); vm.expectRevert(abi.encodeWithSelector(ISuperchainConfig.SuperchainConfig_AlreadyPaused.selector, _identifier)); - superchainConfig.pause(_identifier); } } @@ -152,12 +130,12 @@ contract SuperchainConfig_Unpause_Test is SuperchainConfig_TestInit { /// @notice Tests that `unpause` successfully unpauses when called by the guardian. /// @param _identifier The identifier to test. function testFuzz_unpause_succeeds(address _identifier) external { - vm.startPrank(superchainConfig.guardian()); - superchainConfig.pause(_identifier); + _pauseAsGuardian(_identifier); assertTrue(superchainConfig.paused(_identifier)); vm.expectEmit(address(superchainConfig)); emit Unpaused(_identifier); + vm.prank(superchainConfig.guardian()); superchainConfig.unpause(_identifier); assertFalse(superchainConfig.paused(_identifier)); @@ -168,15 +146,12 @@ contract SuperchainConfig_Unpause_Test is SuperchainConfig_TestInit { function testFuzz_unpause_notGuardian_reverts(address _caller) external { vm.assume(_caller != superchainConfig.guardian()); - vm.prank(superchainConfig.guardian()); - superchainConfig.pause(address(this)); + _pauseAsGuardian(address(this)); assertTrue(superchainConfig.paused(address(this))); vm.expectRevert(ISuperchainConfig.SuperchainConfig_OnlyGuardian.selector); vm.prank(_caller); superchainConfig.unpause(address(this)); - - assertTrue(superchainConfig.paused(address(this))); } } @@ -186,14 +161,14 @@ contract SuperchainConfig_Extend_Test is SuperchainConfig_TestInit { /// @notice Tests that `extend` successfully resets and re-pauses an identifier. /// @param _identifier The identifier to test. function testFuzz_extend_succeeds(address _identifier) external { - vm.startPrank(superchainConfig.guardian()); - superchainConfig.pause(_identifier); + _pauseAsGuardian(_identifier); uint256 firstPauseTimestamp = block.timestamp; vm.warp(block.timestamp + 1); vm.expectEmit(address(superchainConfig)); emit PauseExtended(_identifier); + vm.prank(superchainConfig.guardian()); superchainConfig.extend(_identifier); assertTrue(superchainConfig.pauseTimestamps(_identifier) > firstPauseTimestamp); assertTrue(superchainConfig.paused(_identifier)); @@ -204,11 +179,10 @@ contract SuperchainConfig_Extend_Test is SuperchainConfig_TestInit { function testFuzz_extend_notGuardian_reverts(address _caller) external { vm.assume(_caller != superchainConfig.guardian()); - vm.prank(superchainConfig.guardian()); - superchainConfig.pause(address(this)); + _pauseAsGuardian(address(this)); - vm.prank(_caller); vm.expectRevert(ISuperchainConfig.SuperchainConfig_OnlyGuardian.selector); + vm.prank(_caller); superchainConfig.extend(address(this)); } @@ -235,23 +209,19 @@ contract SuperchainConfig_Pausable_Test is SuperchainConfig_TestInit { /// @notice Tests that `pausable` returns false when the identifier is paused. /// @param _identifier The identifier to test. function testFuzz_pausable_paused_succeeds(address _identifier) external { - vm.prank(superchainConfig.guardian()); - superchainConfig.pause(_identifier); + _pauseAsGuardian(_identifier); assertFalse(superchainConfig.pausable(_identifier)); } /// @notice Tests that `pausable` returns false even after pause expires. /// @param _identifier The identifier to test. function testFuzz_pausable_expired_succeeds(address _identifier) external { - vm.prank(superchainConfig.guardian()); - superchainConfig.pause(_identifier); + _pauseAsGuardian(_identifier); - // Warp past expiry - vm.warp(block.timestamp + superchainConfig.pauseExpiry() + 1); + vm.warp(block.timestamp + PAUSE_EXPIRY + 1); - // pausable() should still return false because timestamp is set + // Expired pauses remain unpausable because the timestamp stays set. assertFalse(superchainConfig.pausable(_identifier)); - // But paused() should return false because pause expired assertFalse(superchainConfig.paused(_identifier)); } } @@ -277,18 +247,17 @@ contract SuperchainConfig_PauseTimestamps_Test is SuperchainConfig_TestInit { /// @notice Tests that `pauseTimestamps` returns the correct timestamp for paused identifiers. /// @param _identifier The identifier to test. function testFuzz_pauseTimestamps_paused_succeeds(address _identifier) external { - vm.prank(superchainConfig.guardian()); - superchainConfig.pause(_identifier); + _pauseAsGuardian(_identifier); assertEq(superchainConfig.pauseTimestamps(_identifier), block.timestamp); } /// @notice Tests that `pauseTimestamps` returns 0 after unpausing. /// @param _identifier The identifier to test. function testFuzz_pauseTimestamps_afterUnpause_succeeds(address _identifier) external { - vm.startPrank(superchainConfig.guardian()); - superchainConfig.pause(_identifier); + _pauseAsGuardian(_identifier); assertTrue(superchainConfig.pauseTimestamps(_identifier) != 0); + vm.prank(superchainConfig.guardian()); superchainConfig.unpause(_identifier); assertEq(superchainConfig.pauseTimestamps(_identifier), 0); } @@ -314,39 +283,27 @@ contract SuperchainConfig_Expiration_Test is SuperchainConfig_TestInit { /// @notice Tests that `expiration` returns the correct timestamp when the identifier is /// paused. function test_expiration_paused_succeeds() external { - vm.prank(superchainConfig.guardian()); - superchainConfig.pause(address(this)); - uint256 expectedExpiration = block.timestamp + superchainConfig.pauseExpiry(); - assertEq(superchainConfig.expiration(address(this)), expectedExpiration); + _pauseAsGuardian(address(this)); + assertEq(superchainConfig.expiration(address(this)), block.timestamp + PAUSE_EXPIRY); } /// @notice Tests that `expiration` returns the updated timestamp after extending the pause. function test_expiration_afterExtend_succeeds() external { - vm.startPrank(superchainConfig.guardian()); - superchainConfig.pause(address(this)); - uint256 firstExpiration = superchainConfig.expiration(address(this)); - - // Warp forward in time + _pauseAsGuardian(address(this)); vm.warp(block.timestamp + 100); - // Extend the pause + vm.prank(superchainConfig.guardian()); superchainConfig.extend(address(this)); - uint256 newExpiration = superchainConfig.expiration(address(this)); - assertTrue(newExpiration > firstExpiration); - assertEq(newExpiration, block.timestamp + superchainConfig.pauseExpiry()); + assertEq(superchainConfig.expiration(address(this)), block.timestamp + PAUSE_EXPIRY); } /// @notice Tests that `expiration` works correctly with fuzzed identifiers. /// @param _identifier The identifier to test. function testFuzz_expiration_succeeds(address _identifier) external { - // Test unpaused state assertEq(superchainConfig.expiration(_identifier), 0); - // Test paused state - vm.prank(superchainConfig.guardian()); - superchainConfig.pause(_identifier); - uint256 expectedExpiration = block.timestamp + superchainConfig.pauseExpiry(); - assertEq(superchainConfig.expiration(_identifier), expectedExpiration); + _pauseAsGuardian(_identifier); + assertEq(superchainConfig.expiration(_identifier), block.timestamp + PAUSE_EXPIRY); } } diff --git a/test/L1/SystemConfig.t.sol b/test/L1/SystemConfig.t.sol index f51bfc8b..796edaed 100644 --- a/test/L1/SystemConfig.t.sol +++ b/test/L1/SystemConfig.t.sol @@ -24,6 +24,7 @@ abstract contract SystemConfig_TestInit is CommonTest { event ConfigUpdate(uint256 indexed version, ISystemConfig.UpdateType indexed updateType, bytes data); bytes32 public constant EXAMPLE_FEATURE = "EXAMPLE_FEATURE"; + bytes32 internal constant INITIALIZED_SLOT = bytes32(0); address owner; bytes32 batcherHash; @@ -45,6 +46,42 @@ abstract contract SystemConfig_TestInit is CommonTest { systemConfigImpl = EIP1967Helper.getImplementation(address(systemConfig)); optimismMintableERC20Factory = artifacts.mustGetAddress("OptimismMintableERC20FactoryProxy"); } + + function _resetInitialized() internal { + vm.store(address(systemConfig), INITIALIZED_SLOT, bytes32(0)); + } + + function _initializedValue() internal view returns (uint8) { + bytes32 slotVal = vm.load(address(systemConfig), INITIALIZED_SLOT); + return uint8(uint256(slotVal) & 0xFF); + } + + function _emptyAddresses() internal pure returns (ISystemConfig.Addresses memory) { + return ISystemConfig.Addresses({ + l1CrossDomainMessenger: address(0), + l1ERC721Bridge: address(0), + l1StandardBridge: address(0), + optimismPortal: address(0), + optimismMintableERC20Factory: address(0), + delayedWETH: address(0) + }); + } + + function _initializeSystemConfig(uint64 _gasLimit, IResourceMetering.ResourceConfig memory _config) internal { + systemConfig.initialize({ + _owner: alice, + _basefeeScalar: basefeeScalar, + _blobbasefeeScalar: blobbasefeeScalar, + _batcherHash: bytes32(hex"abcd"), + _gasLimit: _gasLimit, + _unsafeBlockSigner: address(1), + _config: _config, + _batchInbox: address(0), + _addresses: _emptyAddresses(), + _l2ChainId: 1234, + _superchainConfig: ISuperchainConfig(address(0)) + }); + } } /// @title SystemConfig_Version_Test @@ -139,48 +176,18 @@ contract SystemConfig_Initialize_Test is SystemConfig_TestInit { function test_initialize_lowGasLimit_reverts() external { uint64 minimumGasLimit = systemConfig.minimumGasLimit(); - // Wipe out the initialized slot so the proxy can be initialized again - vm.store(address(systemConfig), bytes32(0), bytes32(0)); - - address admin = address(uint160(uint256(vm.load(address(systemConfig), Constants.PROXY_OWNER_ADDRESS)))); - vm.prank(admin); + _resetInitialized(); + vm.prank(address(systemConfig.proxyAdmin())); vm.expectRevert("SystemConfig: gas limit too low"); - systemConfig.initialize({ - _owner: alice, - _basefeeScalar: basefeeScalar, - _blobbasefeeScalar: blobbasefeeScalar, - _batcherHash: bytes32(hex"abcd"), - _gasLimit: minimumGasLimit - 1, - _unsafeBlockSigner: address(1), - _config: Constants.DEFAULT_RESOURCE_CONFIG(), - _batchInbox: address(0), - _addresses: ISystemConfig.Addresses({ - l1CrossDomainMessenger: address(0), - l1ERC721Bridge: address(0), - l1StandardBridge: address(0), - optimismPortal: address(0), - optimismMintableERC20Factory: address(0), - delayedWETH: address(0) - }), - _l2ChainId: 1234, - _superchainConfig: ISuperchainConfig(address(0)) - }); + _initializeSystemConfig(minimumGasLimit - 1, Constants.DEFAULT_RESOURCE_CONFIG()); } /// @notice Tests that the initializer value is correct. Trivial test for normal /// initialization but confirms that the initValue is not incremented incorrectly if /// an upgrade function is not present. function test_initialize_correctInitializerValue_succeeds() public view { - // Get the slot for _initialized. - StorageSlot memory slot = ForgeArtifacts.getSlot("SystemConfig", "_initialized"); - - // Get the initializer value. - bytes32 slotVal = vm.load(address(systemConfig), bytes32(slot.slot)); - uint8 val = uint8(uint256(slotVal) & 0xFF); - - // Assert that the initializer value matches the expected value. - assertEq(val, systemConfig.initVersion()); + assertEq(_initializedValue(), systemConfig.initVersion()); } /// @notice Tests that `initialize` reverts if called by a non-proxy admin or owner. @@ -189,40 +196,16 @@ contract SystemConfig_Initialize_Test is SystemConfig_TestInit { // Prank as the not ProxyAdmin or ProxyAdmin owner. vm.assume(_sender != address(systemConfig.proxyAdmin()) && _sender != systemConfig.proxyAdminOwner()); - // Get the slot for _initialized. - StorageSlot memory slot = ForgeArtifacts.getSlot("SystemConfig", "_initialized"); - - // Set the initialized slot to 0. - vm.store(address(systemConfig), bytes32(slot.slot), bytes32(0)); + _resetInitialized(); // Get the minimum gas limit. uint64 minimumGasLimit = systemConfig.minimumGasLimit(); // Expect the revert with `ProxyAdminOwnedBase_NotProxyAdminOrProxyAdminOwner` selector. + vm.prank(_sender); vm.expectRevert(IProxyAdminOwnedBase.ProxyAdminOwnedBase_NotProxyAdminOrProxyAdminOwner.selector); - // Call the `initialize` function with the sender - vm.prank(_sender); - systemConfig.initialize({ - _owner: alice, - _basefeeScalar: basefeeScalar, - _blobbasefeeScalar: blobbasefeeScalar, - _batcherHash: bytes32(hex"abcd"), - _gasLimit: minimumGasLimit - 1, - _unsafeBlockSigner: address(1), - _config: Constants.DEFAULT_RESOURCE_CONFIG(), - _batchInbox: address(0), - _addresses: ISystemConfig.Addresses({ - l1CrossDomainMessenger: address(0), - l1ERC721Bridge: address(0), - l1StandardBridge: address(0), - optimismPortal: address(0), - optimismMintableERC20Factory: address(0), - delayedWETH: address(0) - }), - _l2ChainId: 1234, - _superchainConfig: ISuperchainConfig(address(0)) - }); + _initializeSystemConfig(minimumGasLimit - 1, Constants.DEFAULT_RESOURCE_CONFIG()); } } @@ -231,65 +214,25 @@ contract SystemConfig_Initialize_Test is SystemConfig_TestInit { contract SystemConfig_StartBlock_Test is SystemConfig_TestInit { /// @notice Tests that startBlock is updated correctly when it's zero. function test_startBlock_update_succeeds() external { - // Wipe out the initialized slot so the proxy can be initialized again - vm.store(address(systemConfig), bytes32(0), bytes32(0)); + _resetInitialized(); // Set slot startBlock to zero vm.store(address(systemConfig), systemConfig.START_BLOCK_SLOT(), bytes32(uint256(0))); // Initialize and check that StartBlock updates to current block number vm.prank(address(systemConfig.proxyAdmin())); - systemConfig.initialize({ - _owner: alice, - _basefeeScalar: basefeeScalar, - _blobbasefeeScalar: blobbasefeeScalar, - _batcherHash: bytes32(hex"abcd"), - _gasLimit: gasLimit, - _unsafeBlockSigner: address(1), - _config: Constants.DEFAULT_RESOURCE_CONFIG(), - _batchInbox: address(0), - _addresses: ISystemConfig.Addresses({ - l1CrossDomainMessenger: address(0), - l1ERC721Bridge: address(0), - l1StandardBridge: address(0), - optimismPortal: address(0), - optimismMintableERC20Factory: address(0), - delayedWETH: address(0) - }), - _l2ChainId: 1234, - _superchainConfig: ISuperchainConfig(address(0)) - }); + _initializeSystemConfig(gasLimit, Constants.DEFAULT_RESOURCE_CONFIG()); assertEq(systemConfig.startBlock(), block.number); } /// @notice Tests that startBlock is not updated when it's not zero. function test_startBlock_update_fails() external { - // Wipe out the initialized slot so the proxy can be initialized again - vm.store(address(systemConfig), bytes32(0), bytes32(0)); + _resetInitialized(); // Set slot startBlock to non-zero value 1 vm.store(address(systemConfig), systemConfig.START_BLOCK_SLOT(), bytes32(uint256(1))); // Initialize and check that StartBlock doesn't update vm.prank(address(systemConfig.proxyAdmin())); - systemConfig.initialize({ - _owner: alice, - _basefeeScalar: basefeeScalar, - _blobbasefeeScalar: blobbasefeeScalar, - _batcherHash: bytes32(hex"abcd"), - _gasLimit: gasLimit, - _unsafeBlockSigner: address(1), - _config: Constants.DEFAULT_RESOURCE_CONFIG(), - _batchInbox: address(0), - _addresses: ISystemConfig.Addresses({ - l1CrossDomainMessenger: address(0), - l1ERC721Bridge: address(0), - l1StandardBridge: address(0), - optimismPortal: address(0), - optimismMintableERC20Factory: address(0), - delayedWETH: address(0) - }), - _l2ChainId: 1234, - _superchainConfig: ISuperchainConfig(address(0)) - }); + _initializeSystemConfig(gasLimit, Constants.DEFAULT_RESOURCE_CONFIG()); assertEq(systemConfig.startBlock(), 1); } } @@ -392,8 +335,7 @@ contract SystemConfig_SetGasConfigEcotone_Test is SystemConfig_TestInit { } function testFuzz_setGasConfigEcotone_succeeds(uint32 _basefeeScalar, uint32 _blobbasefeeScalar) external { - bytes32 encoded = - ffi.encodeScalarEcotone({ _basefeeScalar: _basefeeScalar, _blobbasefeeScalar: _blobbasefeeScalar }); + uint256 encoded = (uint256(0x01) << 248) | (uint256(_blobbasefeeScalar) << 32) | _basefeeScalar; vm.expectEmit(address(systemConfig)); emit ConfigUpdate(0, ISystemConfig.UpdateType.FEE_SCALARS, abi.encode(systemConfig.overhead(), encoded)); @@ -402,11 +344,10 @@ contract SystemConfig_SetGasConfigEcotone_Test is SystemConfig_TestInit { systemConfig.setGasConfigEcotone({ _basefeeScalar: _basefeeScalar, _blobbasefeeScalar: _blobbasefeeScalar }); assertEq(systemConfig.basefeeScalar(), _basefeeScalar); assertEq(systemConfig.blobbasefeeScalar(), _blobbasefeeScalar); - assertEq(systemConfig.scalar(), uint256(encoded)); + assertEq(systemConfig.scalar(), encoded); - (uint32 basefeeScalar, uint32 blobbbasefeeScalar) = ffi.decodeScalarEcotone(encoded); - assertEq(uint256(basefeeScalar), uint256(_basefeeScalar)); - assertEq(uint256(blobbbasefeeScalar), uint256(_blobbasefeeScalar)); + assertEq(uint32(encoded), _basefeeScalar); + assertEq(uint32(encoded >> 32), _blobbasefeeScalar); } } @@ -579,9 +520,8 @@ contract SystemConfig_SetResourceConfig_Test is SystemConfig_TestInit { ) internal { - // Wipe out the initialized slot so the proxy can be initialized again - vm.store(address(systemConfig), bytes32(0), bytes32(0)); - // Fetch the current gas limit + _resetInitialized(); + uint64 gasLimit = systemConfig.gasLimit(); vm.prank(address(systemConfig.proxyAdmin())); @@ -595,14 +535,7 @@ contract SystemConfig_SetResourceConfig_Test is SystemConfig_TestInit { _unsafeBlockSigner: address(0), _config: config, _batchInbox: address(0), - _addresses: ISystemConfig.Addresses({ - l1CrossDomainMessenger: address(0), - l1ERC721Bridge: address(0), - l1StandardBridge: address(0), - optimismPortal: address(0), - optimismMintableERC20Factory: address(0), - delayedWETH: address(0) - }), + _addresses: _emptyAddresses(), _l2ChainId: 1234, _superchainConfig: ISuperchainConfig(address(0)) }); @@ -700,6 +633,15 @@ contract SystemConfig_Paused_Test is SystemConfig_TestInit { contract SystemConfig_SetFeature_Test is SystemConfig_TestInit { event FeatureSet(bytes32 indexed feature, bool indexed enabled); + function _enableEthLockboxIfDisabled(address proxyAdmin) internal { + if (!systemConfig.isFeatureEnabled(Features.ETH_LOCKBOX)) { + vm.prank(proxyAdmin); + systemConfig.setFeature(Features.ETH_LOCKBOX, true); + } + + assertTrue(systemConfig.isFeatureEnabled(Features.ETH_LOCKBOX)); + } + /// @notice Tests that `setFeature` reverts if the caller is not ProxyAdmin or ProxyAdmin owner. /// @param _sender The address to test. function testFuzz_setFeature_notProxyAdminOrProxyAdminOwner_reverts(address _sender) external { @@ -789,7 +731,7 @@ contract SystemConfig_SetFeature_Test is SystemConfig_TestInit { function test_setFeature_alreadyDisabled_reverts() external { vm.prank(address(systemConfig.proxyAdmin())); vm.expectRevert(ISystemConfig.SystemConfig_InvalidFeatureState.selector); - systemConfig.setFeature("EXAMPLE FEATURE", false); + systemConfig.setFeature(EXAMPLE_FEATURE, false); } /// @notice Tests that disabling ETH_LOCKBOX reverts if the OptimismPortal has a non-zero @@ -797,12 +739,7 @@ contract SystemConfig_SetFeature_Test is SystemConfig_TestInit { function test_setFeature_ethLockboxDisableWhileConfigured_reverts() external { address proxyAdmin = address(systemConfig.proxyAdmin()); - // Ensure ETH_LOCKBOX is enabled first (no pause active in fresh setup). - if (!systemConfig.isFeatureEnabled(Features.ETH_LOCKBOX)) { - vm.prank(proxyAdmin); - systemConfig.setFeature(Features.ETH_LOCKBOX, true); - assertTrue(systemConfig.isFeatureEnabled(Features.ETH_LOCKBOX)); - } + _enableEthLockboxIfDisabled(proxyAdmin); // Force the portal to have a configured ETHLockbox address. StorageSlot memory slot = ForgeArtifacts.getSlot("OptimismPortal2", "ethLockbox"); @@ -818,12 +755,7 @@ contract SystemConfig_SetFeature_Test is SystemConfig_TestInit { function test_setFeature_ethLockboxEnableWhilePaused_reverts() external { address proxyAdmin = address(systemConfig.proxyAdmin()); - // Ensure ETH_LOCKBOX is enabled first (no pause active in fresh setup). - if (!systemConfig.isFeatureEnabled(Features.ETH_LOCKBOX)) { - vm.prank(proxyAdmin); - systemConfig.setFeature(Features.ETH_LOCKBOX, true); - assertTrue(systemConfig.isFeatureEnabled(Features.ETH_LOCKBOX)); - } + _enableEthLockboxIfDisabled(proxyAdmin); // Pause globally. vm.prank(superchainConfig.guardian()); @@ -839,12 +771,7 @@ contract SystemConfig_SetFeature_Test is SystemConfig_TestInit { function test_setFeature_ethLockboxDisableWhilePaused_reverts() external { address proxyAdmin = address(systemConfig.proxyAdmin()); - // Ensure ETH_LOCKBOX is enabled first. - if (!systemConfig.isFeatureEnabled(Features.ETH_LOCKBOX)) { - vm.prank(proxyAdmin); - systemConfig.setFeature(Features.ETH_LOCKBOX, true); - assertTrue(systemConfig.isFeatureEnabled(Features.ETH_LOCKBOX)); - } + _enableEthLockboxIfDisabled(proxyAdmin); // Pause globally. vm.prank(superchainConfig.guardian()); @@ -897,9 +824,8 @@ contract SystemConfig_Guardian_Test is SystemConfig_TestInit { } } -/// @title SystemConfig_Uncategorized_Test -/// @notice General tests that are not testing any function directly of the `SystemConfig` contract -/// are testing multiple functions at once. +/// @title SystemConfig_SuperchainConfig_Test +/// @notice Test contract for SystemConfig `superchainConfig` function. contract SystemConfig_SuperchainConfig_Test is SystemConfig_TestInit { /// @notice Tests that `superchainConfig()` returns the correct address. function test_superchainConfig_succeeds() external view { diff --git a/test/L1/proofs/AggregateVerifier.t.sol b/test/L1/proofs/AggregateVerifier.t.sol index 5b0de174..7f631f30 100644 --- a/test/L1/proofs/AggregateVerifier.t.sol +++ b/test/L1/proofs/AggregateVerifier.t.sol @@ -12,68 +12,32 @@ import { Claim, Timestamp } from "src/libraries/bridge/LibUDT.sol"; import { AggregateVerifier } from "src/L1/proofs/AggregateVerifier.sol"; import { IVerifier } from "interfaces/L1/proofs/IVerifier.sol"; +import { LibClone } from "lib/solady/src/utils/LibClone.sol"; + import { BaseTest } from "./BaseTest.t.sol"; contract AggregateVerifierTest is BaseTest { - function testInitializeWithTEEProof() public { - currentL2BlockNumber += BLOCK_INTERVAL; - Claim rootClaim = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber))); - bytes memory proof = _generateProof("tee-proof", AggregateVerifier.ProofType.TEE); + using LibClone for address; - AggregateVerifier game = _createAggregateVerifierGame( - TEE_PROVER, rootClaim, currentL2BlockNumber, address(anchorStateRegistry), proof - ); + AggregateVerifier private aggregateVerifierImpl; - assertEq(game.wasRespectedGameTypeWhenCreated(), true); - assertEq(address(game.teeProver()), TEE_PROVER); - assertEq(address(game.zkProver()), address(0)); - assertEq(uint8(game.status()), uint8(GameStatus.IN_PROGRESS)); - assertEq(game.l2SequenceNumber(), currentL2BlockNumber); - assertEq(game.rootClaim().raw(), rootClaim.raw()); - assertEq(game.parentAddress(), address(anchorStateRegistry)); - assertEq(game.gameType().raw(), AGGREGATE_VERIFIER_GAME_TYPE.raw()); - assertEq(game.gameCreator(), TEE_PROVER); - assertEq( - game.extraData(), - abi.encodePacked(currentL2BlockNumber, address(anchorStateRegistry), game.intermediateOutputRoots()) - ); - assertEq(game.bondRecipient(), TEE_PROVER); - assertEq(anchorStateRegistry.isGameProper(IDisputeGame(address(game))), true); - assertEq(delayedWETH.balanceOf(address(game)), INIT_BOND); - assertEq(game.proofCount(), 1); + function setUp() public override { + super.setUp(); + aggregateVerifierImpl = AggregateVerifier(address(factory.gameImpls(AGGREGATE_VERIFIER_GAME_TYPE))); } - function testInitializeWithZKProof() public { - currentL2BlockNumber += BLOCK_INTERVAL; - Claim rootClaim = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber))); - bytes memory proof = _generateProof("zk-proof", AggregateVerifier.ProofType.ZK); - - AggregateVerifier game = _createAggregateVerifierGame( - ZK_PROVER, rootClaim, currentL2BlockNumber, address(anchorStateRegistry), proof + function testInitializeWithTEEProof() public { + _createAndAssertInitializedGame( + "tee-proof", AggregateVerifier.ProofType.TEE, TEE_PROVER, TEE_PROVER, address(0) ); + } - assertEq(game.wasRespectedGameTypeWhenCreated(), true); - assertEq(address(game.teeProver()), address(0)); - assertEq(address(game.zkProver()), ZK_PROVER); - assertEq(uint8(game.status()), uint8(GameStatus.IN_PROGRESS)); - assertEq(game.l2SequenceNumber(), currentL2BlockNumber); - assertEq(game.rootClaim().raw(), rootClaim.raw()); - assertEq(game.parentAddress(), address(anchorStateRegistry)); - assertEq(game.gameType().raw(), AGGREGATE_VERIFIER_GAME_TYPE.raw()); - assertEq(game.gameCreator(), ZK_PROVER); - assertEq( - game.extraData(), - abi.encodePacked(currentL2BlockNumber, address(anchorStateRegistry), game.intermediateOutputRoots()) - ); - assertEq(game.bondRecipient(), ZK_PROVER); - assertEq(anchorStateRegistry.isGameProper(IDisputeGame(address(game))), true); - assertEq(delayedWETH.balanceOf(address(game)), INIT_BOND); - assertEq(game.proofCount(), 1); + function testInitializeWithZKProof() public { + _createAndAssertInitializedGame("zk-proof", AggregateVerifier.ProofType.ZK, ZK_PROVER, address(0), ZK_PROVER); } function testInitializeFailsIfInvalidCallDataSize() public { - currentL2BlockNumber += BLOCK_INTERVAL; - Claim rootClaim = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber))); + Claim rootClaim = _advanceL2BlockAndClaim(); vm.deal(TEE_PROVER, INIT_BOND); bytes memory extraData = ""; @@ -85,72 +49,48 @@ contract AggregateVerifierTest is BaseTest { } function testUpdatingAnchorStateRegistryWithTEEProof() public { - currentL2BlockNumber += BLOCK_INTERVAL; - Claim rootClaim = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber))); + Claim rootClaim = _advanceL2BlockAndClaim(); bytes memory proof = _generateProof("tee-proof", AggregateVerifier.ProofType.TEE); AggregateVerifier game = _createAggregateVerifierGame( TEE_PROVER, rootClaim, currentL2BlockNumber, address(anchorStateRegistry), proof ); - // Cannot claim bond before game is over vm.expectRevert(GameNotResolved.selector); game.claimCredit(); - // Resolve after 7 days - vm.warp(block.timestamp + 7 days); + vm.warp(block.timestamp + aggregateVerifierImpl.SLOW_FINALIZATION_DELAY()); game.resolve(); - assertEq(uint8(game.status()), uint8(GameStatus.DEFENDER_WINS)); + _assertStatus(game, GameStatus.DEFENDER_WINS); - // Unlock and reclaim bond after resolving - uint256 balanceBefore = game.gameCreator().balance; - game.claimCredit(); - vm.warp(block.timestamp + DELAYED_WETH_DELAY); - game.claimCredit(); - assertEq(game.gameCreator().balance, balanceBefore + INIT_BOND); - assertEq(delayedWETH.balanceOf(address(game)), 0); + _claimCreditAfterDelay(game); - // Update AnchorStateRegistry vm.warp(block.timestamp + 1); game.closeGame(); - (Hash root, uint256 l2SequenceNumber) = anchorStateRegistry.getAnchorRoot(); - assertEq(root.raw(), rootClaim.raw()); - assertEq(l2SequenceNumber, currentL2BlockNumber); + _assertAnchorRoot(rootClaim); } function testUpdatingAnchorStateRegistryWithZKProof() public { - currentL2BlockNumber += BLOCK_INTERVAL; - Claim rootClaim = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber))); + Claim rootClaim = _advanceL2BlockAndClaim(); bytes memory proof = _generateProof("zk-proof", AggregateVerifier.ProofType.ZK); AggregateVerifier game = _createAggregateVerifierGame( ZK_PROVER, rootClaim, currentL2BlockNumber, address(anchorStateRegistry), proof ); - // Resolve after 7 days - vm.warp(block.timestamp + 7 days); + vm.warp(block.timestamp + aggregateVerifierImpl.SLOW_FINALIZATION_DELAY()); game.resolve(); - assertEq(uint8(game.status()), uint8(GameStatus.DEFENDER_WINS)); + _assertStatus(game, GameStatus.DEFENDER_WINS); - // Unlock and reclaim bond after delay - uint256 balanceBefore = game.gameCreator().balance; - game.claimCredit(); - vm.warp(block.timestamp + DELAYED_WETH_DELAY); - game.claimCredit(); - assertEq(game.gameCreator().balance, balanceBefore + INIT_BOND); - assertEq(delayedWETH.balanceOf(address(game)), 0); + _claimCreditAfterDelay(game); - // Update AnchorStateRegistry vm.warp(block.timestamp + 1); game.closeGame(); - (Hash root, uint256 l2SequenceNumber) = anchorStateRegistry.getAnchorRoot(); - assertEq(root.raw(), rootClaim.raw()); - assertEq(l2SequenceNumber, currentL2BlockNumber); + _assertAnchorRoot(rootClaim); } function testUpdatingAnchorStateRegistryWithBothProofs() public { - currentL2BlockNumber += BLOCK_INTERVAL; - Claim rootClaim = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber))); + Claim rootClaim = _advanceL2BlockAndClaim(); bytes memory teeProof = _generateProof("tee-proof", AggregateVerifier.ProofType.TEE); bytes memory zkProof = _generateProof("zk-proof", AggregateVerifier.ProofType.ZK); @@ -161,61 +101,44 @@ contract AggregateVerifierTest is BaseTest { _provideProof(game, ZK_PROVER, zkProof); assertEq(game.proofCount(), 2); - // Resolve after 1 day (FAST_FINALIZATION_DELAY with 2 proofs) - vm.warp(block.timestamp + 1 days); + vm.warp(block.timestamp + aggregateVerifierImpl.FAST_FINALIZATION_DELAY()); game.resolve(); - assertEq(uint8(game.status()), uint8(GameStatus.DEFENDER_WINS)); + _assertStatus(game, GameStatus.DEFENDER_WINS); - // Update AnchorStateRegistry vm.warp(block.timestamp + 1); game.closeGame(); - (Hash root, uint256 l2SequenceNumber) = anchorStateRegistry.getAnchorRoot(); - assertEq(root.raw(), rootClaim.raw()); - assertEq(l2SequenceNumber, currentL2BlockNumber); + _assertAnchorRoot(rootClaim); - // Unlock and reclaim bond after delay - uint256 balanceBefore = game.gameCreator().balance; - game.claimCredit(); - vm.warp(block.timestamp + DELAYED_WETH_DELAY); - game.claimCredit(); - assertEq(game.gameCreator().balance, balanceBefore + INIT_BOND); - assertEq(delayedWETH.balanceOf(address(game)), 0); + _claimCreditAfterDelay(game); } function testProofCannotIncreaseExpectedResolution() public { - currentL2BlockNumber += BLOCK_INTERVAL; - Claim rootClaim = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber))); + Claim rootClaim = _advanceL2BlockAndClaim(); bytes memory teeProof = _generateProof("tee-proof", AggregateVerifier.ProofType.TEE); bytes memory zkProof = _generateProof("zk-proof", AggregateVerifier.ProofType.ZK); + uint256 slowDelay = aggregateVerifierImpl.SLOW_FINALIZATION_DELAY(); AggregateVerifier game = _createAggregateVerifierGame( TEE_PROVER, rootClaim, currentL2BlockNumber, address(anchorStateRegistry), teeProof ); Timestamp originalExpectedResolution = game.expectedResolution(); - assertEq(originalExpectedResolution.raw(), block.timestamp + 7 days); + assertEq(originalExpectedResolution.raw(), block.timestamp + slowDelay); - vm.warp(block.timestamp + 7 days - 1); - // Cannot resolve yet + vm.warp(block.timestamp + slowDelay - 1); vm.expectRevert(AggregateVerifier.GameNotOver.selector); game.resolve(); - // Provide ZK proof _provideProof(game, ZK_PROVER, zkProof); + assertEq(game.expectedResolution().raw(), originalExpectedResolution.raw()); - // Proof should not have increased expected resolution - Timestamp expectedResolution = game.expectedResolution(); - assertEq(expectedResolution.raw(), originalExpectedResolution.raw()); - - // Resolve after 1 second vm.warp(block.timestamp + 1); game.resolve(); - assertEq(uint8(game.status()), uint8(GameStatus.DEFENDER_WINS)); + _assertStatus(game, GameStatus.DEFENDER_WINS); } function testCannotCreateSameProposal() public { - currentL2BlockNumber += BLOCK_INTERVAL; - Claim rootClaim = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber))); + Claim rootClaim = _advanceL2BlockAndClaim(); bytes memory teeProof = _generateProof("tee-proof", AggregateVerifier.ProofType.TEE); bytes memory zkProof = _generateProof("zk-proof", AggregateVerifier.ProofType.ZK); @@ -223,12 +146,8 @@ contract AggregateVerifierTest is BaseTest { TEE_PROVER, rootClaim, currentL2BlockNumber, address(anchorStateRegistry), teeProof ); - Hash uuid = factory.getGameUUID( - AGGREGATE_VERIFIER_GAME_TYPE, - rootClaim, - abi.encodePacked(currentL2BlockNumber, address(anchorStateRegistry), game.intermediateOutputRoots()) - ); - vm.expectRevert(abi.encodeWithSelector(IDisputeGameFactory.GameAlreadyExists.selector, uuid)); + Hash gameId = factory.getGameUUID(AGGREGATE_VERIFIER_GAME_TYPE, rootClaim, game.extraData()); + vm.expectRevert(abi.encodeWithSelector(IDisputeGameFactory.GameAlreadyExists.selector, gameId)); _createAggregateVerifierGame(ZK_PROVER, rootClaim, currentL2BlockNumber, address(anchorStateRegistry), zkProof); } @@ -262,72 +181,60 @@ contract AggregateVerifierTest is BaseTest { } function testVerifyFailsWithL1OriginInFuture() public { - currentL2BlockNumber += BLOCK_INTERVAL; - // Use a future block number + Claim rootClaim = _advanceL2BlockAndClaim(); uint256 l1OriginNumber = block.number + 1; - bytes32 l1OriginHash = bytes32(uint256(1)); // Fake hash - Claim rootClaim = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber))); + bytes32 l1OriginHash = bytes32(uint256(1)); - bytes memory proofBytes = - abi.encodePacked(uint8(AggregateVerifier.ProofType.TEE), l1OriginHash, l1OriginNumber, rootClaim.raw()); + bytes memory proofBytes = _teeProof(l1OriginHash, l1OriginNumber, rootClaim); - vm.expectRevert( + _expectCreateGameRevertsForTeeProof( + rootClaim, + proofBytes, abi.encodeWithSelector(AggregateVerifier.L1OriginInFuture.selector, l1OriginNumber, block.number) ); - _createAggregateVerifierGame( - TEE_PROVER, rootClaim, currentL2BlockNumber, address(anchorStateRegistry), proofBytes - ); } function testVerifyFailsWithL1OriginTooOld() public { - currentL2BlockNumber += BLOCK_INTERVAL; + Claim rootClaim = _advanceL2BlockAndClaim(); - // Roll forward many blocks to make old blocks unavailable vm.roll(block.number + 300); - // Use a block number that's too old (outside both blockhash window and EIP-2935 window) uint256 l1OriginNumber = 1; - bytes32 l1OriginHash = bytes32(uint256(1)); // Fake hash - Claim rootClaim = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber))); + bytes32 l1OriginHash = bytes32(uint256(1)); - bytes memory proofBytes = - abi.encodePacked(uint8(AggregateVerifier.ProofType.TEE), l1OriginHash, l1OriginNumber, rootClaim.raw()); + bytes memory proofBytes = _teeProof(l1OriginHash, l1OriginNumber, rootClaim); - vm.expectRevert(abi.encodeWithSelector(AggregateVerifier.L1OriginTooOld.selector, l1OriginNumber, block.number)); - _createAggregateVerifierGame( - TEE_PROVER, rootClaim, currentL2BlockNumber, address(anchorStateRegistry), proofBytes + _expectCreateGameRevertsForTeeProof( + rootClaim, + proofBytes, + abi.encodeWithSelector(AggregateVerifier.L1OriginTooOld.selector, l1OriginNumber, block.number) ); } function testVerifyFailsWithL1OriginHashMismatch() public { - currentL2BlockNumber += BLOCK_INTERVAL; + Claim rootClaim = _advanceL2BlockAndClaim(); uint256 l1OriginNumber = block.number - 1; - bytes32 wrongHash = bytes32(uint256(0xdeadbeef)); // Wrong hash - Claim rootClaim = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber))); + bytes32 wrongHash = bytes32(uint256(0xdeadbeef)); - bytes memory proofBytes = - abi.encodePacked(uint8(AggregateVerifier.ProofType.TEE), wrongHash, l1OriginNumber, rootClaim.raw()); + bytes memory proofBytes = _teeProof(wrongHash, l1OriginNumber, rootClaim); bytes32 actualHash = blockhash(l1OriginNumber); - vm.expectRevert(abi.encodeWithSelector(AggregateVerifier.L1OriginHashMismatch.selector, wrongHash, actualHash)); - _createAggregateVerifierGame( - TEE_PROVER, rootClaim, currentL2BlockNumber, address(anchorStateRegistry), proofBytes + _expectCreateGameRevertsForTeeProof( + rootClaim, + proofBytes, + abi.encodeWithSelector(AggregateVerifier.L1OriginHashMismatch.selector, wrongHash, actualHash) ); } function testVerifyWithBlockhashWindow() public { - currentL2BlockNumber += BLOCK_INTERVAL; + Claim rootClaim = _advanceL2BlockAndClaim(); - // Test verification within the 256 block window vm.roll(block.number + 100); - // Use a block that's within the 256 block window uint256 l1OriginNumber = block.number - 50; bytes32 l1OriginHash = blockhash(l1OriginNumber); - Claim rootClaim = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber))); - bytes memory proofBytes = - abi.encodePacked(uint8(AggregateVerifier.ProofType.TEE), l1OriginHash, l1OriginNumber, rootClaim.raw()); + bytes memory proofBytes = _teeProof(l1OriginHash, l1OriginNumber, rootClaim); _createAggregateVerifierGame( TEE_PROVER, rootClaim, currentL2BlockNumber, address(anchorStateRegistry), proofBytes @@ -335,25 +242,16 @@ contract AggregateVerifierTest is BaseTest { } function testVerifyWithEIP2935Window() public { - currentL2BlockNumber += BLOCK_INTERVAL; + Claim rootClaim = _advanceL2BlockAndClaim(); - // Roll forward past the 256 blockhash window vm.roll(block.number + 300); - // Use a block that's outside blockhash window but within EIP-2935 window - uint256 l1OriginNumber = block.number - 260; // 260 > 256, so blockhash() returns 0 + uint256 l1OriginNumber = block.number - 260; bytes32 expectedHash = keccak256(abi.encodePacked("mock-blockhash", l1OriginNumber)); - Claim rootClaim = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber))); - // Mock the EIP-2935 contract response - vm.mockCall( - 0x0000F90827F1C53a10cb7A02335B175320002935, // EIP-2935 contract address - abi.encode(l1OriginNumber), // raw 32-byte calldata - abi.encode(expectedHash) // returns the blockhash - ); + vm.mockCall(aggregateVerifierImpl.EIP2935_CONTRACT(), abi.encode(l1OriginNumber), abi.encode(expectedHash)); - bytes memory proofBytes = - abi.encodePacked(uint8(AggregateVerifier.ProofType.TEE), expectedHash, l1OriginNumber, rootClaim.raw()); + bytes memory proofBytes = _teeProof(expectedHash, l1OriginNumber, rootClaim); _createAggregateVerifierGame( TEE_PROVER, rootClaim, currentL2BlockNumber, address(anchorStateRegistry), proofBytes @@ -361,43 +259,154 @@ contract AggregateVerifierTest is BaseTest { } function testDeployWithInvalidBlockIntervals() public { - // Case 1: BLOCK_INTERVAL is 0 - vm.expectRevert( - abi.encodeWithSelector(AggregateVerifier.InvalidBlockInterval.selector, 0, INTERMEDIATE_BLOCK_INTERVAL) + _expectDeployWithInvalidBlockIntervalsReverts(0, INTERMEDIATE_BLOCK_INTERVAL); + _expectDeployWithInvalidBlockIntervalsReverts(BLOCK_INTERVAL, 0); + _expectDeployWithInvalidBlockIntervalsReverts(3, 2); + } + + function _advanceL2BlockAndClaim() private returns (Claim rootClaim) { + currentL2BlockNumber += BLOCK_INTERVAL; + return Claim.wrap(keccak256(abi.encode(currentL2BlockNumber))); + } + + function _createAndAssertInitializedGame( + bytes memory proofSalt, + AggregateVerifier.ProofType proofType, + address prover, + address expectedTeeProver, + address expectedZkProver + ) + private + { + Claim rootClaim = _advanceL2BlockAndClaim(); + bytes memory proof = _generateProof(proofSalt, proofType); + + AggregateVerifier game = _createAggregateVerifierGame( + prover, rootClaim, currentL2BlockNumber, address(anchorStateRegistry), proof ); - new AggregateVerifier( - AGGREGATE_VERIFIER_GAME_TYPE, - IAnchorStateRegistry(address(anchorStateRegistry)), - IDelayedWETH(payable(address(delayedWETH))), - IVerifier(address(teeVerifier)), - IVerifier(address(zkVerifier)), - TEE_IMAGE_HASH, - AggregateVerifier.ZkHashes(ZK_RANGE_HASH, ZK_AGGREGATE_HASH), - CONFIG_HASH, - L2_CHAIN_ID, - 0, - INTERMEDIATE_BLOCK_INTERVAL + + _assertInitializedGame(game, rootClaim, prover, expectedTeeProver, expectedZkProver); + } + + function _assertInitializedGame( + AggregateVerifier game, + Claim rootClaim, + address expectedCreator, + address expectedTeeProver, + address expectedZkProver + ) + private + view + { + assertTrue(game.wasRespectedGameTypeWhenCreated()); + assertEq(game.teeProver(), expectedTeeProver); + assertEq(game.zkProver(), expectedZkProver); + _assertStatus(game, GameStatus.IN_PROGRESS); + assertEq(game.l2SequenceNumber(), currentL2BlockNumber); + assertEq(game.rootClaim().raw(), rootClaim.raw()); + assertEq(game.parentAddress(), address(anchorStateRegistry)); + assertEq(game.gameType().raw(), AGGREGATE_VERIFIER_GAME_TYPE.raw()); + assertEq(game.gameCreator(), expectedCreator); + bytes memory intermediateOutputRoots = game.intermediateOutputRoots(); + assertEq( + game.extraData(), + abi.encodePacked(currentL2BlockNumber, address(anchorStateRegistry), intermediateOutputRoots) ); + assertEq(game.bondRecipient(), expectedCreator); + assertTrue(anchorStateRegistry.isGameProper(IDisputeGame(address(game)))); + assertEq(delayedWETH.balanceOf(address(game)), INIT_BOND); + assertEq(game.proofCount(), 1); + } - // Case 2: INTERMEDIATE_BLOCK_INTERVAL is 0 - vm.expectRevert(abi.encodeWithSelector(AggregateVerifier.InvalidBlockInterval.selector, BLOCK_INTERVAL, 0)); - new AggregateVerifier( - AGGREGATE_VERIFIER_GAME_TYPE, - IAnchorStateRegistry(address(anchorStateRegistry)), - IDelayedWETH(payable(address(delayedWETH))), - IVerifier(address(teeVerifier)), - IVerifier(address(zkVerifier)), - TEE_IMAGE_HASH, - AggregateVerifier.ZkHashes(ZK_RANGE_HASH, ZK_AGGREGATE_HASH), - CONFIG_HASH, - L2_CHAIN_ID, - BLOCK_INTERVAL, - 0 + function _assertStatus(AggregateVerifier game, GameStatus expectedStatus) private view { + assertEq(uint8(game.status()), uint8(expectedStatus)); + } + + function _assertAnchorRoot(Claim rootClaim) private view { + (Hash root, uint256 l2SequenceNumber) = anchorStateRegistry.getAnchorRoot(); + assertEq(root.raw(), rootClaim.raw()); + assertEq(l2SequenceNumber, currentL2BlockNumber); + } + + function _claimCreditAfterDelay(AggregateVerifier game) private { + uint256 balanceBefore = game.gameCreator().balance; + game.claimCredit(); + vm.warp(block.timestamp + DELAYED_WETH_DELAY); + game.claimCredit(); + assertEq(game.gameCreator().balance, balanceBefore + INIT_BOND); + assertEq(delayedWETH.balanceOf(address(game)), 0); + } + + function _expectCreateGameRevertsForTeeProof( + Claim rootClaim, + bytes memory proofBytes, + bytes memory revertData + ) + private + { + vm.expectRevert(revertData); + _createAggregateVerifierGame( + TEE_PROVER, rootClaim, currentL2BlockNumber, address(anchorStateRegistry), proofBytes ); + } + + function _teeProof( + bytes32 l1OriginHash, + uint256 l1OriginNumber, + Claim rootClaim + ) + private + pure + returns (bytes memory) + { + return abi.encodePacked(uint8(AggregateVerifier.ProofType.TEE), l1OriginHash, l1OriginNumber, rootClaim.raw()); + } + + function _expectDeployWithInvalidBlockIntervalsReverts( + uint256 blockInterval, + uint256 intermediateBlockInterval + ) + private + { + vm.expectRevert( + abi.encodeWithSelector( + AggregateVerifier.InvalidBlockInterval.selector, blockInterval, intermediateBlockInterval + ) + ); + _deployAggregateVerifierWithIntervals(blockInterval, intermediateBlockInterval); + } + + /// @notice Clones the implementation like the factory, but skips `_finalizeGameCreation`. + function _deployAggregateVerifierCloneWithoutFactoryRegistration( + address creator, + Claim rootClaim, + uint256 l2BlockNumber, + address parentAddress, + bytes memory proof + ) + private + returns (AggregateVerifier) + { + IDisputeGame impl = factory.gameImpls(AGGREGATE_VERIFIER_GAME_TYPE); + bytes memory extraData = _aggregateVerifierExtraData(rootClaim, l2BlockNumber, parentAddress); + bytes32 l1Head = blockhash(block.number - 1); + address clone = address(impl).clone(abi.encodePacked(creator, rootClaim, l1Head, extraData)); + + vm.deal(creator, INIT_BOND); + vm.prank(creator); + AggregateVerifier(payable(clone)).initializeWithInitData{ value: INIT_BOND }(proof); + + return AggregateVerifier(payable(clone)); + } - // Case 3: BLOCK_INTERVAL is not divisible by INTERMEDIATE_BLOCK_INTERVAL - vm.expectRevert(abi.encodeWithSelector(AggregateVerifier.InvalidBlockInterval.selector, 3, 2)); - new AggregateVerifier( + function _deployAggregateVerifierWithIntervals( + uint256 blockInterval, + uint256 intermediateBlockInterval + ) + private + returns (AggregateVerifier) + { + return new AggregateVerifier( AGGREGATE_VERIFIER_GAME_TYPE, IAnchorStateRegistry(address(anchorStateRegistry)), IDelayedWETH(payable(address(delayedWETH))), @@ -407,8 +416,8 @@ contract AggregateVerifierTest is BaseTest { AggregateVerifier.ZkHashes(ZK_RANGE_HASH, ZK_AGGREGATE_HASH), CONFIG_HASH, L2_CHAIN_ID, - 3, - 2 + blockInterval, + intermediateBlockInterval ); } } diff --git a/test/L1/proofs/AnchorStateRegistry.t.sol b/test/L1/proofs/AnchorStateRegistry.t.sol index 604f7521..0f794f96 100644 --- a/test/L1/proofs/AnchorStateRegistry.t.sol +++ b/test/L1/proofs/AnchorStateRegistry.t.sol @@ -22,6 +22,10 @@ import { AggregateVerifier } from "src/L1/proofs/AggregateVerifier.sol"; /// @title AnchorStateRegistry_TestInit /// @notice Reusable test initialization for `AnchorStateRegistry` tests. abstract contract AnchorStateRegistry_TestInit is BaseTest { + string internal constant ANCHOR_STATE_REGISTRY_NAME = "AnchorStateRegistry"; + bytes32 internal constant DUMMY_STARTING_ANCHOR_ROOT = + 0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF; + IDisputeGameFactory disputeGameFactory; ISuperchainConfig superchainConfig; IDisputeGame gameProxy; @@ -58,11 +62,118 @@ abstract contract AnchorStateRegistry_TestInit is BaseTest { ); } - function skipIfForkTest(string memory) public pure { } - function _setPaused(bool _paused) internal { vm.mockCall(address(systemConfig), abi.encodeCall(systemConfig.paused, ()), abi.encode(_paused)); } + + function _mockGameStatus(GameStatus _status) internal { + vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.status, ()), abi.encode(_status)); + } + + function _mockGameResolvedAt(uint256 _timestamp) internal { + vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.resolvedAt, ()), abi.encode(_timestamp)); + } + + function _mockGamePastFinalityWithStatus(GameStatus _status) internal { + _mockGameResolvedAt(block.timestamp); + _warpByFinalityDelay(); + vm.warp(block.timestamp + 1); + _mockGameStatus(_status); + } + + function _warpByFinalityDelay() internal returns (uint256 finalityDelay_) { + finalityDelay_ = anchorStateRegistry.disputeGameFinalityDelaySeconds(); + vm.warp(block.timestamp + finalityDelay_); + } + + function _mockGameWasRespected(bool _wasRespected) internal { + vm.mockCall( + address(gameProxy), abi.encodeCall(gameProxy.wasRespectedGameTypeWhenCreated, ()), abi.encode(_wasRespected) + ); + } + + function _mockGameNotRegistered() internal { + vm.mockCall( + address(disputeGameFactory), + abi.encodeCall( + disputeGameFactory.games, (gameProxy.gameType(), gameProxy.rootClaim(), gameProxy.extraData()) + ), + abi.encode(address(0), 0) + ); + } + + function _initializeWithDummyStartingAnchorRoot() internal { + anchorStateRegistry.initialize( + systemConfig, + disputeGameFactory, + Proposal({ root: Hash.wrap(DUMMY_STARTING_ANCHOR_ROOT), l2SequenceNumber: 0 }), + GameType.wrap(0) + ); + } + + function _assumeNotGuardian(address _caller) internal view { + vm.assume(_caller != superchainConfig.guardian()); + } + + function _expectUnauthorizedGuardianRevert(address _caller) internal { + _assumeNotGuardian(_caller); + vm.prank(_caller); + vm.expectRevert(IAnchorStateRegistry.AnchorStateRegistry_Unauthorized.selector); + } + + function _updateRetirementTimestampAsGuardian() internal { + vm.prank(superchainConfig.guardian()); + anchorStateRegistry.updateRetirementTimestamp(); + } + + function _blacklistDisputeGameAsGuardian(IDisputeGame _game) internal { + vm.prank(superchainConfig.guardian()); + anchorStateRegistry.blacklistDisputeGame(_game); + } + + function _mockGameCreatedAt(uint64 _createdAtTimestamp) internal { + vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.createdAt, ()), abi.encode(_createdAtTimestamp)); + } + + function _mockRetiredGame(uint64 _createdAtTimestamp) internal returns (uint64) { + _updateRetirementTimestampAsGuardian(); + uint64 createdAtTimestamp = uint64(bound(_createdAtTimestamp, 0, anchorStateRegistry.retirementTimestamp())); + _mockGameCreatedAt(createdAtTimestamp); + return createdAtTimestamp; + } + + function _mockGameL2SequenceNumber(uint256 _l2BlockNumber) internal { + vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.l2SequenceNumber, ()), abi.encode(_l2BlockNumber)); + } + + function _expectSetAnchorStateInvalid(IDisputeGame _game, address _caller) internal { + vm.prank(_caller); + vm.expectRevert(IAnchorStateRegistry.AnchorStateRegistry_InvalidAnchorGame.selector); + anchorStateRegistry.setAnchorState(_game); + } + + function _setAnchorStateToGameProxy() internal { + _mockGamePastFinalityWithStatus(GameStatus.DEFENDER_WINS); + anchorStateRegistry.setAnchorState(gameProxy); + } + + function _assertAnchorRootEqGame(IDisputeGame _game) internal view { + (Hash root, uint256 l2BlockNumber) = anchorStateRegistry.getAnchorRoot(); + assertEq(root.raw(), _game.rootClaim().raw()); + assertEq(l2BlockNumber, _game.l2SequenceNumber()); + } + + function _assertCurrentAnchorRootEq(Hash _root, uint256 _l2BlockNumber) internal view { + (Hash updatedRoot, uint256 updatedL2BlockNumber) = anchorStateRegistry.getAnchorRoot(); + assertEq(updatedL2BlockNumber, _l2BlockNumber); + assertEq(updatedRoot.raw(), _root.raw()); + } + + function _assertAnchorEq(GameType _gameType, Hash _root, uint256 _l2BlockNumber) internal view { + (Hash updatedRoot, uint256 updatedL2BlockNumber) = anchorStateRegistry.anchors(_gameType); + assertEq(updatedL2BlockNumber, _l2BlockNumber); + assertEq(updatedRoot.raw(), _root.raw()); + } } /// @title AnchorStateRegistry_Version_Test @@ -77,50 +188,49 @@ contract AnchorStateRegistry_Version_Test is AnchorStateRegistry_TestInit { /// @title AnchorStateRegistry_Initialize_Test /// @notice Tests the `initialize` function of the `AnchorStateRegistry` contract. contract AnchorStateRegistry_Initialize_Test is AnchorStateRegistry_TestInit { + StorageSlot internal initializedSlot; + StorageSlot internal retirementTimestampSlot; + + function setUp() public override { + super.setUp(); + + initializedSlot = ForgeArtifacts.getSlot(ANCHOR_STATE_REGISTRY_NAME, "_initialized"); + retirementTimestampSlot = ForgeArtifacts.getSlot(ANCHOR_STATE_REGISTRY_NAME, "retirementTimestamp"); + } + /// @notice Tests that initialization is successful. function test_initialize_succeeds() public view { - skipIfForkTest("State has changed since initialization on a forked network."); - // Verify starting anchor root. (Hash root, uint256 l2BlockNumber) = anchorStateRegistry.getAnchorRoot(); assertEq(root.raw(), keccak256(abi.encode(uint256(0)))); assertEq(l2BlockNumber, 0); // Verify contract addresses. - assert(anchorStateRegistry.systemConfig() == systemConfig); - assert(anchorStateRegistry.disputeGameFactory() == disputeGameFactory); - assert(anchorStateRegistry.superchainConfig() == superchainConfig); + assertEq(address(anchorStateRegistry.systemConfig()), address(systemConfig)); + assertEq(address(anchorStateRegistry.disputeGameFactory()), address(disputeGameFactory)); + assertEq(address(anchorStateRegistry.superchainConfig()), address(superchainConfig)); } /// @notice Tests that the initializer value is correct. Trivial test for normal /// initialization but confirms that the initValue is not incremented incorrectly if /// an upgrade function is not present. function test_initialize_correctInitializerValue_succeeds() public view { - // Get the slot for _initialized. - StorageSlot memory slot = ForgeArtifacts.getSlot("AnchorStateRegistry", "_initialized"); - - // Get the initializer value. - bytes32 slotVal = vm.load(address(anchorStateRegistry), bytes32(slot.slot)); + bytes32 slotVal = vm.load(address(anchorStateRegistry), bytes32(initializedSlot.slot)); uint8 val = uint8(uint256(slotVal) & 0xFF); - // Assert that the initializer value matches the expected value. assertEq(val, anchorStateRegistry.initVersion()); } /// @notice Tests that the retirement timestamp is set on the first initialization. function test_initialize_setsRetirementTimestamp_succeeds() public { - skipIfForkTest("State has changed since initialization on a forked network."); - (Hash root, uint256 l2SequenceNumber) = anchorStateRegistry.getAnchorRoot(); GameType startingGameType = anchorStateRegistry.respectedGameType(); - StorageSlot memory initSlot = ForgeArtifacts.getSlot("AnchorStateRegistry", "_initialized"); - StorageSlot memory retirementSlot = ForgeArtifacts.getSlot("AnchorStateRegistry", "retirementTimestamp"); address proxyAdminOwner = anchorStateRegistry.proxyAdminOwner(); // Reset initialization and retirement timestamp state. - vm.store(address(anchorStateRegistry), bytes32(initSlot.slot), bytes32(0)); - vm.store(address(anchorStateRegistry), bytes32(retirementSlot.slot), bytes32(0)); + vm.store(address(anchorStateRegistry), bytes32(initializedSlot.slot), bytes32(0)); + vm.store(address(anchorStateRegistry), bytes32(retirementTimestampSlot.slot), bytes32(0)); uint256 newTimestamp = block.timestamp + 100; vm.warp(newTimestamp); @@ -139,24 +249,20 @@ contract AnchorStateRegistry_Initialize_Test is AnchorStateRegistry_TestInit { /// @notice Tests that the retirement timestamp is unchanged during re-initialization. function test_initialize_reinitializationDoesNotChangeRetirementTimestamp_succeeds() public { - skipIfForkTest("State has changed since initialization on a forked network."); - (Hash root, uint256 l2SequenceNumber) = anchorStateRegistry.getAnchorRoot(); GameType startingGameType = anchorStateRegistry.respectedGameType(); - StorageSlot memory initSlot = ForgeArtifacts.getSlot("AnchorStateRegistry", "_initialized"); address proxyAdminOwner = anchorStateRegistry.proxyAdminOwner(); uint256 initialTimestamp = block.timestamp + 200; vm.warp(initialTimestamp); - vm.prank(superchainConfig.guardian()); - anchorStateRegistry.updateRetirementTimestamp(); + _updateRetirementTimestampAsGuardian(); uint64 originalTimestamp = anchorStateRegistry.retirementTimestamp(); uint256 reinitTimestamp = block.timestamp + 200; vm.warp(reinitTimestamp); - vm.store(address(anchorStateRegistry), bytes32(initSlot.slot), bytes32(0)); + vm.store(address(anchorStateRegistry), bytes32(initializedSlot.slot), bytes32(0)); vm.prank(proxyAdminOwner); anchorStateRegistry.initialize( @@ -172,14 +278,7 @@ contract AnchorStateRegistry_Initialize_Test is AnchorStateRegistry_TestInit { /// @notice Tests that initialization cannot be done twice function test_initialize_twice_reverts() public { vm.expectRevert("Initializable: contract is already initialized"); - anchorStateRegistry.initialize( - systemConfig, - disputeGameFactory, - Proposal({ - root: Hash.wrap(0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF), l2SequenceNumber: 0 - }), - GameType.wrap(0) - ); + _initializeWithDummyStartingAnchorRoot(); } /// @notice Tests that initialization reverts if called by a non-proxy admin or owner. @@ -190,25 +289,15 @@ contract AnchorStateRegistry_Initialize_Test is AnchorStateRegistry_TestInit { _sender != address(anchorStateRegistry.proxyAdmin()) && _sender != anchorStateRegistry.proxyAdminOwner() ); - // Get the slot for _initialized. - StorageSlot memory slot = ForgeArtifacts.getSlot("AnchorStateRegistry", "_initialized"); - // Set the initialized slot to 0. - vm.store(address(anchorStateRegistry), bytes32(slot.slot), bytes32(0)); + vm.store(address(anchorStateRegistry), bytes32(initializedSlot.slot), bytes32(0)); // Expect the revert with `ProxyAdminOwnedBase_NotProxyAdminOrProxyAdminOwner` selector. vm.expectRevert(IProxyAdminOwnedBase.ProxyAdminOwnedBase_NotProxyAdminOrProxyAdminOwner.selector); // Call the `initialize` function with the sender vm.prank(_sender); - anchorStateRegistry.initialize( - systemConfig, - disputeGameFactory, - Proposal({ - root: Hash.wrap(0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF), l2SequenceNumber: 0 - }), - GameType.wrap(0) - ); + _initializeWithDummyStartingAnchorRoot(); } } @@ -251,12 +340,7 @@ contract AnchorStateRegistry_SetRespectedGameType_Test is AnchorStateRegistry_Te /// @param _gameType The game type to attempt to set /// @param _caller The address attempting to call the function function testFuzz_setRespectedGameType_notGuardian_reverts(GameType _gameType, address _caller) public { - // Ensure caller is not the guardian - vm.assume(_caller != superchainConfig.guardian()); - - // Attempt to call as non-guardian - vm.prank(_caller); - vm.expectRevert(IAnchorStateRegistry.AnchorStateRegistry_Unauthorized.selector); + _expectUnauthorizedGuardianRevert(_caller); anchorStateRegistry.setRespectedGameType(_gameType); } } @@ -266,11 +350,9 @@ contract AnchorStateRegistry_SetRespectedGameType_Test is AnchorStateRegistry_Te contract AnchorStateRegistry_UpdateRetirementTimestamp_Test is AnchorStateRegistry_TestInit { /// @notice Tests that updateRetirementTimestamp succeeds when called by the guardian function test_updateRetirementTimestamp_succeeds() public { - // Call as guardian - vm.prank(superchainConfig.guardian()); vm.expectEmit(address(anchorStateRegistry)); emit RetirementTimestampSet(block.timestamp); - anchorStateRegistry.updateRetirementTimestamp(); + _updateRetirementTimestampAsGuardian(); // Verify the timestamp was set assertEq(anchorStateRegistry.retirementTimestamp(), block.timestamp); @@ -278,32 +360,25 @@ contract AnchorStateRegistry_UpdateRetirementTimestamp_Test is AnchorStateRegist /// @notice Tests that updateRetirementTimestamp can be called multiple times by the guardian function test_updateRetirementTimestamp_multipleUpdates_succeeds() public { - // First update - vm.prank(superchainConfig.guardian()); - anchorStateRegistry.updateRetirementTimestamp(); + _updateRetirementTimestampAsGuardian(); uint64 firstTimestamp = anchorStateRegistry.retirementTimestamp(); // Warp forward and update again vm.warp(block.timestamp + 1000); - vm.prank(superchainConfig.guardian()); vm.expectEmit(address(anchorStateRegistry)); emit RetirementTimestampSet(block.timestamp); - anchorStateRegistry.updateRetirementTimestamp(); + _updateRetirementTimestampAsGuardian(); // Verify the timestamp was updated - assertEq(anchorStateRegistry.retirementTimestamp(), block.timestamp); - assertGt(anchorStateRegistry.retirementTimestamp(), firstTimestamp); + uint64 secondTimestamp = anchorStateRegistry.retirementTimestamp(); + assertEq(secondTimestamp, block.timestamp); + assertGt(secondTimestamp, firstTimestamp); } /// @notice Tests that updateRetirementTimestamp reverts when not called by the guardian /// @param _caller The address attempting to call the function function testFuzz_updateRetirementTimestamp_notGuardian_reverts(address _caller) public { - // Ensure caller is not the guardian - vm.assume(_caller != superchainConfig.guardian()); - - // Attempt to call as non-guardian - vm.prank(_caller); - vm.expectRevert(IAnchorStateRegistry.AnchorStateRegistry_Unauthorized.selector); + _expectUnauthorizedGuardianRevert(_caller); anchorStateRegistry.updateRetirementTimestamp(); } } @@ -313,11 +388,9 @@ contract AnchorStateRegistry_UpdateRetirementTimestamp_Test is AnchorStateRegist contract AnchorStateRegistry_BlacklistDisputeGame_Test is AnchorStateRegistry_TestInit { /// @notice Tests that blacklistDisputeGame succeeds when called by the guardian function test_blacklistDisputeGame_succeeds() public { - // Call as guardian - vm.prank(superchainConfig.guardian()); vm.expectEmit(address(anchorStateRegistry)); emit DisputeGameBlacklisted(gameProxy); - anchorStateRegistry.blacklistDisputeGame(gameProxy); + _blacklistDisputeGameAsGuardian(gameProxy); // Verify the game was blacklisted assertTrue(anchorStateRegistry.disputeGameBlacklist(gameProxy)); @@ -326,7 +399,7 @@ contract AnchorStateRegistry_BlacklistDisputeGame_Test is AnchorStateRegistry_Te /// @notice Tests that multiple games can be blacklisted function test_blacklistDisputeGame_multipleGames_succeeds() public { // Create a second game proxy - IDisputeGame secondGame = IDisputeGame(address(0x123)); + IDisputeGame secondGame = IDisputeGame(makeAddr("second-game")); // Blacklist both games vm.startPrank(superchainConfig.guardian()); @@ -342,12 +415,7 @@ contract AnchorStateRegistry_BlacklistDisputeGame_Test is AnchorStateRegistry_Te /// @notice Tests that blacklistDisputeGame reverts when not called by the guardian /// @param _caller The address attempting to call the function function testFuzz_blacklistDisputeGame_notGuardian_reverts(address _caller) public { - // Ensure caller is not the guardian - vm.assume(_caller != superchainConfig.guardian()); - - // Attempt to call as non-guardian - vm.prank(_caller); - vm.expectRevert(IAnchorStateRegistry.AnchorStateRegistry_Unauthorized.selector); + _expectUnauthorizedGuardianRevert(_caller); anchorStateRegistry.blacklistDisputeGame(gameProxy); } @@ -393,10 +461,7 @@ contract AnchorStateRegistry_GetAnchorRoot_Test is AnchorStateRegistry_TestInit /// @notice Tests that getAnchorRoot will return the value of the starting anchor root when no /// anchor game exists yet. function test_getAnchorRoot_noAnchorGame_succeeds() public view { - skipIfForkTest("On a forked network, there would most likely be an anchor game already."); - - // Assert that we nave no anchor game yet. - assert(address(anchorStateRegistry.anchorGame()) == address(0)); + assertEq(address(anchorStateRegistry.anchorGame()), address(0)); // We should get the starting anchor root back. (Hash root, uint256 l2BlockNumber) = anchorStateRegistry.getAnchorRoot(); @@ -406,64 +471,29 @@ contract AnchorStateRegistry_GetAnchorRoot_Test is AnchorStateRegistry_TestInit /// @notice Tests that getAnchorRoot will return the correct anchor root if an anchor game exists. function test_getAnchorRoot_anchorGameExists_succeeds() public { - // Mock the game to be resolved. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.resolvedAt, ()), abi.encode(block.timestamp)); - vm.warp(block.timestamp + anchorStateRegistry.disputeGameFinalityDelaySeconds() + 1); - - // Mock the game to be the defender wins. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.status, ()), abi.encode(GameStatus.DEFENDER_WINS)); - - // Set the anchor game to the game proxy. - anchorStateRegistry.setAnchorState(gameProxy); - - // We should get the anchor root back. - (Hash root, uint256 l2BlockNumber) = anchorStateRegistry.getAnchorRoot(); - assertEq(root.raw(), gameProxy.rootClaim().raw()); - assertEq(l2BlockNumber, gameProxy.l2SequenceNumber()); + _setAnchorStateToGameProxy(); + _assertAnchorRootEqGame(gameProxy); } /// @notice Tests that getAnchorRoot will return the latest anchor root even if the superchain /// is paused. function test_getAnchorRoot_superchainPaused_succeeds() public { - // Mock the game to be resolved. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.resolvedAt, ()), abi.encode(block.timestamp)); - vm.warp(block.timestamp + anchorStateRegistry.disputeGameFinalityDelaySeconds() + 1); - - // Mock the game to be the defender wins. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.status, ()), abi.encode(GameStatus.DEFENDER_WINS)); - - // Set the anchor game to the game proxy. - anchorStateRegistry.setAnchorState(gameProxy); + _setAnchorStateToGameProxy(); // Pause the superchain. _setPaused(true); - // We should get the anchor root back. - (Hash root, uint256 l2BlockNumber) = anchorStateRegistry.getAnchorRoot(); - assertEq(root.raw(), gameProxy.rootClaim().raw()); - assertEq(l2BlockNumber, gameProxy.l2SequenceNumber()); + _assertAnchorRootEqGame(gameProxy); } /// @notice Tests that getAnchorRoot returns even if the anchor game is blacklisted. function test_getAnchorRoot_blacklistedGame_succeeds() public { - // Mock the game to be resolved. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.resolvedAt, ()), abi.encode(block.timestamp)); - vm.warp(block.timestamp + anchorStateRegistry.disputeGameFinalityDelaySeconds() + 1); - - // Mock the game to be the defender wins. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.status, ()), abi.encode(GameStatus.DEFENDER_WINS)); - - // Set the anchor game to the game proxy. - anchorStateRegistry.setAnchorState(gameProxy); + _setAnchorStateToGameProxy(); // Blacklist the game. - vm.prank(superchainConfig.guardian()); - anchorStateRegistry.blacklistDisputeGame(gameProxy); + _blacklistDisputeGameAsGuardian(gameProxy); - // Get the anchor root. - (Hash root, uint256 l2BlockNumber) = anchorStateRegistry.getAnchorRoot(); - assertEq(root.raw(), gameProxy.rootClaim().raw()); - assertEq(l2BlockNumber, gameProxy.l2SequenceNumber()); + _assertAnchorRootEqGame(gameProxy); } } @@ -473,44 +503,32 @@ contract AnchorStateRegistry_GetStartingAnchorRoot_Test is AnchorStateRegistry_T /// @notice Tests that getStartingAnchorRoot remains unchanged even if the current anchor root /// changes. function test_getStartingAnchorRoot_afterUpdate_succeeds() public { - // Mock the game to be resolved. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.resolvedAt, ()), abi.encode(block.timestamp)); - vm.warp(block.timestamp + anchorStateRegistry.disputeGameFinalityDelaySeconds() + 1); - - // Mock the game to be the defender wins. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.status, ()), abi.encode(GameStatus.DEFENDER_WINS)); + _mockGamePastFinalityWithStatus(GameStatus.DEFENDER_WINS); + uint256 expectedL2BlockNumber = validL2BlockNumber + 1; + Claim expectedRoot = Claim.wrap(keccak256(abi.encode(123))); + Proposal memory startingAnchorRootBeforeUpdate = anchorStateRegistry.getStartingAnchorRoot(); // Mock the game's L2 block number to be greater than the starting anchor root block number. - vm.mockCall( - address(gameProxy), abi.encodeCall(gameProxy.l2SequenceNumber, ()), abi.encode(validL2BlockNumber + 1) - ); + _mockGameL2SequenceNumber(expectedL2BlockNumber); // Mock the game's anchor root to be different from the starting anchor root. - vm.mockCall( - address(gameProxy), - abi.encodeCall(gameProxy.rootClaim, ()), - abi.encode(Claim.wrap(keccak256(abi.encode(123)))) - ); + vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.rootClaim, ()), abi.encode(expectedRoot)); // Set the anchor game to the game proxy. anchorStateRegistry.setAnchorState(gameProxy); - // Grab the value of the starting anchor root before the update. - Proposal memory startingAnchorRootBeforeUpdate = anchorStateRegistry.getStartingAnchorRoot(); - // Verify the CURRENT anchor root has changed. (Hash currentRoot, uint256 currentL2BlockNumber) = anchorStateRegistry.getAnchorRoot(); - assertEq(currentRoot.raw(), gameProxy.rootClaim().raw()); - assertEq(currentL2BlockNumber, gameProxy.l2SequenceNumber()); + assertEq(currentRoot.raw(), expectedRoot.raw()); + assertEq(currentL2BlockNumber, expectedL2BlockNumber); // Verify the STARTING anchor root has NOT changed. Proposal memory startingAnchorRootAfterUpdate = anchorStateRegistry.getStartingAnchorRoot(); assertEq(startingAnchorRootAfterUpdate.root.raw(), startingAnchorRootBeforeUpdate.root.raw()); assertEq(startingAnchorRootAfterUpdate.l2SequenceNumber, startingAnchorRootBeforeUpdate.l2SequenceNumber); - // Explicitly assert they are different (assuming the new game has different values). - assertFalse(currentRoot.raw() == startingAnchorRootAfterUpdate.root.raw()); - assertFalse(currentL2BlockNumber == startingAnchorRootAfterUpdate.l2SequenceNumber); + assertNotEq(currentRoot.raw(), startingAnchorRootAfterUpdate.root.raw()); + assertNotEq(currentL2BlockNumber, startingAnchorRootAfterUpdate.l2SequenceNumber); } } @@ -524,14 +542,7 @@ contract AnchorStateRegistry_IsGameRegistered_Test is AnchorStateRegistry_TestIn /// @notice Tests that isGameRegistered will return false if the game is not registered. function test_isGameRegistered_isNotFactoryRegistered_succeeds() public { - // Mock the DisputeGameFactory to make it seem that the game was not registered. - vm.mockCall( - address(disputeGameFactory), - abi.encodeCall( - disputeGameFactory.games, (gameProxy.gameType(), gameProxy.rootClaim(), gameProxy.extraData()) - ), - abi.encode(address(0), 0) - ); + _mockGameNotRegistered(); // Game should not be registered. assertFalse(anchorStateRegistry.isGameRegistered(gameProxy)); @@ -560,18 +571,14 @@ contract AnchorStateRegistry_IsGameRespected_Test is AnchorStateRegistry_TestIni /// @notice Tests that isGameRespected will return true if the game is of the respected game /// type. function test_isGameRespected_isRespected_succeeds() public { - // Mock that the game was respected. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.wasRespectedGameTypeWhenCreated, ()), abi.encode(true)); + _mockGameWasRespected(true); assertTrue(anchorStateRegistry.isGameRespected(gameProxy)); } /// @notice Tests that isGameRespected will return false if the game is not of the respected /// game type. function test_isGameRespected_isNotRespected_succeeds() public { - // Mock that the game was not respected. - vm.mockCall( - address(gameProxy), abi.encodeCall(gameProxy.wasRespectedGameTypeWhenCreated, ()), abi.encode(false) - ); + _mockGameWasRespected(false); assertFalse(anchorStateRegistry.isGameRespected(gameProxy)); } } @@ -582,21 +589,14 @@ contract AnchorStateRegistry_IsGameBlacklisted_Test is AnchorStateRegistry_TestI /// @notice Tests that isGameBlacklisted will return true if the game is blacklisted. function test_isGameBlacklisted_isActuallyBlacklisted_succeeds() public { // Blacklist the game. - vm.prank(superchainConfig.guardian()); - anchorStateRegistry.blacklistDisputeGame(gameProxy); + _blacklistDisputeGameAsGuardian(gameProxy); // Should return true. assertTrue(anchorStateRegistry.isGameBlacklisted(gameProxy)); } /// @notice Tests that isGameBlacklisted will return false if the game is not blacklisted. - function test_isGameBlacklisted_isNotBlacklisted_succeeds() public { - // Mock the disputeGameBlacklist call to return false. - vm.mockCall( - address(anchorStateRegistry), - abi.encodeCall(anchorStateRegistry.disputeGameBlacklist, (gameProxy)), - abi.encode(false) - ); + function test_isGameBlacklisted_isNotBlacklisted_succeeds() public view { assertFalse(anchorStateRegistry.isGameBlacklisted(gameProxy)); } } @@ -607,15 +607,7 @@ contract AnchorStateRegistry_IsGameRetired_Test is AnchorStateRegistry_TestInit /// @notice Tests that isGameRetired will return true if the game is retired. /// @param _createdAtTimestamp The createdAt timestamp to use for the test. function testFuzz_isGameRetired_isRetired_succeeds(uint64 _createdAtTimestamp) public { - // Set the retirement timestamp to now. - vm.prank(superchainConfig.guardian()); - anchorStateRegistry.updateRetirementTimestamp(); - - // Make sure createdAt timestamp is less than or equal to the retirementTimestamp. - _createdAtTimestamp = uint64(bound(_createdAtTimestamp, 0, anchorStateRegistry.retirementTimestamp())); - - // Mock the createdAt call. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.createdAt, ()), abi.encode(_createdAtTimestamp)); + _mockRetiredGame(_createdAtTimestamp); // Game should be retired. assertTrue(anchorStateRegistry.isGameRetired(gameProxy)); @@ -625,15 +617,14 @@ contract AnchorStateRegistry_IsGameRetired_Test is AnchorStateRegistry_TestInit /// @param _createdAtTimestamp The createdAt timestamp to use for the test. function testFuzz_isGameRetired_isNotRetired_succeeds(uint64 _createdAtTimestamp) public { // Set the retirement timestamp to now. - vm.prank(superchainConfig.guardian()); - anchorStateRegistry.updateRetirementTimestamp(); + _updateRetirementTimestampAsGuardian(); // Make sure createdAt timestamp is greater than the retirementTimestamp. _createdAtTimestamp = uint64(bound(_createdAtTimestamp, anchorStateRegistry.retirementTimestamp() + 1, type(uint64).max)); // Mock the call to createdAt. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.createdAt, ()), abi.encode(_createdAtTimestamp)); + _mockGameCreatedAt(_createdAtTimestamp); // Game should not be retired. assertFalse(anchorStateRegistry.isGameRetired(gameProxy)); @@ -643,17 +634,16 @@ contract AnchorStateRegistry_IsGameRetired_Test is AnchorStateRegistry_TestInit /// @title AnchorStateRegistry_IsGameResolved_Test /// @notice Tests the `isGameResolved` function of the `AnchorStateRegistry` contract. contract AnchorStateRegistry_IsGameResolved_Test is AnchorStateRegistry_TestInit { + function _mockResolvedGame(uint256 _resolvedAtTimestamp, GameStatus _status) internal { + _resolvedAtTimestamp = bound(_resolvedAtTimestamp, 1, block.timestamp); + _mockGameResolvedAt(_resolvedAtTimestamp); + _mockGameStatus(_status); + } + /// @notice Tests that isGameResolved will return true if the game is resolved. /// @param _resolvedAtTimestamp The resolvedAt timestamp to use for the test. function testFuzz_isGameResolved_challengerWins_succeeds(uint256 _resolvedAtTimestamp) public { - // Bound resolvedAt to be less than or equal to current timestamp. - _resolvedAtTimestamp = bound(_resolvedAtTimestamp, 1, block.timestamp); - - // Mock the resolvedAt timestamp. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.resolvedAt, ()), abi.encode(_resolvedAtTimestamp)); - - // Mock the status to be CHALLENGER_WINS. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.status, ()), abi.encode(GameStatus.CHALLENGER_WINS)); + _mockResolvedGame(_resolvedAtTimestamp, GameStatus.CHALLENGER_WINS); // Game should be resolved. assertTrue(anchorStateRegistry.isGameResolved(gameProxy)); @@ -662,14 +652,7 @@ contract AnchorStateRegistry_IsGameResolved_Test is AnchorStateRegistry_TestInit /// @notice Tests that isGameResolved will return true if the game is resolved. /// @param _resolvedAtTimestamp The resolvedAt timestamp to use for the test. function testFuzz_isGameResolved_defenderWins_succeeds(uint256 _resolvedAtTimestamp) public { - // Bound resolvedAt to be less than or equal to current timestamp. - _resolvedAtTimestamp = bound(_resolvedAtTimestamp, 1, block.timestamp); - - // Mock the resolvedAt timestamp. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.resolvedAt, ()), abi.encode(_resolvedAtTimestamp)); - - // Mock the status to be DEFENDER_WINS. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.status, ()), abi.encode(GameStatus.DEFENDER_WINS)); + _mockResolvedGame(_resolvedAtTimestamp, GameStatus.DEFENDER_WINS); // Game should be resolved. assertTrue(anchorStateRegistry.isGameResolved(gameProxy)); @@ -679,14 +662,7 @@ contract AnchorStateRegistry_IsGameResolved_Test is AnchorStateRegistry_TestInit /// resolved. /// @param _resolvedAtTimestamp The resolvedAt timestamp to use for the test. function testFuzz_isGameResolved_inProgressNotResolved_succeeds(uint256 _resolvedAtTimestamp) public { - // Bound resolvedAt to be less than or equal to current timestamp. - _resolvedAtTimestamp = bound(_resolvedAtTimestamp, 1, block.timestamp); - - // Mock the resolvedAt timestamp. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.resolvedAt, ()), abi.encode(_resolvedAtTimestamp)); - - // Mock the status to be IN_PROGRESS. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.status, ()), abi.encode(GameStatus.IN_PROGRESS)); + _mockResolvedGame(_resolvedAtTimestamp, GameStatus.IN_PROGRESS); // Game should not be resolved. assertFalse(anchorStateRegistry.isGameResolved(gameProxy)); @@ -704,40 +680,22 @@ contract AnchorStateRegistry_IsGameProper_Test is AnchorStateRegistry_TestInit { /// @notice Tests that isGameProper will return false if the game is not registered. function test_isGameProper_isNotFactoryRegistered_succeeds() public { - // Mock the DisputeGameFactory to make it seem that the game was not registered. - vm.mockCall( - address(disputeGameFactory), - abi.encodeCall( - disputeGameFactory.games, (gameProxy.gameType(), gameProxy.rootClaim(), gameProxy.extraData()) - ), - abi.encode(address(0), 0) - ); + _mockGameNotRegistered(); assertFalse(anchorStateRegistry.isGameProper(gameProxy)); } - /// @notice Tests that isGameProper will return false if the game is not the respected game + /// @notice Tests that isGameProper still returns true if the game was not the respected game /// type. - /// @param _gameType The game type to use for the test. - function testFuzz_isGameProper_anyGameType_succeeds(GameType _gameType) public { - if (_gameType.raw() == gameProxy.gameType().raw()) { - _gameType = GameType.wrap(_gameType.raw() + 1); - } - - // Mock that the game was not respected. - vm.mockCall( - address(gameProxy), abi.encodeCall(gameProxy.wasRespectedGameTypeWhenCreated, ()), abi.encode(false) - ); - - // Still a proper game. + function test_isGameProper_notRespected_succeeds() public { + _mockGameWasRespected(false); assertTrue(anchorStateRegistry.isGameProper(gameProxy)); } /// @notice Tests that isGameProper will return false if the game is blacklisted. function test_isGameProper_isBlacklisted_succeeds() public { // Blacklist the game. - vm.prank(superchainConfig.guardian()); - anchorStateRegistry.blacklistDisputeGame(gameProxy); + _blacklistDisputeGameAsGuardian(gameProxy); // Should return false. assertFalse(anchorStateRegistry.isGameProper(gameProxy)); @@ -755,15 +713,7 @@ contract AnchorStateRegistry_IsGameProper_Test is AnchorStateRegistry_TestInit { /// @notice Tests that isGameProper will return false if the game is retired. /// @param _createdAtTimestamp The createdAt timestamp to use for the test. function testFuzz_isGameProper_isRetired_succeeds(uint64 _createdAtTimestamp) public { - // Set the retirement timestamp to now. - vm.prank(superchainConfig.guardian()); - anchorStateRegistry.updateRetirementTimestamp(); - - // Make sure createdAt timestamp is less than or equal to the retirementTimestamp. - _createdAtTimestamp = uint64(bound(_createdAtTimestamp, 0, anchorStateRegistry.retirementTimestamp())); - - // Mock the call to createdAt. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.createdAt, ()), abi.encode(_createdAtTimestamp)); + _mockRetiredGame(_createdAtTimestamp); // Game should not be proper. assertFalse(anchorStateRegistry.isGameProper(gameProxy)); @@ -776,19 +726,14 @@ contract AnchorStateRegistry_IsGameFinalized_Test is AnchorStateRegistry_TestIni /// @notice Tests that isGameFinalized will return true if the game is finalized. /// @param _resolvedAtTimestamp The resolvedAt timestamp to use for the test. function testFuzz_isGameFinalized_isFinalized_succeeds(uint256 _resolvedAtTimestamp) public { - // Warp forward by disputeGameFinalityDelaySeconds. - vm.warp(block.timestamp + anchorStateRegistry.disputeGameFinalityDelaySeconds()); + uint256 finalityDelay = _warpByFinalityDelay(); // Bound resolvedAt to be at least disputeGameFinalityDelaySeconds in the past. // Must be greater than 0. - _resolvedAtTimestamp = - bound(_resolvedAtTimestamp, 1, block.timestamp - anchorStateRegistry.disputeGameFinalityDelaySeconds() - 1); + _resolvedAtTimestamp = bound(_resolvedAtTimestamp, 1, block.timestamp - finalityDelay - 1); - // Mock the resolvedAt timestamp. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.resolvedAt, ()), abi.encode(_resolvedAtTimestamp)); - - // Mock the status to be DEFENDER_WINS. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.status, ()), abi.encode(GameStatus.DEFENDER_WINS)); + _mockGameResolvedAt(_resolvedAtTimestamp); + _mockGameStatus(GameStatus.DEFENDER_WINS); // Game should be finalized. assertTrue(anchorStateRegistry.isGameFinalized(gameProxy)); @@ -796,19 +741,13 @@ contract AnchorStateRegistry_IsGameFinalized_Test is AnchorStateRegistry_TestIni /// @notice Tests that isGameFinalized will return false if the game is not finalized. /// @param _resolvedAtTimestamp The resolvedAt timestamp to use for the test. - function testFuzz_isGameFinalized_isNotAirgapped_succeeds(uint256 _resolvedAtTimestamp) public { - // Warp forward by disputeGameFinalityDelaySeconds. - vm.warp(block.timestamp + anchorStateRegistry.disputeGameFinalityDelaySeconds()); + function testFuzz_isGameFinalized_isNotFinalized_succeeds(uint256 _resolvedAtTimestamp) public { + uint256 finalityDelay = _warpByFinalityDelay(); // Bound resolvedAt to be less than disputeGameFinalityDelaySeconds in the past. - _resolvedAtTimestamp = bound( - _resolvedAtTimestamp, - block.timestamp - anchorStateRegistry.disputeGameFinalityDelaySeconds(), - block.timestamp - ); + _resolvedAtTimestamp = bound(_resolvedAtTimestamp, block.timestamp - finalityDelay, block.timestamp); - // Mock the resolvedAt timestamp. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.resolvedAt, ()), abi.encode(_resolvedAtTimestamp)); + _mockGameResolvedAt(_resolvedAtTimestamp); // Game should not be finalized. assertFalse(anchorStateRegistry.isGameFinalized(gameProxy)); @@ -816,11 +755,9 @@ contract AnchorStateRegistry_IsGameFinalized_Test is AnchorStateRegistry_TestIni /// @notice Tests that isGameFinalized will return false if the game is not resolved. function test_isGameFinalized_isNotResolved_succeeds() public { - // Warp forward by disputeGameFinalityDelaySeconds. - vm.warp(block.timestamp + anchorStateRegistry.disputeGameFinalityDelaySeconds()); + _warpByFinalityDelay(); - // Mock the status call to be IN_PROGRESS. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.status, ()), abi.encode(GameStatus.IN_PROGRESS)); + _mockGameStatus(GameStatus.IN_PROGRESS); // Game should not be finalized. assertFalse(anchorStateRegistry.isGameFinalized(gameProxy)); @@ -833,65 +770,38 @@ contract AnchorStateRegistry_IsGameClaimValid_Test is AnchorStateRegistry_TestIn /// @notice Tests that isGameClaimValid will return true if the game claim is valid. /// @param _resolvedAtTimestamp The resolvedAt timestamp to use for the test. function testFuzz_isGameClaimValid_claimIsValid_succeeds(uint256 _resolvedAtTimestamp) public { - // Warp forward by disputeGameFinalityDelaySeconds. - vm.warp(block.timestamp + anchorStateRegistry.disputeGameFinalityDelaySeconds()); + uint256 finalityDelay = _warpByFinalityDelay(); // Bound resolvedAt to be at least disputeGameFinalityDelaySeconds in the past. - _resolvedAtTimestamp = - bound(_resolvedAtTimestamp, 1, block.timestamp - anchorStateRegistry.disputeGameFinalityDelaySeconds() - 1); - - // Mock that the game was respected. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.wasRespectedGameTypeWhenCreated, ()), abi.encode(true)); + _resolvedAtTimestamp = bound(_resolvedAtTimestamp, 1, block.timestamp - finalityDelay - 1); - // Mock the resolvedAt timestamp. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.resolvedAt, ()), abi.encode(_resolvedAtTimestamp)); - - // Mock the status to be DEFENDER_WINS. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.status, ()), abi.encode(GameStatus.DEFENDER_WINS)); + _mockGameWasRespected(true); + _mockGameResolvedAt(_resolvedAtTimestamp); + _mockGameStatus(GameStatus.DEFENDER_WINS); // Claim should be valid. assertTrue(anchorStateRegistry.isGameClaimValid(gameProxy)); } /// @notice Tests that isGameClaimValid will return false if the game is not registered. - function testFuzz_isGameClaimValid_notRegistered_succeeds() public { - // Mock the DisputeGameFactory to make it seem that the game was not registered. - vm.mockCall( - address(disputeGameFactory), - abi.encodeCall( - disputeGameFactory.games, (gameProxy.gameType(), gameProxy.rootClaim(), gameProxy.extraData()) - ), - abi.encode(address(0), 0) - ); + function test_isGameClaimValid_notRegistered_succeeds() public { + _mockGameNotRegistered(); // Claim should not be valid. assertFalse(anchorStateRegistry.isGameClaimValid(gameProxy)); } /// @notice Tests that isGameClaimValid will return false if the game is not respected. - /// @param _gameType The game type to use for the test. - function testFuzz_isGameClaimValid_isNotRespected_succeeds(GameType _gameType) public { - if (_gameType.raw() == gameProxy.gameType().raw()) { - _gameType = GameType.wrap(_gameType.raw() + 1); - } - - // Mock that the game was not respected. - vm.mockCall( - address(gameProxy), abi.encodeCall(gameProxy.wasRespectedGameTypeWhenCreated, ()), abi.encode(false) - ); + function test_isGameClaimValid_isNotRespected_succeeds() public { + _mockGameWasRespected(false); // Claim should not be valid. assertFalse(anchorStateRegistry.isGameClaimValid(gameProxy)); } /// @notice Tests that isGameClaimValid will return false if the game is blacklisted. - function testFuzz_isGameClaimValid_isBlacklisted_succeeds() public { - // Mock the disputeGameBlacklist call to return true. - vm.mockCall( - address(anchorStateRegistry), - abi.encodeCall(anchorStateRegistry.disputeGameBlacklist, (gameProxy)), - abi.encode(true) - ); + function test_isGameClaimValid_isBlacklisted_succeeds() public { + _blacklistDisputeGameAsGuardian(gameProxy); // Claim should not be valid. assertFalse(anchorStateRegistry.isGameClaimValid(gameProxy)); @@ -899,45 +809,30 @@ contract AnchorStateRegistry_IsGameClaimValid_Test is AnchorStateRegistry_TestIn /// @notice Tests that isGameClaimValid will return false if the game is retired. /// @param _createdAtTimestamp The createdAt timestamp to use for the test. - function testFuzz_isGameClaimValid_isRetired_succeeds(uint256 _createdAtTimestamp) public { - // Set the retirement timestamp to now. - vm.prank(superchainConfig.guardian()); - anchorStateRegistry.updateRetirementTimestamp(); - - // Make sure createdAt timestamp is less than or equal to the retirementTimestamp. - _createdAtTimestamp = uint64(bound(_createdAtTimestamp, 0, anchorStateRegistry.retirementTimestamp())); - - // Mock the call to createdAt. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.createdAt, ()), abi.encode(_createdAtTimestamp)); + function testFuzz_isGameClaimValid_isRetired_succeeds(uint64 _createdAtTimestamp) public { + _mockRetiredGame(_createdAtTimestamp); // Claim should not be valid. assertFalse(anchorStateRegistry.isGameClaimValid(gameProxy)); } /// @notice Tests that isGameClaimValid will return false if the game is not resolved. - function testFuzz_isGameClaimValid_notResolved_succeeds() public { - // Mock the status to be IN_PROGRESS. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.status, ()), abi.encode(GameStatus.IN_PROGRESS)); + function test_isGameClaimValid_notResolved_succeeds() public { + _mockGameStatus(GameStatus.IN_PROGRESS); // Claim should not be valid. assertFalse(anchorStateRegistry.isGameClaimValid(gameProxy)); } - /// @notice Tests that isGameClaimValid will return false if the game is not airgapped. + /// @notice Tests that isGameClaimValid will return false if the game is not finalized. /// @param _resolvedAtTimestamp The resolvedAt timestamp to use for the test. - function testFuzz_isGameClaimValid_notAirgapped_succeeds(uint256 _resolvedAtTimestamp) public { - // Warp forward by disputeGameFinalityDelaySeconds. - vm.warp(block.timestamp + anchorStateRegistry.disputeGameFinalityDelaySeconds()); + function testFuzz_isGameClaimValid_notFinalized_succeeds(uint256 _resolvedAtTimestamp) public { + uint256 finalityDelay = _warpByFinalityDelay(); // Bound resolvedAt to be less than disputeGameFinalityDelaySeconds in the past. - _resolvedAtTimestamp = bound( - _resolvedAtTimestamp, - block.timestamp - anchorStateRegistry.disputeGameFinalityDelaySeconds(), - block.timestamp - ); + _resolvedAtTimestamp = bound(_resolvedAtTimestamp, block.timestamp - finalityDelay, block.timestamp); - // Mock the resolvedAt timestamp. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.resolvedAt, ()), abi.encode(_resolvedAtTimestamp)); + _mockGameResolvedAt(_resolvedAtTimestamp); // Claim should not be valid. assertFalse(anchorStateRegistry.isGameClaimValid(gameProxy)); @@ -961,24 +856,14 @@ contract AnchorStateRegistry_SetAnchorState_Test is AnchorStateRegistry_TestInit /// respected game type. /// @param _l2BlockNumber The L2 block number to use for the game. function testFuzz_setAnchorState_validNewerState_succeeds(uint256 _l2BlockNumber) public { - // Grab block number of the existing anchor root. - (Hash root, uint256 l2BlockNumber) = anchorStateRegistry.getAnchorRoot(); - // Bound the new block number. _l2BlockNumber = bound(_l2BlockNumber, validL2BlockNumber, type(uint256).max); // Mock the l2BlockNumber call. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.l2SequenceNumber, ()), abi.encode(_l2BlockNumber)); - - // Mock the DEFENDER_WINS state. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.status, ()), abi.encode(GameStatus.DEFENDER_WINS)); - - // Mock that the game was respected. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.wasRespectedGameTypeWhenCreated, ()), abi.encode(true)); + _mockGameL2SequenceNumber(_l2BlockNumber); - // Mock the resolvedAt timestamp and fast forward to beyond the delay. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.resolvedAt, ()), abi.encode(block.timestamp)); - vm.warp(block.timestamp + anchorStateRegistry.disputeGameFinalityDelaySeconds() + 1); + _mockGamePastFinalityWithStatus(GameStatus.DEFENDER_WINS); + _mockGameWasRespected(true); // Update the anchor state. vm.prank(address(gameProxy)); @@ -987,9 +872,7 @@ contract AnchorStateRegistry_SetAnchorState_Test is AnchorStateRegistry_TestInit anchorStateRegistry.setAnchorState(gameProxy); // Confirm that the anchor state is now the same as the game state. - (root, l2BlockNumber) = anchorStateRegistry.getAnchorRoot(); - assertEq(l2BlockNumber, gameProxy.l2SequenceNumber()); - assertEq(root.raw(), gameProxy.rootClaim().raw()); + _assertAnchorRootEqGame(gameProxy); // Confirm that the anchor game is now set. IDisputeGame anchorGame = anchorStateRegistry.anchorGame(); @@ -1007,250 +890,102 @@ contract AnchorStateRegistry_SetAnchorState_Test is AnchorStateRegistry_TestInit _l2BlockNumber = bound(_l2BlockNumber, 0, l2BlockNumber); // Mock the l2BlockNumber call. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.l2SequenceNumber, ()), abi.encode(_l2BlockNumber)); - - // Mock the DEFENDER_WINS state. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.status, ()), abi.encode(GameStatus.DEFENDER_WINS)); - - // Mock that the game was respected. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.wasRespectedGameTypeWhenCreated, ()), abi.encode(true)); + _mockGameL2SequenceNumber(_l2BlockNumber); - // Mock the resolvedAt timestamp and fast forward to beyond the delay. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.resolvedAt, ()), abi.encode(block.timestamp)); - vm.warp(block.timestamp + anchorStateRegistry.disputeGameFinalityDelaySeconds() + 1); + _mockGamePastFinalityWithStatus(GameStatus.DEFENDER_WINS); + _mockGameWasRespected(true); - // Try to update the anchor state. - vm.prank(address(gameProxy)); - vm.expectRevert(IAnchorStateRegistry.AnchorStateRegistry_InvalidAnchorGame.selector); - anchorStateRegistry.setAnchorState(gameProxy); + _expectSetAnchorStateInvalid(gameProxy, address(gameProxy)); - // Confirm that the anchor state has not updated. - (Hash updatedRoot, uint256 updatedL2BlockNumber) = anchorStateRegistry.getAnchorRoot(); - assertEq(updatedL2BlockNumber, l2BlockNumber); - assertEq(updatedRoot.raw(), root.raw()); + _assertCurrentAnchorRootEq(root, l2BlockNumber); } /// @notice Tests that setAnchorState will revert if the game is not registered. - /// @param _l2BlockNumber The L2 block number to use for the game. - function testFuzz_setAnchorState_notFactoryRegisteredGame_fails(uint256 _l2BlockNumber) public { + function test_setAnchorState_notFactoryRegisteredGame_fails() public { // Grab block number of the existing anchor root. (Hash root, uint256 l2BlockNumber) = anchorStateRegistry.getAnchorRoot(); - // Bound the new block number. - _l2BlockNumber = bound(_l2BlockNumber, l2BlockNumber, type(uint256).max); - - // Mock the l2BlockNumber call. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.l2SequenceNumber, ()), abi.encode(_l2BlockNumber)); - - // Mock the DEFENDER_WINS state. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.status, ()), abi.encode(GameStatus.DEFENDER_WINS)); + _mockGameNotRegistered(); - // Mock that the game was respected. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.wasRespectedGameTypeWhenCreated, ()), abi.encode(true)); + _expectSetAnchorStateInvalid(gameProxy, superchainConfig.guardian()); - // Mock the DisputeGameFactory to make it seem that the game was not registered. - vm.mockCall( - address(disputeGameFactory), - abi.encodeCall( - disputeGameFactory.games, (gameProxy.gameType(), gameProxy.rootClaim(), gameProxy.extraData()) - ), - abi.encode(address(0), 0) - ); - - // Try to update the anchor state. - vm.prank(superchainConfig.guardian()); - vm.expectRevert(IAnchorStateRegistry.AnchorStateRegistry_InvalidAnchorGame.selector); - anchorStateRegistry.setAnchorState(gameProxy); - - // Confirm that the anchor state has not updated. - (Hash updatedRoot, uint256 updatedL2BlockNumber) = anchorStateRegistry.getAnchorRoot(); - assertEq(updatedL2BlockNumber, l2BlockNumber); - assertEq(updatedRoot.raw(), root.raw()); + _assertCurrentAnchorRootEq(root, l2BlockNumber); } /// @notice Tests that setAnchorState will revert if the game is valid and the game status is /// CHALLENGER_WINS. - /// @param _l2BlockNumber The L2 block number to use for the game. - function testFuzz_setAnchorState_challengerWins_fails(uint256 _l2BlockNumber) public { + function test_setAnchorState_challengerWins_fails() public { // Grab block number of the existing anchor root. (Hash root, uint256 l2BlockNumber) = anchorStateRegistry.getAnchorRoot(); - // Bound the new block number. - _l2BlockNumber = bound(_l2BlockNumber, l2BlockNumber, type(uint256).max); - - // Mock the l2BlockNumber call. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.l2SequenceNumber, ()), abi.encode(_l2BlockNumber)); - - // Mock the CHALLENGER_WINS state. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.status, ()), abi.encode(GameStatus.CHALLENGER_WINS)); - - // Mock that the game was respected. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.wasRespectedGameTypeWhenCreated, ()), abi.encode(true)); + _mockGamePastFinalityWithStatus(GameStatus.CHALLENGER_WINS); + _mockGameWasRespected(true); - // Mock the resolvedAt timestamp and fast forward to beyond the delay. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.resolvedAt, ()), abi.encode(block.timestamp)); - vm.warp(block.timestamp + anchorStateRegistry.disputeGameFinalityDelaySeconds() + 1); - - // Try to update the anchor state. - vm.prank(address(gameProxy)); - vm.expectRevert(IAnchorStateRegistry.AnchorStateRegistry_InvalidAnchorGame.selector); - anchorStateRegistry.setAnchorState(gameProxy); + _expectSetAnchorStateInvalid(gameProxy, address(gameProxy)); - // Confirm that the anchor state has not updated. - (Hash updatedRoot, uint256 updatedL2BlockNumber) = anchorStateRegistry.getAnchorRoot(); - assertEq(updatedL2BlockNumber, l2BlockNumber); - assertEq(updatedRoot.raw(), root.raw()); + _assertCurrentAnchorRootEq(root, l2BlockNumber); } /// @notice Tests that setAnchorState will revert if the game is valid and the game status is /// IN_PROGRESS. - /// @param _l2BlockNumber The L2 block number to use for the game. - function testFuzz_setAnchorState_inProgress_fails(uint256 _l2BlockNumber) public { + function test_setAnchorState_inProgress_fails() public { // Grab block number of the existing anchor root. (Hash root, uint256 l2BlockNumber) = anchorStateRegistry.getAnchorRoot(); - // Bound the new block number. - _l2BlockNumber = bound(_l2BlockNumber, l2BlockNumber, type(uint256).max); - - // Mock the l2BlockNumber call. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.l2SequenceNumber, ()), abi.encode(_l2BlockNumber)); - - // Mock the CHALLENGER_WINS state. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.status, ()), abi.encode(GameStatus.IN_PROGRESS)); - - // Mock that the game was respected. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.wasRespectedGameTypeWhenCreated, ()), abi.encode(true)); + _mockGamePastFinalityWithStatus(GameStatus.IN_PROGRESS); + _mockGameWasRespected(true); - // Mock the resolvedAt timestamp and fast forward to beyond the delay. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.resolvedAt, ()), abi.encode(block.timestamp)); - vm.warp(block.timestamp + anchorStateRegistry.disputeGameFinalityDelaySeconds() + 1); + _expectSetAnchorStateInvalid(gameProxy, address(gameProxy)); - // Try to update the anchor state. - vm.prank(address(gameProxy)); - vm.expectRevert(IAnchorStateRegistry.AnchorStateRegistry_InvalidAnchorGame.selector); - anchorStateRegistry.setAnchorState(gameProxy); - - // Confirm that the anchor state has not updated. - (Hash updatedRoot, uint256 updatedL2BlockNumber) = anchorStateRegistry.getAnchorRoot(); - assertEq(updatedL2BlockNumber, l2BlockNumber); - assertEq(updatedRoot.raw(), root.raw()); + _assertCurrentAnchorRootEq(root, l2BlockNumber); } /// @notice Tests that setAnchorState will revert if the game is not respected. - /// @param _l2BlockNumber The L2 block number to use for the game. - function testFuzz_setAnchorState_isNotRespectedGame_fails(uint256 _l2BlockNumber) public { + function test_setAnchorState_isNotRespectedGame_fails() public { // Grab block number of the existing anchor root. (Hash root, uint256 l2BlockNumber) = anchorStateRegistry.anchors(gameProxy.gameType()); - // Bound the new block number. - _l2BlockNumber = bound(_l2BlockNumber, l2BlockNumber, type(uint256).max); - - // Mock the l2BlockNumber call. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.l2SequenceNumber, ()), abi.encode(_l2BlockNumber)); - - // Mock the DEFENDER_WINS state. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.status, ()), abi.encode(GameStatus.DEFENDER_WINS)); - - // Mock the resolvedAt timestamp and fast forward to beyond the delay. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.resolvedAt, ()), abi.encode(block.timestamp)); - vm.warp(block.timestamp + anchorStateRegistry.disputeGameFinalityDelaySeconds() + 1); - - // Mock that the game was not respected when created. - vm.mockCall( - address(gameProxy), abi.encodeCall(gameProxy.wasRespectedGameTypeWhenCreated, ()), abi.encode(false) - ); + _mockGameWasRespected(false); - // Try to update the anchor state. - vm.prank(address(gameProxy)); - vm.expectRevert(IAnchorStateRegistry.AnchorStateRegistry_InvalidAnchorGame.selector); - anchorStateRegistry.setAnchorState(gameProxy); + _expectSetAnchorStateInvalid(gameProxy, address(gameProxy)); - // Confirm that the anchor state has not updated. - (Hash updatedRoot, uint256 updatedL2BlockNumber) = anchorStateRegistry.anchors(gameProxy.gameType()); - assertEq(updatedL2BlockNumber, l2BlockNumber); - assertEq(updatedRoot.raw(), root.raw()); + _assertAnchorEq(gameProxy.gameType(), root, l2BlockNumber); } /// @notice Tests that setAnchorState will revert if the game is valid and the game is /// blacklisted. - /// @param _l2BlockNumber The L2 block number to use for the game. - function testFuzz_setAnchorState_blacklistedGame_fails(uint256 _l2BlockNumber) public { + function test_setAnchorState_blacklistedGame_fails() public { // Grab block number of the existing anchor root. (Hash root, uint256 l2BlockNumber) = anchorStateRegistry.getAnchorRoot(); - // Bound the new block number. - _l2BlockNumber = bound(_l2BlockNumber, validL2BlockNumber, type(uint256).max); - - // Mock the DEFENDER_WINS state. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.status, ()), abi.encode(GameStatus.DEFENDER_WINS)); - - // Mock that the game was respected. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.wasRespectedGameTypeWhenCreated, ()), abi.encode(true)); - - // Mock the resolvedAt timestamp and fast forward to beyond the delay. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.resolvedAt, ()), abi.encode(block.timestamp)); - vm.warp(block.timestamp + anchorStateRegistry.disputeGameFinalityDelaySeconds() + 1); - // Blacklist the game. - vm.prank(superchainConfig.guardian()); - anchorStateRegistry.blacklistDisputeGame(gameProxy); + _blacklistDisputeGameAsGuardian(gameProxy); - // Update the anchor state. - vm.prank(address(gameProxy)); - vm.expectRevert(IAnchorStateRegistry.AnchorStateRegistry_InvalidAnchorGame.selector); - anchorStateRegistry.setAnchorState(gameProxy); + _expectSetAnchorStateInvalid(gameProxy, address(gameProxy)); - // Confirm that the anchor state has not updated. - (Hash updatedRoot, uint256 updatedL2BlockNumber) = anchorStateRegistry.anchors(gameProxy.gameType()); - assertEq(updatedL2BlockNumber, l2BlockNumber); - assertEq(updatedRoot.raw(), root.raw()); + _assertAnchorEq(gameProxy.gameType(), root, l2BlockNumber); } /// @notice Tests that setAnchorState will revert if the game is retired. - /// @param _l2BlockNumber The L2 block number to use for the game. - function testFuzz_setAnchorState_retiredGame_fails(uint256 _l2BlockNumber) public { + function test_setAnchorState_retiredGame_fails() public { // Grab block number of the existing anchor root. (Hash root, uint256 l2BlockNumber) = anchorStateRegistry.getAnchorRoot(); - // Bound the new block number. - _l2BlockNumber = bound(_l2BlockNumber, validL2BlockNumber, type(uint256).max); - - // Mock the DEFENDER_WINS state. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.status, ()), abi.encode(GameStatus.DEFENDER_WINS)); + _mockRetiredGame(0); + _expectSetAnchorStateInvalid(gameProxy, address(gameProxy)); - // Mock that the game was respected. - vm.mockCall(address(gameProxy), abi.encodeCall(gameProxy.wasRespectedGameTypeWhenCreated, ()), abi.encode(true)); - - // Set the retirement timestamp. - vm.prank(superchainConfig.guardian()); - anchorStateRegistry.updateRetirementTimestamp(); - - // Mock the call to createdAt. - vm.mockCall( - address(gameProxy), - abi.encodeCall(gameProxy.createdAt, ()), - abi.encode(anchorStateRegistry.retirementTimestamp() - 1) - ); - - // Update the anchor state. - vm.prank(address(gameProxy)); - vm.expectRevert(IAnchorStateRegistry.AnchorStateRegistry_InvalidAnchorGame.selector); - anchorStateRegistry.setAnchorState(gameProxy); - - // Confirm that the anchor state has not updated. - (Hash updatedRoot, uint256 updatedL2BlockNumber) = anchorStateRegistry.anchors(gameProxy.gameType()); - assertEq(updatedL2BlockNumber, l2BlockNumber); - assertEq(updatedRoot.raw(), root.raw()); + _assertAnchorEq(gameProxy.gameType(), root, l2BlockNumber); } /// @notice Tests that setAnchorState will revert if the superchain is paused. function test_setAnchorState_superchainPaused_fails() public { + (Hash root, uint256 l2BlockNumber) = anchorStateRegistry.getAnchorRoot(); + // Pause the superchain. _setPaused(true); - // Update the anchor state. - vm.prank(address(gameProxy)); - vm.expectRevert(IAnchorStateRegistry.AnchorStateRegistry_InvalidAnchorGame.selector); - anchorStateRegistry.setAnchorState(gameProxy); + _expectSetAnchorStateInvalid(gameProxy, address(gameProxy)); + + _assertCurrentAnchorRootEq(root, l2BlockNumber); } } diff --git a/test/L1/proofs/BaseTest.t.sol b/test/L1/proofs/BaseTest.t.sol index 068d62dc..50184d9a 100644 --- a/test/L1/proofs/BaseTest.t.sol +++ b/test/L1/proofs/BaseTest.t.sol @@ -1,9 +1,8 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.15; import { Test } from "lib/forge-std/src/Test.sol"; -// Optimism import { AnchorStateRegistry } from "src/L1/proofs/AnchorStateRegistry.sol"; import { DelayedWETH } from "src/L1/proofs/DelayedWETH.sol"; import { DisputeGameFactory } from "src/L1/proofs/DisputeGameFactory.sol"; @@ -12,10 +11,9 @@ import { IDelayedWETH } from "interfaces/L1/proofs/IDelayedWETH.sol"; import { IDisputeGame } from "interfaces/L1/proofs/IDisputeGame.sol"; import { IDisputeGameFactory } from "interfaces/L1/proofs/IDisputeGameFactory.sol"; import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; -import { GameStatus, GameType, Hash, Proposal } from "src/libraries/bridge/Types.sol"; -import { Claim, Timestamp } from "src/libraries/bridge/LibUDT.sol"; +import { GameType, Hash, Proposal } from "src/libraries/bridge/Types.sol"; +import { Claim } from "src/libraries/bridge/LibUDT.sol"; -// OpenZeppelin import { ProxyAdmin } from "src/universal/ProxyAdmin.sol"; import { TransparentUpgradeableProxy @@ -26,54 +24,49 @@ import { IVerifier } from "interfaces/L1/proofs/IVerifier.sol"; import { MockVerifier } from "test/mocks/MockVerifier.sol"; -import { LibClone } from "lib/solady/src/utils/LibClone.sol"; - contract BaseTest is Test { - using LibClone for address; - // Constants - GameType public constant AGGREGATE_VERIFIER_GAME_TYPE = GameType.wrap(621); - uint256 public constant L2_CHAIN_ID = 8453; + GameType internal constant AGGREGATE_VERIFIER_GAME_TYPE = GameType.wrap(621); + uint256 internal constant L2_CHAIN_ID = 8453; - // MUST HAVE: BLOCK_INTERVAL % INTERMEDIATE_BLOCK_INTERVAL == 0 - uint256 public constant BLOCK_INTERVAL = 100; - uint256 public constant INTERMEDIATE_BLOCK_INTERVAL = 10; + // AggregateVerifier expects evenly spaced intermediate roots. + uint256 internal constant BLOCK_INTERVAL = 100; + uint256 internal constant INTERMEDIATE_BLOCK_INTERVAL = 10; + uint256 private constant INTERMEDIATE_ROOTS_COUNT = BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL; - uint256 public constant INIT_BOND = 1 ether; - uint256 public constant DELAYED_WETH_DELAY = 1 days; + uint256 internal constant INIT_BOND = 1 ether; + uint256 internal constant DELAYED_WETH_DELAY = 1 days; // Finality delay handled by the AggregateVerifier - uint256 public constant FINALITY_DELAY = 0 days; + uint256 internal constant FINALITY_DELAY = 0 days; - uint256 public currentL2BlockNumber = 0; + uint256 internal currentL2BlockNumber; - address public immutable TEE_PROVER = makeAddr("tee-prover"); - address public immutable ZK_PROVER = makeAddr("zk-prover"); - address public immutable ATTACKER = makeAddr("attacker"); + address internal immutable TEE_PROVER = makeAddr("tee-prover"); + address internal immutable ZK_PROVER = makeAddr("zk-prover"); - bytes32 public immutable TEE_IMAGE_HASH = keccak256("tee-image"); - bytes32 public immutable ZK_RANGE_HASH = keccak256("zk-range"); - bytes32 public immutable ZK_AGGREGATE_HASH = keccak256("zk-aggregate"); - bytes32 public immutable CONFIG_HASH = keccak256("config"); + bytes32 internal immutable TEE_IMAGE_HASH = keccak256("tee-image"); + bytes32 internal immutable ZK_RANGE_HASH = keccak256("zk-range"); + bytes32 internal immutable ZK_AGGREGATE_HASH = keccak256("zk-aggregate"); + bytes32 internal immutable CONFIG_HASH = keccak256("config"); - ProxyAdmin public proxyAdmin; - ISystemConfig public systemConfig; + ProxyAdmin internal proxyAdmin; + ISystemConfig internal systemConfig; - DisputeGameFactory public factory; - AnchorStateRegistry public anchorStateRegistry; - DelayedWETH public delayedWETH; + DisputeGameFactory internal factory; + AnchorStateRegistry internal anchorStateRegistry; + DelayedWETH internal delayedWETH; - MockVerifier public teeVerifier; - MockVerifier public zkVerifier; + MockVerifier internal teeVerifier; + MockVerifier internal zkVerifier; function setUp() public virtual { _deployContractsAndProxies(); _initializeProxies(); - // Deploy the implementations _deployAndSetAggregateVerifier(); anchorStateRegistry.setRespectedGameType(AGGREGATE_VERIFIER_GAME_TYPE); - // Set the timestamp to after the retirement timestamp + // Games created at or before the registry's retirement timestamp are invalid. vm.warp(block.timestamp + 1); } @@ -81,38 +74,26 @@ contract BaseTest is Test { systemConfig = ISystemConfig(makeAddr("system-config")); vm.mockCall(address(systemConfig), abi.encodeCall(ISystemConfig.guardian, ()), abi.encode(address(this))); vm.mockCall(address(systemConfig), abi.encodeCall(ISystemConfig.paused, ()), abi.encode(false)); - // Deploy the relay anchor state registry + AnchorStateRegistry _anchorStateRegistry = new AnchorStateRegistry(FINALITY_DELAY); - // Deploy the delayed WETH DelayedWETH _delayedWETH = new DelayedWETH(DELAYED_WETH_DELAY); - // Deploy the dispute game factory DisputeGameFactory _factory = new DisputeGameFactory(); - // Deploy proxy admin proxyAdmin = new ProxyAdmin(address(this)); - // Deploy proxy for anchor state registry - TransparentUpgradeableProxy anchorStateRegistryProxy = - new TransparentUpgradeableProxy(address(_anchorStateRegistry), address(proxyAdmin), ""); - anchorStateRegistry = AnchorStateRegistry(address(anchorStateRegistryProxy)); - - // Deploy proxy for factory - TransparentUpgradeableProxy factoryProxy = - new TransparentUpgradeableProxy(address(_factory), address(proxyAdmin), ""); - factory = DisputeGameFactory(address(factoryProxy)); - - // Deploy proxy for delayed WETH - TransparentUpgradeableProxy delayedWETHProxy = - new TransparentUpgradeableProxy(address(_delayedWETH), address(proxyAdmin), ""); - delayedWETH = DelayedWETH(payable(address(delayedWETHProxy))); + anchorStateRegistry = AnchorStateRegistry(_deployProxy(address(_anchorStateRegistry))); + factory = DisputeGameFactory(_deployProxy(address(_factory))); + delayedWETH = DelayedWETH(payable(_deployProxy(address(_delayedWETH)))); - // Deploy the verifiers teeVerifier = new MockVerifier(IAnchorStateRegistry(address(anchorStateRegistry))); zkVerifier = new MockVerifier(IAnchorStateRegistry(address(anchorStateRegistry))); } + function _deployProxy(address implementation) private returns (address) { + return address(new TransparentUpgradeableProxy(implementation, address(proxyAdmin), "")); + } + function _initializeProxies() internal { - // Initialize the proxies anchorStateRegistry.initialize( systemConfig, IDisputeGameFactory(address(factory)), @@ -126,7 +107,6 @@ contract BaseTest is Test { } function _deployAndSetAggregateVerifier() internal { - // Deploy the dispute game relay implementation AggregateVerifier aggregateVerifierImpl = new AggregateVerifier( AGGREGATE_VERIFIER_GAME_TYPE, IAnchorStateRegistry(address(anchorStateRegistry)), @@ -141,14 +121,10 @@ contract BaseTest is Test { INTERMEDIATE_BLOCK_INTERVAL ); - // Set the implementation for the aggregate verifier factory.setImplementation(AGGREGATE_VERIFIER_GAME_TYPE, IDisputeGame(address(aggregateVerifierImpl))); - - // Set the bond amount for the aggregate verifier factory.setInitBond(AGGREGATE_VERIFIER_GAME_TYPE, INIT_BOND); } - // Helper function to create a game via factory function _createAggregateVerifierGame( address creator, Claim rootClaim, @@ -159,9 +135,7 @@ contract BaseTest is Test { internal returns (AggregateVerifier game) { - bytes memory intermediateRoots = - abi.encodePacked(_generateIntermediateRootsExceptLast(l2BlockNumber), rootClaim.raw()); - bytes memory extraData = abi.encodePacked(uint256(l2BlockNumber), parentAddress, intermediateRoots); + bytes memory extraData = _aggregateVerifierExtraData(rootClaim, l2BlockNumber, parentAddress); vm.deal(creator, INIT_BOND); vm.prank(creator); @@ -174,41 +148,12 @@ contract BaseTest is Test { ); } - /// @notice Clones the `AggregateVerifier` implementation with the same CWIA layout as `DisputeGameFactory`, - /// initializes it, but does not register the game in the factory (no `_finalizeGameCreation`). - function _deployAggregateVerifierCloneWithoutFactoryRegistration( - address creator, - Claim rootClaim, - uint256 l2BlockNumber, - address parentAddress, - bytes memory proof - ) - internal - returns (AggregateVerifier game) - { - IDisputeGame impl = factory.gameImpls(AGGREGATE_VERIFIER_GAME_TYPE); - bytes memory intermediateRoots = - abi.encodePacked(_generateIntermediateRootsExceptLast(l2BlockNumber), rootClaim.raw()); - bytes memory extraData = abi.encodePacked(uint256(l2BlockNumber), parentAddress, intermediateRoots); - bytes32 l1Head = blockhash(block.number - 1); - address clone = address(impl).clone(abi.encodePacked(creator, rootClaim, l1Head, extraData)); - vm.deal(creator, INIT_BOND); - vm.prank(creator); - AggregateVerifier(payable(clone)).initializeWithInitData{ value: INIT_BOND }(proof); - return AggregateVerifier(payable(clone)); - } - function _provideProof(AggregateVerifier game, address prover, bytes memory proofBytes) internal { vm.prank(prover); game.verifyProposalProof(proofBytes); } - /// @notice Generates a properly formatted proof for testing. - /// @dev The proof format is: l1OriginHash (32) + l1OriginNumber (32) + additional data. - /// Since MockVerifier always returns true, we just need the correct structure. - /// @param salt A salt to make proofs unique. - /// @param proofType The type of proof to generate. - /// @return proof The formatted proof bytes. + /// @dev Encodes proofType || l1OriginHash || l1OriginNumber || mock verifier payload. function _generateProof( bytes memory salt, AggregateVerifier.ProofType proofType @@ -217,24 +162,32 @@ contract BaseTest is Test { view returns (bytes memory) { - // Use the previous block hash as l1OriginHash - bytes32 l1OriginHash = blockhash(block.number - 1); - // Use the previous block number as l1OriginNumber uint256 l1OriginNumber = block.number - 1; - // Add some padding/signature data (65 bytes minimum for a signature) - bytes memory signature = abi.encodePacked(salt, bytes32(0), bytes32(0), uint8(27)); + bytes32 l1OriginHash = blockhash(l1OriginNumber); + + return abi.encodePacked(uint8(proofType), l1OriginHash, l1OriginNumber, salt, bytes32(0), bytes32(0), uint8(27)); + } - return abi.encodePacked(uint8(proofType), l1OriginHash, l1OriginNumber, signature); + function _aggregateVerifierExtraData( + Claim rootClaim, + uint256 l2BlockNumber, + address parentAddress + ) + internal + pure + returns (bytes memory) + { + return abi.encodePacked(l2BlockNumber, parentAddress, _generateIntermediateRoots(l2BlockNumber, rootClaim)); } - function _generateIntermediateRootsExceptLast(uint256 l2BlockNumber) internal pure returns (bytes memory) { - bytes memory intermediateRoots; + function _generateIntermediateRoots(uint256 l2BlockNumber, Claim rootClaim) private pure returns (bytes memory) { + bytes32[] memory intermediateRoots = new bytes32[](INTERMEDIATE_ROOTS_COUNT); uint256 startingL2BlockNumber = l2BlockNumber - BLOCK_INTERVAL; - for (uint256 i = 1; i < BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL; i++) { - intermediateRoots = abi.encodePacked( - intermediateRoots, keccak256(abi.encode(startingL2BlockNumber + INTERMEDIATE_BLOCK_INTERVAL * i)) - ); + for (uint256 i = 1; i < INTERMEDIATE_ROOTS_COUNT; i++) { + intermediateRoots[i - 1] = keccak256(abi.encode(startingL2BlockNumber + INTERMEDIATE_BLOCK_INTERVAL * i)); } - return intermediateRoots; + intermediateRoots[INTERMEDIATE_ROOTS_COUNT - 1] = rootClaim.raw(); + + return abi.encodePacked(intermediateRoots); } } diff --git a/test/L1/proofs/Challenge.t.sol b/test/L1/proofs/Challenge.t.sol index 977518aa..0aca7e4d 100644 --- a/test/L1/proofs/Challenge.t.sol +++ b/test/L1/proofs/Challenge.t.sol @@ -2,9 +2,8 @@ pragma solidity 0.8.15; import { ClaimAlreadyResolved } from "src/libraries/bridge/Errors.sol"; -import { IAnchorStateRegistry } from "interfaces/L1/proofs/IAnchorStateRegistry.sol"; import { IDisputeGame } from "interfaces/L1/proofs/IDisputeGame.sol"; -import { GameStatus, Hash } from "src/libraries/bridge/Types.sol"; +import { GameStatus } from "src/libraries/bridge/Types.sol"; import { Claim } from "src/libraries/bridge/LibUDT.sol"; import { AggregateVerifier } from "src/L1/proofs/AggregateVerifier.sol"; @@ -13,298 +12,207 @@ import { Verifier } from "src/L1/proofs/Verifier.sol"; import { BaseTest } from "./BaseTest.t.sol"; contract ChallengeTest is BaseTest { - function testChallengeTEEProofWithZKProof() public { - currentL2BlockNumber += BLOCK_INTERVAL; - - // Create game with TEE proof - Claim rootClaim1 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee"))); - bytes memory teeProof = _generateProof("tee-proof", AggregateVerifier.ProofType.TEE); - - AggregateVerifier game = _createAggregateVerifierGame( - TEE_PROVER, rootClaim1, currentL2BlockNumber, address(anchorStateRegistry), teeProof - ); - - // Challenge game with ZK proof - Claim rootClaim2 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "zk"))); - bytes memory zkProof = _generateProof("zk-proof", AggregateVerifier.ProofType.ZK); + uint256 private constant LAST_INTERMEDIATE_ROOT_INDEX = BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1; - vm.prank(ZK_PROVER); - game.challenge(zkProof, BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1, rootClaim2.raw()); + function testChallengeTEEProofWithZKProof() public { + AggregateVerifier game = + _createGame(TEE_PROVER, "tee", "tee-proof", AggregateVerifier.ProofType.TEE, address(anchorStateRegistry)); + _challengeWithZk(game, "zk"); assertEq(uint8(game.status()), uint8(GameStatus.IN_PROGRESS)); - // 2 proofs so that it can decrease to 1 if ZK is nullified and then the TEE proof can resolve assertEq(game.proofCount(), 2); - // Resolve after SLOW_FINALIZATION_DELAY - vm.warp(block.timestamp + 7 days); - game.resolve(); - - assertEq(uint8(game.status()), uint8(GameStatus.CHALLENGER_WINS)); - assertEq(game.bondRecipient(), ZK_PROVER); - - uint256 balanceBefore = ZK_PROVER.balance; - game.claimCredit(); - vm.warp(block.timestamp + DELAYED_WETH_DELAY); - game.claimCredit(); - assertEq(ZK_PROVER.balance, balanceBefore + INIT_BOND); - assertEq(delayedWETH.balanceOf(address(game)), 0); + _resolveAfterSlowDelayAndClaim(game, GameStatus.CHALLENGER_WINS, ZK_PROVER); } function testChallengeFailsIfNoTEEProof() public { - currentL2BlockNumber += BLOCK_INTERVAL; - - // Create first game with ZK proof (no TEE proof) - Claim rootClaim1 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "zk1"))); - bytes memory zkProof1 = _generateProof("zk-proof-1", AggregateVerifier.ProofType.ZK); - - AggregateVerifier game1 = _createAggregateVerifierGame( - ZK_PROVER, rootClaim1, currentL2BlockNumber, address(anchorStateRegistry), zkProof1 - ); - - // Challenge game with ZK proof - bytes memory zkProof2 = _generateProof("zk-proof-2", AggregateVerifier.ProofType.ZK); + AggregateVerifier game = + _createGame(ZK_PROVER, "zk1", "zk-proof-1", AggregateVerifier.ProofType.ZK, address(anchorStateRegistry)); vm.expectRevert( abi.encodeWithSelector(AggregateVerifier.MissingProof.selector, AggregateVerifier.ProofType.TEE) ); - game1.challenge(zkProof2, BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1, rootClaim1.raw()); + _challenge(game, AggregateVerifier.ProofType.ZK, bytes32(0)); } function testChallengeFailsIfNotZKProof() public { - currentL2BlockNumber += BLOCK_INTERVAL; - - Claim rootClaim1 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee1"))); - bytes memory teeProof1 = _generateProof("tee-proof-1", AggregateVerifier.ProofType.TEE); - - AggregateVerifier game1 = _createAggregateVerifierGame( - TEE_PROVER, rootClaim1, currentL2BlockNumber, address(anchorStateRegistry), teeProof1 + AggregateVerifier game = _createGame( + TEE_PROVER, "tee1", "tee-proof-1", AggregateVerifier.ProofType.TEE, address(anchorStateRegistry) ); - Claim rootClaim2 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee2"))); - bytes memory teeProof2 = _generateProof("tee-proof-2", AggregateVerifier.ProofType.TEE); - vm.expectRevert(AggregateVerifier.InvalidProofType.selector); - game1.challenge(teeProof2, BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1, rootClaim2.raw()); + _challenge(game, AggregateVerifier.ProofType.TEE, bytes32(0)); } function testChallengeFailsIfGameAlreadyResolved() public { - currentL2BlockNumber += BLOCK_INTERVAL; - - Claim rootClaim1 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee"))); - bytes memory teeProof = _generateProof("tee-proof", AggregateVerifier.ProofType.TEE); - - AggregateVerifier game1 = _createAggregateVerifierGame( - TEE_PROVER, rootClaim1, currentL2BlockNumber, address(anchorStateRegistry), teeProof - ); - - // Resolve game1 - vm.warp(block.timestamp + 7 days + 1); - game1.resolve(); + AggregateVerifier game = + _createGame(TEE_PROVER, "tee", "tee-proof", AggregateVerifier.ProofType.TEE, address(anchorStateRegistry)); - // Try to challenge game1 - Claim rootClaim2 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "zk1"))); - bytes memory zkProof = _generateProof("zk-proof", AggregateVerifier.ProofType.ZK); + vm.warp(block.timestamp + game.SLOW_FINALIZATION_DELAY() + 1); + game.resolve(); vm.expectRevert(ClaimAlreadyResolved.selector); - game1.challenge(zkProof, BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1, rootClaim2.raw()); + _challenge(game, AggregateVerifier.ProofType.ZK, bytes32(0)); } function testChallengeFailsIfParentGameStatusIsChallenged() public { - currentL2BlockNumber += BLOCK_INTERVAL; - - // create parent game - Claim rootClaim1 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee"))); - bytes memory parentProof = _generateProof("parent-proof", AggregateVerifier.ProofType.TEE); - - AggregateVerifier parentGame = _createAggregateVerifierGame( - TEE_PROVER, rootClaim1, currentL2BlockNumber, address(anchorStateRegistry), parentProof + AggregateVerifier parentGame = _createGame( + TEE_PROVER, "tee", "parent-proof", AggregateVerifier.ProofType.TEE, address(anchorStateRegistry) ); - currentL2BlockNumber += BLOCK_INTERVAL; - - // create child game - Claim rootClaim2 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee2"))); - bytes memory childProof = _generateProof("child-proof", AggregateVerifier.ProofType.TEE); - AggregateVerifier childGame = - _createAggregateVerifierGame(TEE_PROVER, rootClaim2, currentL2BlockNumber, address(parentGame), childProof); + _createGame(TEE_PROVER, "tee2", "child-proof", AggregateVerifier.ProofType.TEE, address(parentGame)); - // blacklist parent game anchorStateRegistry.blacklistDisputeGame(IDisputeGame(address(parentGame))); - // challenge child game with ZK proof - Claim rootClaim3 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "zk"))); - bytes memory zkProof = _generateProof("zk-proof", AggregateVerifier.ProofType.ZK); - vm.expectRevert(AggregateVerifier.InvalidParentGame.selector); - childGame.challenge(zkProof, BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1, rootClaim3.raw()); + _challenge(childGame, AggregateVerifier.ProofType.ZK, bytes32(0)); } function testChallengeFailsIfGameItselfIsBlacklisted() public { - currentL2BlockNumber += BLOCK_INTERVAL; - Claim rootClaim = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee"))); - bytes memory proof = _generateProof("tee-proof", AggregateVerifier.ProofType.TEE); - - AggregateVerifier game = _createAggregateVerifierGame( - TEE_PROVER, rootClaim, currentL2BlockNumber, address(anchorStateRegistry), proof - ); + AggregateVerifier game = + _createGame(TEE_PROVER, "tee", "tee-proof", AggregateVerifier.ProofType.TEE, address(anchorStateRegistry)); - // blacklist game anchorStateRegistry.blacklistDisputeGame(IDisputeGame(address(game))); - // challenge game - Claim rootClaim2 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "zk"))); - bytes memory zkProof = _generateProof("zk-proof", AggregateVerifier.ProofType.ZK); - vm.expectRevert(AggregateVerifier.InvalidGame.selector); - game.challenge(zkProof, BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1, rootClaim2.raw()); + _challenge(game, AggregateVerifier.ProofType.ZK, bytes32(0)); } function testChallengeFailsAfterTEENullification() public { - currentL2BlockNumber += BLOCK_INTERVAL; - - Claim rootClaim1 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee1"))); - bytes memory teeProof1 = _generateProof("tee-proof-1", AggregateVerifier.ProofType.TEE); - - AggregateVerifier game = _createAggregateVerifierGame( - TEE_PROVER, rootClaim1, currentL2BlockNumber, address(anchorStateRegistry), teeProof1 + AggregateVerifier game = _createGame( + TEE_PROVER, "tee1", "tee-proof-1", AggregateVerifier.ProofType.TEE, address(anchorStateRegistry) ); - Claim rootClaim2 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee2"))); - bytes memory teeProof2 = _generateProof("tee-proof-2", AggregateVerifier.ProofType.TEE); - - game.nullify(teeProof2, BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1, rootClaim2.raw()); - - // challenge game — TEE proof was nullified, so MissingProof(TEE) is expected - Claim rootClaim3 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "zk"))); - bytes memory zkProof = _generateProof("zk-proof", AggregateVerifier.ProofType.ZK); - + _nullify(game, AggregateVerifier.ProofType.TEE, "tee2"); vm.expectRevert( abi.encodeWithSelector(AggregateVerifier.MissingProof.selector, AggregateVerifier.ProofType.TEE) ); - game.challenge(zkProof, BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1, rootClaim3.raw()); + _challenge(game, AggregateVerifier.ProofType.ZK, bytes32(0)); } function testChallengeFailsAfterZKNullification() public { - currentL2BlockNumber += BLOCK_INTERVAL; - Claim rootClaim1 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee"))); - bytes memory teeProof = _generateProof("tee-proof", AggregateVerifier.ProofType.TEE); - bytes memory zkProof1 = _generateProof("zk-proof-1", AggregateVerifier.ProofType.ZK); - - // create game with both proofs - AggregateVerifier game = _createAggregateVerifierGame( - ZK_PROVER, rootClaim1, currentL2BlockNumber, address(anchorStateRegistry), teeProof - ); - game.verifyProposalProof(zkProof1); - - // nullify ZK proof - Claim rootClaim2 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "zk2"))); - bytes memory zkProof2 = _generateProof("zk-proof-2", AggregateVerifier.ProofType.ZK); - game.nullify(zkProof2, BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1, rootClaim2.raw()); - - // challenge game — ZK is nullified so Nullified() is expected - Claim rootClaim3 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "zk3"))); - bytes memory zkProof3 = _generateProof("zk-proof-3", AggregateVerifier.ProofType.ZK); + AggregateVerifier game = + _createGame(ZK_PROVER, "tee", "tee-proof", AggregateVerifier.ProofType.TEE, address(anchorStateRegistry)); + _provideProof(game, ZK_PROVER, _proofOfType(AggregateVerifier.ProofType.ZK)); + _nullify(game, AggregateVerifier.ProofType.ZK, "zk2"); vm.expectRevert(Verifier.Nullified.selector); - game.challenge(zkProof3, BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1, rootClaim3.raw()); + _challenge(game, AggregateVerifier.ProofType.ZK, _claim("zk3").raw()); } - /// @notice A TEE+ZK challenge on game A is cleared when another game nullifies the shared ZK verifier; A then - /// resolves as defender after `SLOW_FINALIZATION_DELAY`. function testChallengeRemovedWhenZkVerifierNullifiedByOtherGame() public { - currentL2BlockNumber += BLOCK_INTERVAL; - - Claim rootClaimA = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee-challenge"))); - bytes memory teeProofA = _generateProof("tee-ch-a", AggregateVerifier.ProofType.TEE); - AggregateVerifier gameA = _createAggregateVerifierGame( - TEE_PROVER, rootClaimA, currentL2BlockNumber, address(anchorStateRegistry), teeProofA + AggregateVerifier gameA = _createGame( + TEE_PROVER, "tee-challenge", "tee-ch-a", AggregateVerifier.ProofType.TEE, address(anchorStateRegistry) ); - Claim rootChallenge = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "zk-challenge"))); - bytes memory zkChallenge = _generateProof("zk-challenge", AggregateVerifier.ProofType.ZK); - vm.prank(ZK_PROVER); - gameA.challenge(zkChallenge, BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1, rootChallenge.raw()); - - assertEq(gameA.proofCount(), 2); - assertGt(gameA.counteredByIntermediateRootIndexPlusOne(), 0); + _challengeWithZk(gameA, "zk-challenge"); + _assertChallengeRecorded(gameA); - currentL2BlockNumber += BLOCK_INTERVAL; - Claim rootClaimB = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "zk-only-b"))); - bytes memory zkProofB = _generateProof("zk-init-b", AggregateVerifier.ProofType.ZK); AggregateVerifier gameB = - _createAggregateVerifierGame(ZK_PROVER, rootClaimB, currentL2BlockNumber, address(gameA), zkProofB); - - Claim rootNullifyB = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "zk-nullify-b"))); - bytes memory zkNullifyB = _generateProof("zk-nullify-b", AggregateVerifier.ProofType.ZK); - uint256 lastIdx = BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1; - gameB.nullify(zkNullifyB, lastIdx, rootNullifyB.raw()); + _createGame(ZK_PROVER, "zk-only-b", "zk-init-b", AggregateVerifier.ProofType.ZK, address(gameA)); + _nullify(gameB, AggregateVerifier.ProofType.ZK, "zk-nullify-b"); assertTrue(zkVerifier.nullified()); - assertEq(uint8(gameA.resolve()), uint8(GameStatus.IN_PROGRESS)); + _resolveAndAssertStatus(gameA, GameStatus.IN_PROGRESS); assertEq(gameA.proofCount(), 1); assertEq(gameA.counteredByIntermediateRootIndexPlusOne(), 0); assertEq(address(gameA.zkProver()), address(0)); - vm.warp(block.timestamp + 7 days); - assertEq(uint8(gameA.resolve()), uint8(GameStatus.DEFENDER_WINS)); - assertEq(gameA.bondRecipient(), TEE_PROVER); - - uint256 balanceBefore = TEE_PROVER.balance; - gameA.claimCredit(); - vm.warp(block.timestamp + DELAYED_WETH_DELAY); - gameA.claimCredit(); - assertEq(TEE_PROVER.balance, balanceBefore + INIT_BOND); - assertEq(delayedWETH.balanceOf(address(gameA)), 0); + _resolveAfterSlowDelayAndClaim(gameA, GameStatus.DEFENDER_WINS, TEE_PROVER); } - /// @notice Game A is created with TEE and challenged with ZK. Another game nullifies the shared TEE verifier. - /// The first `resolve` persists the TEE refutation; after `SLOW_FINALIZATION_DELAY`, A finalizes as - /// challenger wins and the bond goes to the ZK challenger. function testChallengeWinsWhenSharedTeeVerifierNullifiedByOtherGame() public { - currentL2BlockNumber += BLOCK_INTERVAL; - - Claim rootClaimA = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee-challenge-tee-null"))); - bytes memory teeProofA = _generateProof("tee-proof-a", AggregateVerifier.ProofType.TEE); - AggregateVerifier gameA = _createAggregateVerifierGame( - TEE_PROVER, rootClaimA, currentL2BlockNumber, address(anchorStateRegistry), teeProofA + AggregateVerifier gameA = _createGame( + TEE_PROVER, + "tee-challenge-tee-null", + "tee-proof-a", + AggregateVerifier.ProofType.TEE, + address(anchorStateRegistry) ); - Claim rootChallenge = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "zk-challenge"))); - bytes memory zkChallenge = _generateProof("zk-challenge", AggregateVerifier.ProofType.ZK); - vm.prank(ZK_PROVER); - gameA.challenge(zkChallenge, BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1, rootChallenge.raw()); + _challengeWithZk(gameA, "zk-challenge"); + _assertChallengeRecorded(gameA); - assertEq(gameA.proofCount(), 2); - assertGt(gameA.counteredByIntermediateRootIndexPlusOne(), 0); - - currentL2BlockNumber += BLOCK_INTERVAL; - Claim rootClaimB = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee-only-b"))); - bytes memory teeProofB = _generateProof("tee-init-b", AggregateVerifier.ProofType.TEE); AggregateVerifier gameB = - _createAggregateVerifierGame(TEE_PROVER, rootClaimB, currentL2BlockNumber, address(gameA), teeProofB); - - Claim rootNullifyB = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee-nullify-b"))); - bytes memory teeNullifyB = _generateProof("tee-nullify-b", AggregateVerifier.ProofType.TEE); - uint256 lastIdx = BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1; - gameB.nullify(teeNullifyB, lastIdx, rootNullifyB.raw()); + _createGame(TEE_PROVER, "tee-only-b", "tee-init-b", AggregateVerifier.ProofType.TEE, address(gameA)); + _nullify(gameB, AggregateVerifier.ProofType.TEE, "tee-nullify-b"); assertTrue(teeVerifier.nullified()); - assertEq(uint8(gameA.resolve()), uint8(GameStatus.IN_PROGRESS)); + _resolveAndAssertStatus(gameA, GameStatus.IN_PROGRESS); assertEq(gameA.proofCount(), 1); assertGt(gameA.counteredByIntermediateRootIndexPlusOne(), 0); assertEq(address(gameA.teeProver()), address(0)); assertEq(gameA.zkProver(), ZK_PROVER); - vm.warp(block.timestamp + 7 days); - assertEq(uint8(gameA.resolve()), uint8(GameStatus.CHALLENGER_WINS)); - assertEq(gameA.bondRecipient(), ZK_PROVER); + _resolveAfterSlowDelayAndClaim(gameA, GameStatus.CHALLENGER_WINS, ZK_PROVER); + } + + function _createGame( + address prover, + bytes memory claimSalt, + bytes memory proofSalt, + AggregateVerifier.ProofType proofType, + address parent + ) + private + returns (AggregateVerifier) + { + currentL2BlockNumber += BLOCK_INTERVAL; + Claim rootClaim = _claim(claimSalt); + bytes memory proof = _generateProof(proofSalt, proofType); + return _createAggregateVerifierGame(prover, rootClaim, currentL2BlockNumber, parent, proof); + } + + function _challenge(AggregateVerifier game, AggregateVerifier.ProofType proofType, bytes32 claimRoot) private { + game.challenge(_proofOfType(proofType), LAST_INTERMEDIATE_ROOT_INDEX, claimRoot); + } + + function _challengeWithZk(AggregateVerifier game, bytes memory claimSalt) private { + vm.prank(ZK_PROVER); + _challenge(game, AggregateVerifier.ProofType.ZK, _claim(claimSalt).raw()); + } + + function _nullify(AggregateVerifier game, AggregateVerifier.ProofType proofType, bytes memory claimSalt) private { + game.nullify(_proofOfType(proofType), LAST_INTERMEDIATE_ROOT_INDEX, _claim(claimSalt).raw()); + } - uint256 balanceBefore = ZK_PROVER.balance; - gameA.claimCredit(); + function _proofOfType(AggregateVerifier.ProofType proofType) private pure returns (bytes memory) { + return abi.encodePacked(uint8(proofType), bytes1(0)); + } + + function _claim(bytes memory salt) private view returns (Claim) { + return Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, salt))); + } + + function _assertChallengeRecorded(AggregateVerifier game) private view { + assertEq(game.proofCount(), 2); + assertGt(game.counteredByIntermediateRootIndexPlusOne(), 0); + } + + function _resolveAndAssertStatus(AggregateVerifier game, GameStatus expectedStatus) private { + assertEq(uint8(game.resolve()), uint8(expectedStatus)); + } + + function _resolveAfterSlowDelayAndClaim( + AggregateVerifier game, + GameStatus expectedStatus, + address recipient + ) + private + { + vm.warp(block.timestamp + game.SLOW_FINALIZATION_DELAY()); + _resolveAndAssertStatus(game, expectedStatus); + assertEq(game.bondRecipient(), recipient); + _claimCreditAfterDelay(game, recipient); + } + + function _claimCreditAfterDelay(AggregateVerifier game, address recipient) private { + uint256 balanceBefore = recipient.balance; + game.claimCredit(); vm.warp(block.timestamp + DELAYED_WETH_DELAY); - gameA.claimCredit(); - assertEq(ZK_PROVER.balance, balanceBefore + INIT_BOND); - assertEq(delayedWETH.balanceOf(address(gameA)), 0); + game.claimCredit(); + assertEq(recipient.balance, balanceBefore + INIT_BOND); + assertEq(delayedWETH.balanceOf(address(game)), 0); } } diff --git a/test/L1/proofs/DelayedWETH.t.sol b/test/L1/proofs/DelayedWETH.t.sol index 9a3e65cb..13129953 100644 --- a/test/L1/proofs/DelayedWETH.t.sol +++ b/test/L1/proofs/DelayedWETH.t.sol @@ -3,71 +3,44 @@ pragma solidity ^0.8.15; // Testing import { CommonTest } from "test/setup/CommonTest.sol"; +import { GasBurner, Reverter } from "test/mocks/Callers.sol"; // Libraries import { ForgeArtifacts, StorageSlot } from "scripts/libraries/ForgeArtifacts.sol"; -import { Burn } from "src/libraries/Burn.sol"; -import "src/libraries/bridge/Types.sol"; -import "src/libraries/bridge/Errors.sol"; // Interfaces import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; import { IProxyAdminOwnedBase } from "interfaces/L1/IProxyAdminOwnedBase.sol"; import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; -/// @title DelayedWETH_FallbackGasUser_Harness -/// @notice Contract that burns gas in the fallback function. -contract DelayedWETH_FallbackGasUser_Harness { - /// @notice Amount of gas to use in the fallback function. - uint256 public gas; +/// @title DelayedWETH_TestBase +/// @notice Reusable test utilities for `DelayedWETH` tests. +abstract contract DelayedWETH_TestBase is CommonTest { + address internal constant DUMMY_SYSTEM_CONFIG = address(1234); + uint256 internal constant DEFAULT_AMOUNT = 1 ether; - /// @param _gas Amount of gas to use in the fallback function. - constructor(uint256 _gas) { - gas = _gas; - } - - /// @notice Burn gas on fallback; - fallback() external payable { - Burn.gas(gas); - } - - /// @notice Burn gas on receive. - receive() external payable { - Burn.gas(gas); - } -} - -/// @title DelayedWETH_FallbackReverter_Harness -/// @notice Contract that reverts in the fallback function. -contract DelayedWETH_FallbackReverter_Harness { - /// @notice Revert on fallback. - fallback() external payable { - revert("FallbackReverter: revert"); - } - - /// @notice Revert on receive. - receive() external payable { - revert("FallbackReverter: revert"); - } -} - -/// @title DelayedWETH_TestInit -/// @notice Reusable test initialization for `DelayedWETH` tests. -abstract contract DelayedWETH_TestInit is CommonTest { event Approval(address indexed src, address indexed guy, uint256 wad); - event Transfer(address indexed src, address indexed dst, uint256 wad); - event Deposit(address indexed dst, uint256 wad); event Withdrawal(address indexed src, uint256 wad); - event Unwrap(address indexed src, uint256 wad); - function setUp() public virtual override { - super.setUp(); + function _depositAlice(uint256 _amount) internal returns (uint256 balanceAfterDeposit_) { + vm.prank(alice); + delayedWeth.deposit{ value: _amount }(); + balanceAfterDeposit_ = address(alice).balance; } } /// @title DelayedWETH_Initialize_Test /// @notice Tests the `initialize` function of the `DelayedWETH` contract. -contract DelayedWETH_Initialize_Test is DelayedWETH_TestInit { +contract DelayedWETH_Initialize_Test is DelayedWETH_TestBase { + bytes32 internal initializedSlot; + + function setUp() public override { + super.setUp(); + + StorageSlot memory slot = ForgeArtifacts.getSlot("DelayedWETH", "_initialized"); + initializedSlot = bytes32(slot.slot); + } + /// @notice Tests that initialization is successful. function test_initialize_succeeds() public view { assertEq(delayedWeth.proxyAdminOwner(), proxyAdminOwner); @@ -79,14 +52,9 @@ contract DelayedWETH_Initialize_Test is DelayedWETH_TestInit { /// but confirms that the initValue is not incremented incorrectly if an upgrade /// function is not present. function test_initialize_correctInitializerValue_succeeds() public view { - // Get the slot for _initialized. - StorageSlot memory slot = ForgeArtifacts.getSlot("DelayedWETH", "_initialized"); - - // Get the initializer value. - bytes32 slotVal = vm.load(address(delayedWeth), bytes32(slot.slot)); + bytes32 slotVal = vm.load(address(delayedWeth), initializedSlot); uint8 val = uint8(uint256(slotVal) & 0xFF); - // Assert that the initializer value matches the expected value. assertEq(val, delayedWeth.initVersion()); } @@ -94,88 +62,86 @@ contract DelayedWETH_Initialize_Test is DelayedWETH_TestInit { /// owner. /// @param _sender The address of the sender to test. function testFuzz_initialize_notProxyAdminOrProxyAdminOwner_reverts(address _sender) public { - // Prank as the not ProxyAdmin or ProxyAdmin owner. vm.assume(_sender != address(delayedWeth.proxyAdmin()) && _sender != delayedWeth.proxyAdminOwner()); - // Get the slot for _initialized. - StorageSlot memory slot = ForgeArtifacts.getSlot("DelayedWETH", "_initialized"); - - // Set the initialized slot to 0. - vm.store(address(delayedWeth), bytes32(slot.slot), bytes32(0)); + vm.store(address(delayedWeth), initializedSlot, bytes32(0)); - // Expect the revert with `ProxyAdminOwnedBase_NotProxyAdminOrProxyAdminOwner` selector. vm.expectRevert(IProxyAdminOwnedBase.ProxyAdminOwnedBase_NotProxyAdminOrProxyAdminOwner.selector); - - // Call the `initialize` function with the sender. vm.prank(_sender); - delayedWeth.initialize(ISystemConfig(address(1234))); + delayedWeth.initialize(ISystemConfig(DUMMY_SYSTEM_CONFIG)); } } /// @title DelayedWETH_Unlock_Test /// @notice Tests the `unlock` function of the `DelayedWETH` contract. -contract DelayedWETH_Unlock_Test is DelayedWETH_TestInit { +contract DelayedWETH_Unlock_Test is DelayedWETH_TestBase { /// @notice Tests that unlocking once is successful. function test_unlock_once_succeeds() public { - delayedWeth.unlock(alice, 1 ether); + delayedWeth.unlock(alice, DEFAULT_AMOUNT); (uint256 amount, uint256 timestamp) = delayedWeth.withdrawals(address(this), alice); - assertEq(amount, 1 ether); + assertEq(amount, DEFAULT_AMOUNT); assertEq(timestamp, block.timestamp); } /// @notice Tests that unlocking twice is successful and timestamp/amount is updated. function test_unlock_twice_succeeds() public { - // Unlock once. uint256 ts = block.timestamp; - delayedWeth.unlock(alice, 1 ether); + delayedWeth.unlock(alice, DEFAULT_AMOUNT); (uint256 amount1, uint256 timestamp1) = delayedWeth.withdrawals(address(this), alice); - assertEq(amount1, 1 ether); + assertEq(amount1, DEFAULT_AMOUNT); assertEq(timestamp1, ts); - // Go forward in time. vm.warp(ts + 1); - // Unlock again works. - delayedWeth.unlock(alice, 1 ether); + delayedWeth.unlock(alice, DEFAULT_AMOUNT); (uint256 amount2, uint256 timestamp2) = delayedWeth.withdrawals(address(this), alice); - assertEq(amount2, 2 ether); + assertEq(amount2, 2 * DEFAULT_AMOUNT); assertEq(timestamp2, ts + 1); } } /// @title DelayedWETH_Withdraw_Test /// @notice Tests the `withdraw` function of the `DelayedWETH` contract. -contract DelayedWETH_Withdraw_Test is DelayedWETH_TestInit { - /// @notice Tests that withdrawing while unlocked and delay has passed is successful. - function test_withdraw_whileUnlocked_succeeds() public { - // Deposit some WETH. - vm.prank(alice); - delayedWeth.deposit{ value: 1 ether }(); - uint256 balance = address(alice).balance; - - // Unlock the withdrawal. +contract DelayedWETH_Withdraw_Test is DelayedWETH_TestBase { + function _unlockAlice(uint256 _amount) internal { vm.prank(alice); - delayedWeth.unlock(alice, 1 ether); + delayedWeth.unlock(alice, _amount); + } - // Wait for the delay. + function _warpPastDelay() internal { vm.warp(block.timestamp + delayedWeth.delay() + 1); + } + + function _warpBeforeDelay() internal { + vm.warp(block.timestamp + delayedWeth.delay() - 1); + } + + function _pauseSuperchain() internal { + vm.prank(superchainConfig.guardian()); + superchainConfig.pause(address(0)); + } + + function _prepareUnlockedWithdrawal() internal returns (uint256 balanceAfterDeposit_) { + balanceAfterDeposit_ = _depositAlice(DEFAULT_AMOUNT); + _unlockAlice(DEFAULT_AMOUNT); + _warpPastDelay(); + } - // Withdraw the WETH. - vm.expectEmit(true, true, false, false); - emit Withdrawal(address(alice), 1 ether); + /// @notice Tests that withdrawing while unlocked and delay has passed is successful. + function test_withdraw_whileUnlocked_succeeds() public { + uint256 balance = _prepareUnlockedWithdrawal(); + + vm.expectEmit(address(delayedWeth)); + emit Withdrawal(address(alice), DEFAULT_AMOUNT); vm.prank(alice); - delayedWeth.withdraw(1 ether); - assertEq(address(alice).balance, balance + 1 ether); + delayedWeth.withdraw(DEFAULT_AMOUNT); + assertEq(address(alice).balance, balance + DEFAULT_AMOUNT); } /// @notice Tests that withdrawing when unlock was not called fails. function test_withdraw_whileLocked_fails() public { - // Deposit some WETH. - vm.prank(alice); - delayedWeth.deposit{ value: 1 ether }(); - uint256 balance = address(alice).balance; + uint256 balance = _depositAlice(DEFAULT_AMOUNT); - // Withdraw fails when unlock not called. vm.expectRevert("DelayedWETH: withdrawal not unlocked"); vm.prank(alice); delayedWeth.withdraw(0 ether); @@ -184,100 +150,50 @@ contract DelayedWETH_Withdraw_Test is DelayedWETH_TestInit { /// @notice Tests that withdrawing while locked and delay has not passed fails. function test_withdraw_whileLockedNotLongEnough_fails() public { - // Deposit some WETH. - vm.prank(alice); - delayedWeth.deposit{ value: 1 ether }(); - uint256 balance = address(alice).balance; - - // Call unlock. - vm.prank(alice); - delayedWeth.unlock(alice, 1 ether); - - // Wait for the delay, but not long enough. - vm.warp(block.timestamp + delayedWeth.delay() - 1); + uint256 balance = _depositAlice(DEFAULT_AMOUNT); + _unlockAlice(DEFAULT_AMOUNT); + _warpBeforeDelay(); - // Withdraw fails when delay not met. vm.expectRevert("DelayedWETH: withdrawal delay not met"); vm.prank(alice); - delayedWeth.withdraw(1 ether); + delayedWeth.withdraw(DEFAULT_AMOUNT); assertEq(address(alice).balance, balance); } /// @notice Tests that withdrawing more than unlocked amount fails. function test_withdraw_tooMuch_fails() public { - // Deposit some WETH. - vm.prank(alice); - delayedWeth.deposit{ value: 1 ether }(); - uint256 balance = address(alice).balance; - - // Unlock the withdrawal. - vm.prank(alice); - delayedWeth.unlock(alice, 1 ether); + uint256 balance = _prepareUnlockedWithdrawal(); - // Wait for the delay. - vm.warp(block.timestamp + delayedWeth.delay() + 1); - - // Withdraw too much fails. vm.expectRevert("DelayedWETH: insufficient unlocked withdrawal"); vm.prank(alice); - delayedWeth.withdraw(2 ether); + delayedWeth.withdraw(2 * DEFAULT_AMOUNT); assertEq(address(alice).balance, balance); } /// @notice Tests that withdrawing while paused fails. function test_withdraw_whenPaused_fails() public { - // Deposit some WETH. - vm.prank(alice); - delayedWeth.deposit{ value: 1 ether }(); - - // Unlock the withdrawal. - vm.prank(alice); - delayedWeth.unlock(alice, 1 ether); - - // Wait for the delay. - vm.warp(block.timestamp + delayedWeth.delay() + 1); + _pauseSuperchain(); - // Pause the contract. - address guardian = optimismPortal2.guardian(); - vm.prank(guardian); - superchainConfig.pause(address(0)); - - // Withdraw fails. vm.expectRevert("DelayedWETH: contract is paused"); vm.prank(alice); - delayedWeth.withdraw(1 ether); + delayedWeth.withdraw(DEFAULT_AMOUNT); } /// @notice Tests that withdrawing while unlocked and delay has passed is successful. function test_withdraw_withdrawFromWhileUnlocked_succeeds() public { - // Deposit some WETH. - vm.prank(alice); - delayedWeth.deposit{ value: 1 ether }(); - uint256 balance = address(alice).balance; + uint256 balance = _prepareUnlockedWithdrawal(); - // Unlock the withdrawal. + vm.expectEmit(address(delayedWeth)); + emit Withdrawal(address(alice), DEFAULT_AMOUNT); vm.prank(alice); - delayedWeth.unlock(alice, 1 ether); - - // Wait for the delay. - vm.warp(block.timestamp + delayedWeth.delay() + 1); - - // Withdraw the WETH. - vm.expectEmit(true, true, false, false); - emit Withdrawal(address(alice), 1 ether); - vm.prank(alice); - delayedWeth.withdraw(alice, 1 ether); - assertEq(address(alice).balance, balance + 1 ether); + delayedWeth.withdraw(alice, DEFAULT_AMOUNT); + assertEq(address(alice).balance, balance + DEFAULT_AMOUNT); } /// @notice Tests that withdrawing when unlock was not called fails. function test_withdraw_withdrawFromWhileLocked_fails() public { - // Deposit some WETH. - vm.prank(alice); - delayedWeth.deposit{ value: 1 ether }(); - uint256 balance = address(alice).balance; + uint256 balance = _depositAlice(DEFAULT_AMOUNT); - // Withdraw fails when unlock not called. vm.expectRevert("DelayedWETH: withdrawal not unlocked"); vm.prank(alice); delayedWeth.withdraw(alice, 0 ether); @@ -286,210 +202,144 @@ contract DelayedWETH_Withdraw_Test is DelayedWETH_TestInit { /// @notice Tests that withdrawing while locked and delay has not passed fails. function test_withdraw_withdrawFromWhileLockedNotLongEnough_fails() public { - // Deposit some WETH. - vm.prank(alice); - delayedWeth.deposit{ value: 1 ether }(); - uint256 balance = address(alice).balance; - - // Call unlock. - vm.prank(alice); - delayedWeth.unlock(alice, 1 ether); - - // Wait for the delay, but not long enough. - vm.warp(block.timestamp + delayedWeth.delay() - 1); + uint256 balance = _depositAlice(DEFAULT_AMOUNT); + _unlockAlice(DEFAULT_AMOUNT); + _warpBeforeDelay(); - // Withdraw fails when delay not met. vm.expectRevert("DelayedWETH: withdrawal delay not met"); vm.prank(alice); - delayedWeth.withdraw(alice, 1 ether); + delayedWeth.withdraw(alice, DEFAULT_AMOUNT); assertEq(address(alice).balance, balance); } /// @notice Tests that withdrawing more than unlocked amount fails. function test_withdraw_withdrawFromTooMuch_fails() public { - // Deposit some WETH. - vm.prank(alice); - delayedWeth.deposit{ value: 1 ether }(); - uint256 balance = address(alice).balance; - - // Unlock the withdrawal. - vm.prank(alice); - delayedWeth.unlock(alice, 1 ether); - - // Wait for the delay. - vm.warp(block.timestamp + delayedWeth.delay() + 1); + uint256 balance = _prepareUnlockedWithdrawal(); - // Withdraw too much fails. vm.expectRevert("DelayedWETH: insufficient unlocked withdrawal"); vm.prank(alice); - delayedWeth.withdraw(alice, 2 ether); + delayedWeth.withdraw(alice, 2 * DEFAULT_AMOUNT); assertEq(address(alice).balance, balance); } /// @notice Tests that withdrawing while paused fails. function test_withdraw_withdrawFromWhenPaused_fails() public { - // Deposit some WETH. - vm.prank(alice); - delayedWeth.deposit{ value: 1 ether }(); - - // Unlock the withdrawal. - vm.prank(alice); - delayedWeth.unlock(alice, 1 ether); - - // Wait for the delay. - vm.warp(block.timestamp + delayedWeth.delay() + 1); - - // Pause the contract. - address guardian = optimismPortal2.guardian(); - vm.prank(guardian); - superchainConfig.pause(address(0)); + _pauseSuperchain(); - // Withdraw fails. vm.expectRevert("DelayedWETH: contract is paused"); vm.prank(alice); - delayedWeth.withdraw(alice, 1 ether); + delayedWeth.withdraw(alice, DEFAULT_AMOUNT); } } /// @title DelayedWETH_Recover_Test /// @notice Tests the `recover` function of the `DelayedWETH` contract. -contract DelayedWETH_Recover_Test is DelayedWETH_TestInit { - /// @notice Tests that recovering WETH succeeds. Makes sure that doing so succeeds with any - /// amount of ETH in the contract and any amount of gas used in the fallback function - /// up to a maximum of 20,000,000 gas. Owner contract should never be using that much - /// gas but we might as well set a very large upper bound for ourselves. - /// @param _amount Amount of WETH to recover. - /// @param _fallbackGasUsage Amount of gas to use in the fallback function. - function testFuzz_recover_succeeds(uint256 _amount, uint256 _fallbackGasUsage) public { - // Assume - _fallbackGasUsage = bound(_fallbackGasUsage, 0, 20000000); +contract DelayedWETH_Recover_Test is DelayedWETH_TestBase { + uint256 internal constant MAX_FALLBACK_GAS_USAGE = 20_000_000; + + function _mockProxyAdminOwner(address _owner) internal { + vm.mockCall(address(proxyAdmin), abi.encodeCall(IProxyAdmin.owner, ()), abi.encode(_owner)); + } - // Set up the gas burner. - DelayedWETH_FallbackGasUser_Harness gasUser = new DelayedWETH_FallbackGasUser_Harness(_fallbackGasUsage); + function _recoverToGasBurner(uint256 _amount, uint256 _fallbackGasUsage) internal { + GasBurner gasUser = new GasBurner(_fallbackGasUsage + 500); - // Mock owner to return the gas user. - vm.mockCall(address(proxyAdmin), abi.encodeCall(IProxyAdmin.owner, ()), abi.encode(address(gasUser))); + _mockProxyAdminOwner(address(gasUser)); - // Give the contract some WETH to recover. vm.deal(address(delayedWeth), _amount); - // Record the initial balance. uint256 initialBalance = address(gasUser).balance; - // Recover the WETH. vm.prank(address(gasUser)); delayedWeth.recover(_amount); - // Verify the WETH was recovered. assertEq(address(delayedWeth).balance, 0); assertEq(address(gasUser).balance, initialBalance + _amount); } + /// @notice Tests that recovering WETH succeeds. Makes sure that doing so succeeds with any + /// amount of ETH in the contract. + /// @param _amount Amount of WETH to recover. + function testFuzz_recover_succeeds(uint256 _amount) public { + _recoverToGasBurner(_amount, 0); + } + + /// @notice Tests that recovering WETH succeeds when the recipient uses the maximum allowed fallback gas. + function test_recover_withMaxFallbackGas_succeeds() public { + _recoverToGasBurner(DEFAULT_AMOUNT, MAX_FALLBACK_GAS_USAGE); + } + /// @notice Tests that recovering WETH by non-owner fails. function test_recover_byNonOwner_fails() public { - // Pretend to be a non-owner. vm.prank(alice); - // Recover fails. vm.expectRevert("DelayedWETH: not owner"); - delayedWeth.recover(1 ether); + delayedWeth.recover(DEFAULT_AMOUNT); } /// @notice Tests that recovering more than the balance recovers what it can. function test_recover_moreThanBalance_succeeds() public { - // Mock owner to return alice. - vm.mockCall(address(proxyAdmin), abi.encodeCall(IProxyAdmin.owner, ()), abi.encode(alice)); + _mockProxyAdminOwner(alice); - // Give the contract some WETH to recover. vm.deal(address(delayedWeth), 0.5 ether); - // Record the initial balance. uint256 initialBalance = address(alice).balance; - // Recover the WETH. vm.prank(alice); - delayedWeth.recover(1 ether); + delayedWeth.recover(DEFAULT_AMOUNT); - // Verify the WETH was recovered. assertEq(address(delayedWeth).balance, 0); assertEq(address(alice).balance, initialBalance + 0.5 ether); } /// @notice Tests that recover reverts when recipient reverts. function test_recover_whenRecipientReverts_fails() public { - // Set up the reverter. - DelayedWETH_FallbackReverter_Harness reverter = new DelayedWETH_FallbackReverter_Harness(); + Reverter reverter = new Reverter(); - // Mock owner to return the reverter. - vm.mockCall(address(proxyAdmin), abi.encodeCall(IProxyAdmin.owner, ()), abi.encode(address(reverter))); + _mockProxyAdminOwner(address(reverter)); - // Give the contract some WETH to recover. - vm.deal(address(delayedWeth), 1 ether); + vm.deal(address(delayedWeth), DEFAULT_AMOUNT); - // Recover fails. vm.expectRevert("DelayedWETH: recover failed"); vm.prank(address(reverter)); - delayedWeth.recover(1 ether); + delayedWeth.recover(DEFAULT_AMOUNT); } } /// @title DelayedWETH_Hold_Test /// @notice Tests the `hold` function of the `DelayedWETH` contract. -contract DelayedWETH_Hold_Test is DelayedWETH_TestInit { +contract DelayedWETH_Hold_Test is DelayedWETH_TestBase { /// @notice Tests that holding WETH succeeds. function test_hold_byOwner_succeeds() public { - uint256 amount = 1 ether; - - // Pretend to be alice and deposit some WETH. - vm.prank(alice); - delayedWeth.deposit{ value: amount }(); - - // Get our balance before. + _depositAlice(DEFAULT_AMOUNT); uint256 initialBalance = delayedWeth.balanceOf(address(proxyAdminOwner)); - // Hold some WETH. - vm.expectEmit(true, true, true, false); - emit Approval(alice, address(proxyAdminOwner), amount); + vm.expectEmit(address(delayedWeth)); + emit Approval(alice, address(proxyAdminOwner), DEFAULT_AMOUNT); vm.prank(proxyAdminOwner); - delayedWeth.hold(alice, amount); - - // Get our balance after. - uint256 finalBalance = delayedWeth.balanceOf(address(proxyAdminOwner)); + delayedWeth.hold(alice, DEFAULT_AMOUNT); - // Verify the transfer. - assertEq(finalBalance, initialBalance + amount); + assertEq(delayedWeth.balanceOf(address(proxyAdminOwner)), initialBalance + DEFAULT_AMOUNT); } + /// @notice Tests that holding all WETH succeeds when the amount is omitted. function test_hold_withoutAmount_succeeds() public { - uint256 amount = 1 ether; - - // Pretend to be alice and deposit some WETH. - vm.prank(alice); - delayedWeth.deposit{ value: amount }(); - - // Get our balance before. + _depositAlice(DEFAULT_AMOUNT); uint256 initialBalance = delayedWeth.balanceOf(address(proxyAdminOwner)); - // Hold some WETH. - vm.expectEmit(true, true, true, false); - emit Approval(alice, address(proxyAdminOwner), amount); + vm.expectEmit(address(delayedWeth)); + emit Approval(alice, address(proxyAdminOwner), DEFAULT_AMOUNT); vm.prank(proxyAdminOwner); - delayedWeth.hold(alice); // without amount parameter - - // Get our balance after. - uint256 finalBalance = delayedWeth.balanceOf(address(proxyAdminOwner)); + delayedWeth.hold(alice); - // Verify the transfer. - assertEq(finalBalance, initialBalance + amount); + assertEq(delayedWeth.balanceOf(address(proxyAdminOwner)), initialBalance + DEFAULT_AMOUNT); } /// @notice Tests that holding WETH by non-owner fails. function test_hold_byNonOwner_fails() public { - // Pretend to be a non-owner. vm.prank(alice); - // Hold fails. vm.expectRevert("DelayedWETH: not owner"); - delayedWeth.hold(bob, 1 ether); + delayedWeth.hold(bob, DEFAULT_AMOUNT); } } diff --git a/test/L1/proofs/DisputeGameFactory.t.sol b/test/L1/proofs/DisputeGameFactory.t.sol index dfd9a233..6cab78c2 100644 --- a/test/L1/proofs/DisputeGameFactory.t.sol +++ b/test/L1/proofs/DisputeGameFactory.t.sol @@ -8,9 +8,8 @@ import { CommonTest } from "test/setup/CommonTest.sol"; import { ForgeArtifacts, StorageSlot } from "scripts/libraries/ForgeArtifacts.sol"; // Libraries -import { Timestamp } from "src/libraries/bridge/LibUDT.sol"; +import { Claim, Timestamp } from "src/libraries/bridge/LibUDT.sol"; import "src/libraries/bridge/Types.sol"; -import "src/libraries/bridge/Errors.sol"; import { AggregateVerifier } from "src/L1/proofs/AggregateVerifier.sol"; // Interfaces @@ -44,6 +43,13 @@ contract DisputeGameFactory_FakeClone_Harness { /// @title DisputeGameFactory_TestInit /// @notice Reusable test initialization for `DisputeGameFactory` tests. abstract contract DisputeGameFactory_TestInit is CommonTest { + uint256 internal constant DEFAULT_INIT_BOND = 0.08 ether; + uint256 internal constant L2_CHAIN_ID = 111; + uint256 internal constant AGGREGATE_BLOCK_INTERVAL = 100; + uint256 internal constant AGGREGATE_INTERMEDIATE_BLOCK_INTERVAL = 10; + uint32 internal constant MAX_GAME_TYPE = 8; + address internal constant NON_OWNER = address(0xBEEF); + DisputeGameFactory_FakeClone_Harness fakeClone; event DisputeGameCreated(address indexed disputeProxy, GameType indexed gameType, Claim indexed rootClaim); @@ -51,8 +57,6 @@ abstract contract DisputeGameFactory_TestInit is CommonTest { event ImplementationArgsSet(GameType indexed gameType, bytes args); event InitBondUpdated(GameType indexed gameType, uint256 indexed newBond); - uint256 l2ChainId = 111; - function setUp() public virtual override { super.setUp(); fakeClone = new DisputeGameFactory_FakeClone_Harness(); @@ -63,47 +67,64 @@ abstract contract DisputeGameFactory_TestInit is CommonTest { } function _setGame(address _gameImpl, GameType _gameType) internal { - _setGame(_gameImpl, _gameType, false, ""); + _setGame(_gameImpl, _gameType, DEFAULT_INIT_BOND); } - function _setGame(address _gameImpl, GameType _gameType, bytes memory _implArgs) internal { - _setGame(_gameImpl, _gameType, true, _implArgs); - } - - function _setGame(address _gameImpl, GameType _gameType, bool _hasImplArgs, bytes memory _implArgs) internal { + function _setGame(address _gameImpl, GameType _gameType, uint256 _initBond) internal { vm.startPrank(disputeGameFactory.owner()); - if (_hasImplArgs) { - disputeGameFactory.setImplementation(_gameType, IDisputeGame(_gameImpl), _implArgs); - } else { - disputeGameFactory.setImplementation(_gameType, IDisputeGame(_gameImpl)); - } - disputeGameFactory.setInitBond(_gameType, 0.08 ether); + disputeGameFactory.setImplementation(_gameType, IDisputeGame(_gameImpl)); + disputeGameFactory.setInitBond(_gameType, _initBond); vm.stopPrank(); } + + function _expectNonOwnerRevert() internal { + vm.prank(NON_OWNER); + vm.expectRevert("Ownable: caller is not the owner"); + } + + function _assertCreatedGame( + GameType _gameType, + Claim _rootClaim, + bytes memory _extraData, + IDisputeGame _proxy, + uint256 _index + ) + internal + view + { + (IDisputeGame game, Timestamp timestamp) = disputeGameFactory.games(_gameType, _rootClaim, _extraData); + + assertEq(address(game), address(_proxy)); + assertEq(Timestamp.unwrap(timestamp), block.timestamp); + assertEq(disputeGameFactory.gameCount(), _index + 1); + + (GameType gameType, Timestamp timestampAtIndex, IDisputeGame gameAtIndex) = + disputeGameFactory.gameAtIndex(_index); + assertEq(GameType.unwrap(gameType), GameType.unwrap(_gameType)); + assertEq(address(gameAtIndex), address(_proxy)); + assertEq(Timestamp.unwrap(timestampAtIndex), block.timestamp); + } } /// @title DisputeGameFactory_Initialize_Test /// @notice Tests the `initialize` function of the `DisputeGameFactory` contract. contract DisputeGameFactory_Initialize_Test is DisputeGameFactory_TestInit { + function _initializedSlot() internal view returns (bytes32) { + StorageSlot memory slot = ForgeArtifacts.getSlot("DisputeGameFactory", "_initialized"); + return bytes32(slot.slot); + } + /// @notice Tests that initialization reverts if called by a non-proxy admin or proxy admin /// owner. /// @param _sender The address of the sender to test. function testFuzz_initialize_notProxyAdminOrProxyAdminOwner_reverts(address _sender) public { - // Prank as the not ProxyAdmin or ProxyAdmin owner. vm.assume( _sender != address(disputeGameFactory.proxyAdmin()) && _sender != disputeGameFactory.proxyAdminOwner() ); - // Get the slot for _initialized. - StorageSlot memory slot = ForgeArtifacts.getSlot("DisputeGameFactory", "_initialized"); - - // Set the initialized slot to 0. - vm.store(address(disputeGameFactory), bytes32(slot.slot), bytes32(0)); + vm.store(address(disputeGameFactory), _initializedSlot(), bytes32(0)); - // Expect the revert with `ProxyAdminOwnedBase_NotProxyAdminOrProxyAdminOwner` selector. vm.expectRevert(IProxyAdminOwnedBase.ProxyAdminOwnedBase_NotProxyAdminOrProxyAdminOwner.selector); - - // Call the `initialize` function with the sender. vm.prank(_sender); disputeGameFactory.initialize(address(1234)); } @@ -112,14 +133,9 @@ contract DisputeGameFactory_Initialize_Test is DisputeGameFactory_TestInit { /// but confirms that the initValue is not incremented incorrectly if an upgrade /// function is not present. function test_initialize_correctInitializerValue_succeeds() public view { - // Get the slot for _initialized. - StorageSlot memory slot = ForgeArtifacts.getSlot("DisputeGameFactory", "_initialized"); - - // Get the initializer value. - bytes32 slotVal = vm.load(address(disputeGameFactory), bytes32(slot.slot)); + bytes32 slotVal = vm.load(address(disputeGameFactory), _initializedSlot()); uint8 val = uint8(uint256(slotVal) & 0xFF); - // Assert that the initializer value matches the expected value. assertEq(val, disputeGameFactory.initVersion()); } } @@ -137,17 +153,8 @@ contract DisputeGameFactory_Create_Test is DisputeGameFactory_TestInit { ) public { - // Ensure that the `gameType` is within the bounds of the `GameType` enum's possible - // values. - uint32 maxGameType = 8; - GameType gt = GameType.wrap(uint8(bound(gameType, 0, maxGameType))); - - // Set all three implementations to the same `FakeClone` contract. - for (uint8 i; i < maxGameType + 1; i++) { - GameType lgt = GameType.wrap(i); - disputeGameFactory.setImplementation(lgt, IDisputeGame(address(fakeClone))); - disputeGameFactory.setInitBond(lgt, _value); - } + GameType gt = GameType.wrap(uint8(bound(gameType, 0, MAX_GAME_TYPE))); + _setGame(address(fakeClone), gt, _value); vm.deal(address(this), _value); @@ -157,18 +164,7 @@ contract DisputeGameFactory_Create_Test is DisputeGameFactory_TestInit { emit DisputeGameCreated(address(0), gt, rootClaim); IDisputeGame proxy = disputeGameFactory.create{ value: _value }(gt, rootClaim, extraData); - (IDisputeGame game, Timestamp timestamp) = disputeGameFactory.games(gt, rootClaim, extraData); - - // Ensure that the dispute game was assigned to the `disputeGames` mapping. - assertEq(address(game), address(proxy)); - assertEq(Timestamp.unwrap(timestamp), block.timestamp); - assertEq(disputeGameFactory.gameCount(), gameCountBefore + 1); - - (, Timestamp timestamp2, IDisputeGame game2) = disputeGameFactory.gameAtIndex(gameCountBefore); - assertEq(address(game2), address(proxy)); - assertEq(Timestamp.unwrap(timestamp2), block.timestamp); - - // Ensure that the game proxy received the bonded ETH. + _assertCreatedGame(gt, rootClaim, extraData, proxy, gameCountBefore); assertEq(address(proxy).balance, _value); } @@ -181,63 +177,41 @@ contract DisputeGameFactory_Create_Test is DisputeGameFactory_TestInit { ) public { - // Ensure that the `gameType` is within the bounds of the `GameType` enum's possible - // values. GameType gt = GameType.wrap(uint8(bound(gameType, 0, 2))); + _setGame(address(fakeClone), gt, 1 ether); - // Set all three implementations to the same `FakeClone` contract. - for (uint8 i; i < 3; i++) { - GameType lgt = GameType.wrap(i); - disputeGameFactory.setImplementation(lgt, IDisputeGame(address(fakeClone))); - disputeGameFactory.setInitBond(lgt, 1 ether); - } - - vm.expectRevert(IncorrectBondAmount.selector); + vm.expectRevert(IDisputeGameFactory.IncorrectBondAmount.selector); disputeGameFactory.create(gt, rootClaim, extraData); } /// @notice Tests that the `create` function reverts when there is no implementation set for /// the given `GameType`. function testFuzz_create_noImpl_reverts(uint32 gameType, Claim rootClaim, bytes calldata extraData) public { - // Ensure that the `gameType` is within the bounds of the `GameType` enum's possible - // values. We skip over game type = 0, since the deploy script set the implementation for - // that game type. - uint32 maxGameType = 8; - GameType gt = GameType.wrap(uint32(bound(gameType, maxGameType + 1, type(uint32).max))); + GameType gt = GameType.wrap(gameType); + vm.assume(address(disputeGameFactory.gameImpls(gt)) == address(0)); - vm.expectRevert(abi.encodeWithSelector(NoImplementation.selector, gt)); + vm.expectRevert(abi.encodeWithSelector(IDisputeGameFactory.NoImplementation.selector, gt)); disputeGameFactory.create(gt, rootClaim, extraData); } /// @notice Tests that the `create` function reverts when there exists a dispute game with the /// same UUID. function testFuzz_create_sameUUID_reverts(uint32 gameType, Claim rootClaim, bytes calldata extraData) public { - // Ensure that the `gameType` is within the bounds of the `GameType` enum's possible - // values. - uint32 maxGameType = 8; - GameType gt = GameType.wrap(uint8(bound(gameType, 0, maxGameType))); - - // Set all three implementations to the same `FakeClone` contract. - for (uint8 i; i < maxGameType + 1; i++) { - disputeGameFactory.setImplementation(GameType.wrap(i), IDisputeGame(address(fakeClone))); - } + GameType gt = GameType.wrap(uint8(bound(gameType, 0, MAX_GAME_TYPE))); + disputeGameFactory.setImplementation(gt, IDisputeGame(address(fakeClone))); uint256 bondAmount = disputeGameFactory.initBonds(gt); - // Create our first dispute game - this should succeed. vm.expectEmit(false, true, true, false); emit DisputeGameCreated(address(0), gt, rootClaim); + uint256 gameCountBefore = disputeGameFactory.gameCount(); IDisputeGame proxy = disputeGameFactory.create{ value: bondAmount }(gt, rootClaim, extraData); + _assertCreatedGame(gt, rootClaim, extraData, proxy, gameCountBefore); - (IDisputeGame game, Timestamp timestamp) = disputeGameFactory.games(gt, rootClaim, extraData); - // Ensure that the dispute game was assigned to the `disputeGames` mapping. - assertEq(address(game), address(proxy)); - assertEq(Timestamp.unwrap(timestamp), block.timestamp); - - // Ensure that the `create` function reverts when called with parameters that would result - // in the same UUID. vm.expectRevert( - abi.encodeWithSelector(GameAlreadyExists.selector, disputeGameFactory.getGameUUID(gt, rootClaim, extraData)) + abi.encodeWithSelector( + IDisputeGameFactory.GameAlreadyExists.selector, disputeGameFactory.getGameUUID(gt, rootClaim, extraData) + ) ); disputeGameFactory.create{ value: bondAmount }(gt, rootClaim, extraData); } @@ -254,22 +228,26 @@ contract DisputeGameFactory_Create_Test is DisputeGameFactory_TestInit { bytes32(uint256(1)), AggregateVerifier.ZkHashes(bytes32(uint256(2)), bytes32(uint256(3))), bytes32(uint256(4)), - l2ChainId, - 100, - 10 + L2_CHAIN_ID, + AGGREGATE_BLOCK_INTERVAL, + AGGREGATE_INTERMEDIATE_BLOCK_INTERVAL ); _setGame(address(gameImpl), GameTypes.AGGREGATE_VERIFIER); Claim rootClaim = Claim.wrap(bytes32(hex"beef")); Proposal memory startingRoot = anchorStateRegistry.getStartingAnchorRoot(); bytes memory intermediateRoots; - for (uint256 i = 1; i < gameImpl.intermediateOutputRootsCount(); i++) { - intermediateRoots = - abi.encodePacked(intermediateRoots, keccak256(abi.encode(startingRoot.l2SequenceNumber + 10 * i))); + uint256 intermediateRootsCount = gameImpl.intermediateOutputRootsCount(); + for (uint256 i = 1; i < intermediateRootsCount; i++) { + intermediateRoots = abi.encodePacked( + intermediateRoots, + keccak256(abi.encode(startingRoot.l2SequenceNumber + AGGREGATE_INTERMEDIATE_BLOCK_INTERVAL * i)) + ); } intermediateRoots = abi.encodePacked(intermediateRoots, rootClaim.raw()); - bytes memory extraData = - abi.encodePacked(startingRoot.l2SequenceNumber + 100, address(anchorStateRegistry), intermediateRoots); + bytes memory extraData = abi.encodePacked( + startingRoot.l2SequenceNumber + AGGREGATE_BLOCK_INTERVAL, address(anchorStateRegistry), intermediateRoots + ); bytes memory proof = abi.encodePacked( uint8(AggregateVerifier.ProofType.TEE), blockhash(block.number - 1), block.number - 1, bytes32(0) ); @@ -277,34 +255,26 @@ contract DisputeGameFactory_Create_Test is DisputeGameFactory_TestInit { uint256 bondAmount = disputeGameFactory.initBonds(GameTypes.AGGREGATE_VERIFIER); vm.deal(address(this), bondAmount); - // Create the game + uint256 gameCountBefore = disputeGameFactory.gameCount(); IDisputeGame proxy = disputeGameFactory.createWithInitData{ value: bondAmount }( GameTypes.AGGREGATE_VERIFIER, rootClaim, extraData, proof ); - // Verify the game was created and stored - (IDisputeGame game, Timestamp timestamp) = - disputeGameFactory.games(GameTypes.AGGREGATE_VERIFIER, rootClaim, extraData); + _assertCreatedGame(GameTypes.AGGREGATE_VERIFIER, rootClaim, extraData, proxy, gameCountBefore); - assertEq(address(game), address(proxy)); - assertEq(Timestamp.unwrap(timestamp), block.timestamp); - - // Verify the game has the correct parameters via CWIA AggregateVerifier gameV2 = AggregateVerifier(address(proxy)); - // Test CWIA getters assertEq(Claim.unwrap(gameV2.rootClaim()), Claim.unwrap(rootClaim)); assertEq(gameV2.extraData(), extraData); - assertEq(gameV2.L2_CHAIN_ID(), l2ChainId); + assertEq(gameV2.L2_CHAIN_ID(), L2_CHAIN_ID); assertEq(address(gameV2.gameCreator()), address(this)); - assertEq(gameV2.l2SequenceNumber(), startingRoot.l2SequenceNumber + 100); + assertEq(gameV2.l2SequenceNumber(), startingRoot.l2SequenceNumber + AGGREGATE_BLOCK_INTERVAL); assertEq(gameV2.parentAddress(), address(anchorStateRegistry)); assertEq(address(gameV2.DELAYED_WETH()), address(delayedWeth)); assertEq(address(gameV2.anchorStateRegistry()), address(anchorStateRegistry)); - // Test Constructor args assertEq(GameType.unwrap(gameV2.gameType()), GameType.unwrap(GameTypes.AGGREGATE_VERIFIER)); - assertEq(gameV2.BLOCK_INTERVAL(), 100); - assertEq(gameV2.INTERMEDIATE_BLOCK_INTERVAL(), 10); + assertEq(gameV2.BLOCK_INTERVAL(), AGGREGATE_BLOCK_INTERVAL); + assertEq(gameV2.INTERMEDIATE_BLOCK_INTERVAL(), AGGREGATE_INTERMEDIATE_BLOCK_INTERVAL); } } @@ -317,45 +287,38 @@ contract DisputeGameFactory_SetImplementation_Test is DisputeGameFactory_TestIni vm.expectEmit(true, true, true, true, address(disputeGameFactory)); emit ImplementationSet(address(1), GameTypes.AGGREGATE_VERIFIER); - // Set the implementation for the `GameTypes.AGGREGATE_VERIFIER` enum value. disputeGameFactory.setImplementation(GameTypes.AGGREGATE_VERIFIER, IDisputeGame(address(1))); - // Ensure that the implementation for the `GameTypes.AGGREGATE_VERIFIER` enum value is set. assertEq(address(disputeGameFactory.gameImpls(GameTypes.AGGREGATE_VERIFIER)), address(1)); } /// @notice Tests that the `setImplementation` function reverts when called by a non-owner. function test_setImplementation_notOwner_reverts() public { - // Ensure that the `setImplementation` function reverts when called by a non-owner. - vm.prank(address(0)); - vm.expectRevert("Ownable: caller is not the owner"); + _expectNonOwnerRevert(); disputeGameFactory.setImplementation(GameTypes.AGGREGATE_VERIFIER, IDisputeGame(address(1))); } /// @notice Tests that the `setImplementation` function with args properly sets the implementation /// and args for a given `GameType`. function test_setImplementation_withArgs_succeeds() public { - address fakeGame = address(1); + address gameImpl = address(1); bytes memory args = abi.encodePacked( bytes32(hex"dead"), // 32 bytes address(0xbeef), // 20 bytes anchorStateRegistry, // 20 bytes delayedWeth, // 20 bytes - l2ChainId // 32 bytes (l2ChainId) + L2_CHAIN_ID // 32 bytes ); vm.expectEmit(true, true, true, true, address(disputeGameFactory)); - emit ImplementationSet(address(1), GameTypes.AGGREGATE_VERIFIER); + emit ImplementationSet(gameImpl, GameTypes.AGGREGATE_VERIFIER); vm.expectEmit(true, true, true, true, address(disputeGameFactory)); emit ImplementationArgsSet(GameTypes.AGGREGATE_VERIFIER, args); - // Set the implementation and args for the `GameTypes.AGGREGATE_VERIFIER` enum value. - disputeGameFactory.setImplementation(GameTypes.AGGREGATE_VERIFIER, IDisputeGame(fakeGame), args); + disputeGameFactory.setImplementation(GameTypes.AGGREGATE_VERIFIER, IDisputeGame(gameImpl), args); - // Ensure that the implementation for the `GameTypes.AGGREGATE_VERIFIER` enum value is set. - assertEq(address(disputeGameFactory.gameImpls(GameTypes.AGGREGATE_VERIFIER)), address(1)); - // Ensure that the args for the `GameTypes.AGGREGATE_VERIFIER` enum value are set. + assertEq(address(disputeGameFactory.gameImpls(GameTypes.AGGREGATE_VERIFIER)), gameImpl); assertEq(disputeGameFactory.gameArgs(GameTypes.AGGREGATE_VERIFIER), args); } @@ -363,9 +326,7 @@ contract DisputeGameFactory_SetImplementation_Test is DisputeGameFactory_TestIni function test_setImplementationArgs_notOwner_reverts() public { bytes memory args = abi.encode(uint256(123), address(0xdead)); - // Ensure that the `setImplementation` function reverts when called by a non-owner. - vm.prank(address(0)); - vm.expectRevert("Ownable: caller is not the owner"); + _expectNonOwnerRevert(); disputeGameFactory.setImplementation(GameTypes.AGGREGATE_VERIFIER, IDisputeGame(address(1)), args); } } @@ -379,27 +340,21 @@ contract DisputeGameFactory_SetInitBond_Test is DisputeGameFactory_TestInit { vm.expectEmit(true, true, true, true, address(disputeGameFactory)); emit InitBondUpdated(GameTypes.AGGREGATE_VERIFIER, 1 ether); - // Set the init bond for the `GameTypes.AGGREGATE_VERIFIER` enum value. disputeGameFactory.setInitBond(GameTypes.AGGREGATE_VERIFIER, 1 ether); - // Ensure that the init bond for the `GameTypes.AGGREGATE_VERIFIER` enum value is set. assertEq(disputeGameFactory.initBonds(GameTypes.AGGREGATE_VERIFIER), 1 ether); vm.expectEmit(true, true, true, true, address(disputeGameFactory)); emit InitBondUpdated(GameTypes.AGGREGATE_VERIFIER, 2 ether); - // Set the init bond for the `GameTypes.AGGREGATE_VERIFIER` enum value. disputeGameFactory.setInitBond(GameTypes.AGGREGATE_VERIFIER, 2 ether); - // Ensure that the init bond for the `GameTypes.AGGREGATE_VERIFIER` enum value is set. assertEq(disputeGameFactory.initBonds(GameTypes.AGGREGATE_VERIFIER), 2 ether); } /// @notice Tests that the `setInitBond` function reverts when called by a non-owner. function test_setInitBond_notOwner_reverts() public { - // Ensure that the `setInitBond` function reverts when called by a non-owner. - vm.prank(address(0)); - vm.expectRevert("Ownable: caller is not the owner"); + _expectNonOwnerRevert(); disputeGameFactory.setInitBond(GameTypes.AGGREGATE_VERIFIER, 1 ether); } } @@ -410,8 +365,6 @@ contract DisputeGameFactory_GetGameUUID_Test is DisputeGameFactory_TestInit { /// @notice Tests that the `getGameUUID` function returns the correct hash when comparing /// against the keccak256 hash of the abi-encoded parameters. function testDiff_getGameUUID_succeeds(uint32 gameType, Claim rootClaim, bytes calldata extraData) public view { - // Ensure that the `gameType` is within the bounds of the `GameType` enum's possible - // values. GameType gt = GameType.wrap(uint8(bound(gameType, 0, 2))); assertEq( @@ -424,33 +377,23 @@ contract DisputeGameFactory_GetGameUUID_Test is DisputeGameFactory_TestInit { /// @title DisputeGameFactory_FindLatestGames_Test /// @notice Tests the `findLatestGames` function of the `DisputeGameFactory` contract. contract DisputeGameFactory_FindLatestGames_Test is DisputeGameFactory_TestInit { + GameType internal constant FIND_LATEST_SEARCH_GAME_TYPE = GameType.wrap(type(uint32).max); + GameType internal constant FIND_LATEST_OTHER_GAME_TYPE = GameType.wrap(type(uint32).max - 1); + function setUp() public override { super.setUp(); - // Set three implementations to the same `FakeClone` contract. for (uint8 i; i < 3; i++) { - GameType lgt = GameType.wrap(i); - disputeGameFactory.setImplementation(lgt, IDisputeGame(address(fakeClone))); - disputeGameFactory.setInitBond(lgt, 0); + _setGame(address(fakeClone), GameType.wrap(i), 0); } } /// @notice Tests that `findLatestGames` returns an empty array when the passed starting index /// is greater than or equal to the game count. - function testFuzz_findLatestGames_greaterThanLength_succeeds(uint256 _start) public { - // Creation count should be 32 for normal tests, 5 for upgrade tests. - uint256 creationCount = isForkTest() ? 5 : 32; - - // Create some dispute games of varying game types. - for (uint256 i; i < creationCount; i++) { - disputeGameFactory.create(GameType.wrap(uint8(i % 2)), Claim.wrap(bytes32(i)), abi.encode(i)); - } - - // Bound the starting index to a number greater than the length of the game list. + function testFuzz_findLatestGames_greaterThanLength_succeeds(uint256 _start) public view { uint256 gameCount = disputeGameFactory.gameCount(); _start = bound(_start, gameCount, type(uint256).max); - // The array's length should always be 0. IDisputeGameFactory.GameSearchResult[] memory games = disputeGameFactory.findLatestGames(GameTypes.AGGREGATE_VERIFIER, _start, 1); assertEq(games.length, 0); @@ -458,84 +401,42 @@ contract DisputeGameFactory_FindLatestGames_Test is DisputeGameFactory_TestInit /// @notice Tests that `findLatestGames` returns the correct games. function test_findLatestGames_static_succeeds() public { - // Creation count should be 32 for normal tests, 5 for upgrade tests. - uint256 creationCount = isForkTest() ? 5 : 32; - - // Create some dispute games of varying game types, repeatedly iterating over the game - // types 0, 1, 2. - for (uint256 i; i < creationCount; i++) { + for (uint256 i; i < 5; i++) { disputeGameFactory.create(GameType.wrap(uint8(i % 3)), Claim.wrap(bytes32(i)), abi.encode(i)); } uint256 gameCount = disputeGameFactory.gameCount(); - IDisputeGameFactory.GameSearchResult[] memory games; - uint256 start = gameCount - 1; - // Find type 1 games. - games = disputeGameFactory.findLatestGames(GameType.wrap(1), start, 1); - assertEq(games.length, 1); - - // The type 1 game should be the last one added. - assertEq(games[0].index, start); - (GameType gameType, Timestamp createdAt, address game) = games[0].metadata.unpack(); - assertEq(gameType.raw(), 1); - assertEq(createdAt.raw(), block.timestamp); - - // Find type 0 games. - games = disputeGameFactory.findLatestGames(GameType.wrap(0), start, 1); - assertEq(games.length, 1); - - // The type 0 game should be the second to last one added. - assertEq(games[0].index, start - 1); - (gameType, createdAt, game) = games[0].metadata.unpack(); - assertEq(gameType.raw(), 0); - assertEq(createdAt.raw(), block.timestamp); - - // Find type 2 games. - games = disputeGameFactory.findLatestGames(GameType.wrap(2), start, 1); - assertEq(games.length, 1); - - // The type 2 game should be the third to last one added. - assertEq(games[0].index, start - 2); - (gameType, createdAt, game) = games[0].metadata.unpack(); - assertEq(gameType.raw(), 2); - assertEq(createdAt.raw(), block.timestamp); + _assertLatestGameResult(GameType.wrap(1), start, start); + _assertLatestGameResult(GameType.wrap(0), start, start - 1); + _assertLatestGameResult(GameType.wrap(2), start, start - 2); } /// @notice Tests that `findLatestGames` returns the correct games, if there are less than `_n` /// games of the given type available. function test_findLatestGames_lessThanNAvailable_succeeds() public { - // Need to clear out the length of the game list on forked list to avoid massive iteration. - if (isForkTest()) { - vm.store( - address(disputeGameFactory), - bytes32(ForgeArtifacts.getSlot("DisputeGameFactory", "_disputeGameList").slot), - bytes32(0) - ); - } + _setGame(address(fakeClone), FIND_LATEST_SEARCH_GAME_TYPE, 0); + _setGame(address(fakeClone), FIND_LATEST_OTHER_GAME_TYPE, 0); - // Create some dispute games of varying game types. - disputeGameFactory.create(GameType.wrap(1), Claim.wrap(bytes32(0)), abi.encode(0)); - disputeGameFactory.create(GameType.wrap(1), Claim.wrap(bytes32(uint256(1))), abi.encode(1)); + uint256 firstSearchGameIndex = disputeGameFactory.gameCount(); + disputeGameFactory.create(FIND_LATEST_SEARCH_GAME_TYPE, Claim.wrap(bytes32(0)), abi.encode(0)); + disputeGameFactory.create(FIND_LATEST_SEARCH_GAME_TYPE, Claim.wrap(bytes32(uint256(1))), abi.encode(1)); for (uint256 i; i < 1 << 3; i++) { - disputeGameFactory.create(GameType.wrap(0), Claim.wrap(bytes32(i)), abi.encode(i)); + disputeGameFactory.create(FIND_LATEST_OTHER_GAME_TYPE, Claim.wrap(bytes32(i)), abi.encode(i)); } - // Grab the existing game count. uint256 gameCount = disputeGameFactory.gameCount(); - // Try to find 5 games of type 2, but there are none. IDisputeGameFactory.GameSearchResult[] memory games; - games = disputeGameFactory.findLatestGames(GameType.wrap(2), gameCount - 1, 5); + games = disputeGameFactory.findLatestGames(GameType.wrap(type(uint32).max - 2), gameCount - 1, 5); assertEq(games.length, 0); - // Try to find 2 games of type 1, but there are only 2. - games = disputeGameFactory.findLatestGames(GameType.wrap(1), gameCount - 1, 5); + games = disputeGameFactory.findLatestGames(FIND_LATEST_SEARCH_GAME_TYPE, gameCount - 1, 5); assertEq(games.length, 2); - assertEq(games[0].index, 1); - assertEq(games[1].index, 0); + assertEq(games[0].index, firstSearchGameIndex + 1); + assertEq(games[1].index, firstSearchGameIndex); } /// @notice Tests that the expected number of games are returned when `findLatestGames` is @@ -547,22 +448,30 @@ contract DisputeGameFactory_FindLatestGames_Test is DisputeGameFactory_TestInit ) public { - _numGames = bound(_numGames, 0, isForkTest() ? 5 : 256); + _numGames = bound(_numGames, 0, isForkTest() ? 5 : 32); _numSearchedGames = bound(_numSearchedGames, 0, _numGames); _n = bound(_n, 0, _numSearchedGames); - // Create `_numGames` dispute games, with at least `_numSearchedGames` games. for (uint256 i; i < _numGames; i++) { uint32 gameType = i < _numSearchedGames ? 0 : 1; disputeGameFactory.create(GameType.wrap(gameType), Claim.wrap(bytes32(i)), abi.encode(i)); } - // Ensure that the correct number of games are returned. uint256 start = _numGames == 0 ? 0 : _numGames - 1; IDisputeGameFactory.GameSearchResult[] memory games = disputeGameFactory.findLatestGames(GameType.wrap(0), start, _n); assertEq(games.length, _n); } + + function _assertLatestGameResult(GameType _gameType, uint256 _start, uint256 _expectedIndex) internal view { + IDisputeGameFactory.GameSearchResult[] memory games = disputeGameFactory.findLatestGames(_gameType, _start, 1); + assertEq(games.length, 1); + + assertEq(games[0].index, _expectedIndex); + (GameType gameType, Timestamp createdAt,) = games[0].metadata.unpack(); + assertEq(GameType.unwrap(gameType), GameType.unwrap(_gameType)); + assertEq(Timestamp.unwrap(createdAt), block.timestamp); + } } /// @title DisputeGameFactory_Uncategorized_Test @@ -582,8 +491,7 @@ contract DisputeGameFactory_Uncategorized_Test is DisputeGameFactory_TestInit { /// @notice Tests that the `transferOwnership` function reverts when called by a non-owner. function test_transferOwnership_notOwner_reverts() public { - vm.prank(address(0)); - vm.expectRevert("Ownable: caller is not the owner"); + _expectNonOwnerRevert(); disputeGameFactory.transferOwnership(address(1)); } } diff --git a/test/L1/proofs/NitroEnclaveVerifier.t.sol b/test/L1/proofs/NitroEnclaveVerifier.t.sol index 246c4d0f..34eedbce 100644 --- a/test/L1/proofs/NitroEnclaveVerifier.t.sol +++ b/test/L1/proofs/NitroEnclaveVerifier.t.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.0; import { Test } from "lib/forge-std/src/Test.sol"; +import { Ownable } from "lib/solady/src/auth/Ownable.sol"; import { ZkCoProcessorType, @@ -15,29 +16,35 @@ import { import { NitroEnclaveVerifier } from "src/L1/proofs/tee/NitroEnclaveVerifier.sol"; contract NitroEnclaveVerifierTest is Test { - NitroEnclaveVerifier public verifier; - - address public owner; - address public submitter; - address public revokerAddr; - address public mockRiscZeroVerifier; - address public mockSP1Verifier; - - bytes32 public constant ROOT_CERT = keccak256("root-cert"); - bytes32 public constant INTERMEDIATE_CERT_1 = keccak256("intermediate-cert-1"); - bytes32 public constant INTERMEDIATE_CERT_2 = keccak256("intermediate-cert-2"); - bytes32 public constant VERIFIER_ID = keccak256("verifier-id"); - bytes32 public constant AGGREGATOR_ID = keccak256("aggregator-id"); - bytes32 public constant VERIFIER_PROOF_ID = keccak256("verifier-proof-id"); - - uint64 public constant MAX_TIME_DIFF = 3600; // 1 hour + NitroEnclaveVerifier internal verifier; + + address internal owner; + address internal submitter; + address internal revokerAddr; + address internal mockRiscZeroVerifier; + address internal mockSP1Verifier; + + bytes32 internal constant ROOT_CERT = keccak256("root-cert"); + bytes32 internal constant INTERMEDIATE_CERT_1 = keccak256("intermediate-cert-1"); + bytes32 internal constant INTERMEDIATE_CERT_2 = keccak256("intermediate-cert-2"); + bytes32 internal constant VERIFIER_ID = keccak256("verifier-id"); + bytes32 internal constant AGGREGATOR_ID = keccak256("aggregator-id"); + bytes32 internal constant VERIFIER_PROOF_ID = keccak256("verifier-proof-id"); + bytes4 internal constant DEFAULT_PROOF_SELECTOR = bytes4(0); + bytes4 internal constant NON_DEFAULT_PROOF_SELECTOR = 0x00000001; + bytes4 internal constant TEST_ROUTE_SELECTOR = bytes4(keccak256("test")); + bytes4 internal constant RISC_ZERO_VERIFY_SELECTOR = bytes4(keccak256("verify(bytes,bytes32,bytes32)")); + bytes4 internal constant SP1_VERIFY_PROOF_SELECTOR = bytes4(keccak256("verifyProof(bytes32,bytes,bytes)")); + address internal constant FROZEN_ROUTE_SENTINEL = address(0xdead); + + uint64 internal constant MAX_TIME_DIFF = 3600; // 1 hour // Realistic timestamp so timestamp validation tests work correctly uint256 internal constant REALISTIC_TIMESTAMP = 1_700_000_000; // Expiry timestamps for test certs (well after REALISTIC_TIMESTAMP) uint64 internal constant INTERMEDIATE_CERT_1_EXPIRY = 1_800_000_000; // ~2027 - uint64 internal constant INTERMEDIATE_CERT_2_EXPIRY = 1_750_000_000; // ~2025 + uint64 internal constant ROOT_CERT_EXPIRY = 1_900_000_000; uint64 internal constant NEW_LEAF_CERT_EXPIRY = 1_700_100_000; // ~28 hours after REALISTIC_TIMESTAMP function setUp() public { @@ -55,8 +62,7 @@ contract NitroEnclaveVerifierTest is Test { uint64[] memory trustedCertExpiries = new uint64[](1); trustedCertExpiries[0] = INTERMEDIATE_CERT_1_EXPIRY; - ZkCoProcessorConfig memory zkCfg = - ZkCoProcessorConfig({ verifierId: VERIFIER_ID, aggregatorId: AGGREGATOR_ID, zkVerifier: mockSP1Verifier }); + ZkCoProcessorConfig memory zkCfg = _zkConfig(mockSP1Verifier); verifier = new NitroEnclaveVerifier( owner, @@ -101,7 +107,7 @@ contract NitroEnclaveVerifierTest is Test { function testConstructorRevertsIfCertExpiriesLengthMismatch() public { bytes32[] memory certs = new bytes32[](1); certs[0] = INTERMEDIATE_CERT_1; - uint64[] memory expiries = new uint64[](0); // mismatched length + uint64[] memory expiries = new uint64[](0); ZkCoProcessorConfig memory zkCfg = ZkCoProcessorConfig({ verifierId: bytes32(0), aggregatorId: bytes32(0), zkVerifier: address(0) }); vm.expectRevert(abi.encodeWithSelector(NitroEnclaveVerifier.CertExpiriesLengthMismatch.selector, 1, 0)); @@ -128,8 +134,7 @@ contract NitroEnclaveVerifierTest is Test { } function testSetRootCertRevertsIfNotOwner() public { - vm.prank(submitter); - vm.expectRevert(); + _expectNotOwnerRevert(submitter); verifier.setRootCert(keccak256("bad")); } @@ -147,8 +152,7 @@ contract NitroEnclaveVerifierTest is Test { } function testSetMaxTimeDiffRevertsIfNotOwner() public { - vm.prank(submitter); - vm.expectRevert(); + _expectNotOwnerRevert(submitter); verifier.setMaxTimeDiff(7200); } @@ -166,17 +170,14 @@ contract NitroEnclaveVerifierTest is Test { } function testSetProofSubmitterRevertsIfNotOwner() public { - vm.prank(submitter); - vm.expectRevert(); + _expectNotOwnerRevert(submitter); verifier.setProofSubmitter(makeAddr("anyone")); } // ============ setZkConfiguration Tests ============ function testSetZkConfiguration() public { - ZkCoProcessorConfig memory config = ZkCoProcessorConfig({ - verifierId: VERIFIER_ID, aggregatorId: AGGREGATOR_ID, zkVerifier: mockRiscZeroVerifier - }); + ZkCoProcessorConfig memory config = _zkConfig(mockRiscZeroVerifier); verifier.setZkConfiguration(ZkCoProcessorType.RiscZero, config, VERIFIER_PROOF_ID); @@ -189,12 +190,9 @@ contract NitroEnclaveVerifierTest is Test { } function testSetZkConfigurationRevertsIfNotOwner() public { - ZkCoProcessorConfig memory config = ZkCoProcessorConfig({ - verifierId: VERIFIER_ID, aggregatorId: AGGREGATOR_ID, zkVerifier: mockRiscZeroVerifier - }); + ZkCoProcessorConfig memory config = _zkConfig(mockRiscZeroVerifier); - vm.prank(submitter); - vm.expectRevert(); + _expectNotOwnerRevert(submitter); verifier.setZkConfiguration(ZkCoProcessorType.RiscZero, config, VERIFIER_PROOF_ID); } @@ -227,8 +225,7 @@ contract NitroEnclaveVerifierTest is Test { function testConstructorAcceptsZeroRevoker() public { bytes32[] memory certs = new bytes32[](0); uint64[] memory expiries = new uint64[](0); - ZkCoProcessorConfig memory zkCfg = - ZkCoProcessorConfig({ verifierId: VERIFIER_ID, aggregatorId: AGGREGATOR_ID, zkVerifier: mockSP1Verifier }); + ZkCoProcessorConfig memory zkCfg = _zkConfig(mockSP1Verifier); NitroEnclaveVerifier v = new NitroEnclaveVerifier( owner, MAX_TIME_DIFF, @@ -251,12 +248,6 @@ contract NitroEnclaveVerifierTest is Test { assertEq(verifier.trustedIntermediateCerts(INTERMEDIATE_CERT_1), 0); } - function testOwnerCanStillRevokeCert() public { - assertGt(verifier.trustedIntermediateCerts(INTERMEDIATE_CERT_1), 0); - verifier.revokeCert(INTERMEDIATE_CERT_1); - assertEq(verifier.trustedIntermediateCerts(INTERMEDIATE_CERT_1), 0); - } - function testSetRevoker() public { address newRevoker = makeAddr("new-revoker"); verifier.setRevoker(newRevoker); @@ -267,7 +258,6 @@ contract NitroEnclaveVerifierTest is Test { verifier.setRevoker(address(0)); assertEq(verifier.revoker(), address(0)); - // Now revoker (zero address) can't call revokeCert vm.prank(revokerAddr); vm.expectRevert(NitroEnclaveVerifier.CallerNotOwnerOrRevoker.selector); verifier.revokeCert(INTERMEDIATE_CERT_1); @@ -281,14 +271,12 @@ contract NitroEnclaveVerifierTest is Test { } function testSetRevokerRevertsIfNotOwner() public { - vm.prank(submitter); - vm.expectRevert(); + _expectNotOwnerRevert(submitter); verifier.setRevoker(makeAddr("anyone")); } function testSetRevokerRevertsIfCalledByRevoker() public { - vm.prank(revokerAddr); - vm.expectRevert(); + _expectNotOwnerRevert(revokerAddr); verifier.setRevoker(makeAddr("anyone")); } @@ -324,8 +312,7 @@ contract NitroEnclaveVerifierTest is Test { function testUpdateVerifierIdRevertsIfNotOwner() public { _setUpRiscZeroConfig(); - vm.prank(submitter); - vm.expectRevert(); + _expectNotOwnerRevert(submitter); verifier.updateVerifierId(ZkCoProcessorType.RiscZero, keccak256("new"), keccak256("proof")); } @@ -359,81 +346,65 @@ contract NitroEnclaveVerifierTest is Test { function testUpdateAggregatorIdRevertsIfNotOwner() public { _setUpRiscZeroConfig(); - vm.prank(submitter); - vm.expectRevert(); + _expectNotOwnerRevert(submitter); verifier.updateAggregatorId(ZkCoProcessorType.RiscZero, keccak256("new")); } // ============ addVerifyRoute / freezeVerifyRoute Tests ============ function testAddVerifyRoute() public { - bytes4 selector = bytes4(keccak256("test")); address routeVerifier = makeAddr("route-verifier"); - verifier.addVerifyRoute(ZkCoProcessorType.RiscZero, selector, routeVerifier); - assertEq(verifier.getZkVerifier(ZkCoProcessorType.RiscZero, selector), routeVerifier); + verifier.addVerifyRoute(ZkCoProcessorType.RiscZero, TEST_ROUTE_SELECTOR, routeVerifier); + assertEq(verifier.getZkVerifier(ZkCoProcessorType.RiscZero, TEST_ROUTE_SELECTOR), routeVerifier); } function testAddVerifyRouteRevertsIfZeroAddress() public { vm.expectRevert(NitroEnclaveVerifier.ZeroVerifierAddress.selector); - verifier.addVerifyRoute(ZkCoProcessorType.RiscZero, bytes4(uint32(0x01)), address(0)); + verifier.addVerifyRoute(ZkCoProcessorType.RiscZero, NON_DEFAULT_PROOF_SELECTOR, address(0)); } function testAddVerifyRouteRevertsIfFrozenSentinel() public { vm.expectRevert(NitroEnclaveVerifier.InvalidVerifierAddress.selector); - verifier.addVerifyRoute(ZkCoProcessorType.RiscZero, bytes4(uint32(0x01)), address(0xdead)); + verifier.addVerifyRoute(ZkCoProcessorType.RiscZero, NON_DEFAULT_PROOF_SELECTOR, FROZEN_ROUTE_SENTINEL); } function testAddVerifyRouteRevertsIfNotOwner() public { - vm.prank(submitter); - vm.expectRevert(); - verifier.addVerifyRoute(ZkCoProcessorType.RiscZero, bytes4(keccak256("test")), makeAddr("v")); + _expectNotOwnerRevert(submitter); + verifier.addVerifyRoute(ZkCoProcessorType.RiscZero, TEST_ROUTE_SELECTOR, makeAddr("v")); } function testFreezeVerifyRoute() public { - bytes4 selector = bytes4(keccak256("test")); address routeVerifier = makeAddr("route-verifier"); - verifier.addVerifyRoute(ZkCoProcessorType.RiscZero, selector, routeVerifier); - verifier.freezeVerifyRoute(ZkCoProcessorType.RiscZero, selector); + _addAndFreezeVerifyRoute(ZkCoProcessorType.RiscZero, TEST_ROUTE_SELECTOR, routeVerifier); - vm.expectRevert( - abi.encodeWithSelector(NitroEnclaveVerifier.ZkRouteFrozen.selector, ZkCoProcessorType.RiscZero, selector) - ); - verifier.getZkVerifier(ZkCoProcessorType.RiscZero, selector); + _expectZkRouteFrozenRevert(ZkCoProcessorType.RiscZero, TEST_ROUTE_SELECTOR); + verifier.getZkVerifier(ZkCoProcessorType.RiscZero, TEST_ROUTE_SELECTOR); } function testAddVerifyRouteRevertsIfFrozen() public { - bytes4 selector = bytes4(keccak256("test")); address routeVerifier = makeAddr("route-verifier"); - verifier.addVerifyRoute(ZkCoProcessorType.RiscZero, selector, routeVerifier); - verifier.freezeVerifyRoute(ZkCoProcessorType.RiscZero, selector); + _addAndFreezeVerifyRoute(ZkCoProcessorType.RiscZero, TEST_ROUTE_SELECTOR, routeVerifier); - vm.expectRevert( - abi.encodeWithSelector(NitroEnclaveVerifier.ZkRouteFrozen.selector, ZkCoProcessorType.RiscZero, selector) - ); - verifier.addVerifyRoute(ZkCoProcessorType.RiscZero, selector, routeVerifier); + _expectZkRouteFrozenRevert(ZkCoProcessorType.RiscZero, TEST_ROUTE_SELECTOR); + verifier.addVerifyRoute(ZkCoProcessorType.RiscZero, TEST_ROUTE_SELECTOR, routeVerifier); } function testFreezeVerifyRouteRevertsIfAlreadyFrozen() public { - bytes4 selector = bytes4(keccak256("test")); - verifier.addVerifyRoute(ZkCoProcessorType.RiscZero, selector, makeAddr("v")); - verifier.freezeVerifyRoute(ZkCoProcessorType.RiscZero, selector); + verifier.addVerifyRoute(ZkCoProcessorType.RiscZero, TEST_ROUTE_SELECTOR, makeAddr("v")); + verifier.freezeVerifyRoute(ZkCoProcessorType.RiscZero, TEST_ROUTE_SELECTOR); - vm.expectRevert( - abi.encodeWithSelector(NitroEnclaveVerifier.ZkRouteFrozen.selector, ZkCoProcessorType.RiscZero, selector) - ); - verifier.freezeVerifyRoute(ZkCoProcessorType.RiscZero, selector); + _expectZkRouteFrozenRevert(ZkCoProcessorType.RiscZero, TEST_ROUTE_SELECTOR); + verifier.freezeVerifyRoute(ZkCoProcessorType.RiscZero, TEST_ROUTE_SELECTOR); } function testFreezeVerifyRouteRevertsIfNotOwner() public { - bytes4 selector = bytes4(keccak256("test")); - verifier.addVerifyRoute(ZkCoProcessorType.RiscZero, selector, makeAddr("v")); + verifier.addVerifyRoute(ZkCoProcessorType.RiscZero, TEST_ROUTE_SELECTOR, makeAddr("v")); - vm.prank(submitter); - vm.expectRevert(); - verifier.freezeVerifyRoute(ZkCoProcessorType.RiscZero, selector); + _expectNotOwnerRevert(submitter); + verifier.freezeVerifyRoute(ZkCoProcessorType.RiscZero, TEST_ROUTE_SELECTOR); } // ============ getZkVerifier Tests ============ @@ -462,7 +433,7 @@ contract NitroEnclaveVerifierTest is Test { reportCerts[0] = new bytes32[](3); reportCerts[0][0] = ROOT_CERT; reportCerts[0][1] = INTERMEDIATE_CERT_1; - reportCerts[0][2] = INTERMEDIATE_CERT_2; // not trusted + reportCerts[0][2] = INTERMEDIATE_CERT_2; uint8[] memory results = verifier.checkTrustedIntermediateCerts(reportCerts); assertEq(results[0], 2); // root + 1 intermediate trusted @@ -488,38 +459,29 @@ contract NitroEnclaveVerifierTest is Test { // ============ verify — ZkVerifierNotConfigured ============ function testVerifyRevertsIfZkVerifierNotConfigured() public { - // Set up config WITHOUT a zkVerifier address (zero) - ZkCoProcessorConfig memory config = - ZkCoProcessorConfig({ verifierId: VERIFIER_ID, aggregatorId: AGGREGATOR_ID, zkVerifier: address(0) }); + ZkCoProcessorConfig memory config = _zkConfig(address(0)); verifier.setZkConfiguration(ZkCoProcessorType.RiscZero, config, VERIFIER_PROOF_ID); - VerifierJournal memory journal = _createSuccessJournal(); - bytes memory output = abi.encode(journal); - bytes memory proofBytes = abi.encodePacked(bytes4(0), bytes32(0)); + bytes memory proofBytes = _proofBytes(); vm.prank(submitter); vm.expectRevert( abi.encodeWithSelector(NitroEnclaveVerifier.ZkVerifierNotConfigured.selector, ZkCoProcessorType.RiscZero) ); - verifier.verify(output, ZkCoProcessorType.RiscZero, proofBytes); + verifier.verify("", ZkCoProcessorType.RiscZero, proofBytes); } // ============ verify — Unknown_Zk_Coprocessor ============ function testVerifyRevertsForUnknownCoprocessor() public { - // Use ZkCoProcessorType.Unknown (0) — not RiscZero or Succinct - ZkCoProcessorConfig memory config = ZkCoProcessorConfig({ - verifierId: VERIFIER_ID, aggregatorId: AGGREGATOR_ID, zkVerifier: mockRiscZeroVerifier - }); + ZkCoProcessorConfig memory config = _zkConfig(mockRiscZeroVerifier); verifier.setZkConfiguration(ZkCoProcessorType.Unknown, config, VERIFIER_PROOF_ID); - VerifierJournal memory journal = _createSuccessJournal(); - bytes memory output = abi.encode(journal); - bytes memory proofBytes = abi.encodePacked(bytes4(0), bytes32(0)); + bytes memory proofBytes = _proofBytes(); vm.prank(submitter); vm.expectRevert(NitroEnclaveVerifier.Unknown_Zk_Coprocessor.selector); - verifier.verify(output, ZkCoProcessorType.Unknown, proofBytes); + verifier.verify("", ZkCoProcessorType.Unknown, proofBytes); } // ============ verify — ZkRouteFrozen during verify() ============ @@ -527,19 +489,15 @@ contract NitroEnclaveVerifierTest is Test { function testVerifyRevertsIfRouteFrozen() public { _setUpRiscZeroConfig(); - bytes4 selector = bytes4(0); // matches the selector in our proofBytes + bytes4 selector = DEFAULT_PROOF_SELECTOR; verifier.addVerifyRoute(ZkCoProcessorType.RiscZero, selector, makeAddr("route-v")); verifier.freezeVerifyRoute(ZkCoProcessorType.RiscZero, selector); - VerifierJournal memory journal = _createSuccessJournal(); - bytes memory output = abi.encode(journal); - bytes memory proofBytes = abi.encodePacked(bytes4(0), bytes32(0)); + bytes memory proofBytes = _proofBytes(); vm.prank(submitter); - vm.expectRevert( - abi.encodeWithSelector(NitroEnclaveVerifier.ZkRouteFrozen.selector, ZkCoProcessorType.RiscZero, selector) - ); - verifier.verify(output, ZkCoProcessorType.RiscZero, proofBytes); + _expectZkRouteFrozenRevert(ZkCoProcessorType.RiscZero, selector); + verifier.verify("", ZkCoProcessorType.RiscZero, proofBytes); } // ============ verify — RiscZero happy path ============ @@ -547,16 +505,9 @@ contract NitroEnclaveVerifierTest is Test { function testVerifySuccessfulJournal() public { _setUpRiscZeroConfig(); - VerifierJournal memory journal = _createSuccessJournal(); - bytes memory output = abi.encode(journal); - bytes memory proofBytes = abi.encodePacked(bytes4(0), bytes32(0)); - - _mockRiscZeroVerify(VERIFIER_ID, output, proofBytes); + VerifierJournal memory result = _verifyRiscZeroJournal(_createSuccessJournal()); - vm.prank(submitter); - VerifierJournal memory result = verifier.verify(output, ZkCoProcessorType.RiscZero, proofBytes); - - assertEq(uint8(result.result), uint8(VerificationResult.Success)); + _assertVerificationResult(result, VerificationResult.Success); } function testVerifyJournalRootCertNotTrusted() public { @@ -564,15 +515,9 @@ contract NitroEnclaveVerifierTest is Test { VerifierJournal memory journal = _createSuccessJournal(); journal.certs[0] = keccak256("wrong-root"); - bytes memory output = abi.encode(journal); - bytes memory proofBytes = abi.encodePacked(bytes4(0), bytes32(0)); + VerifierJournal memory result = _verifyRiscZeroJournal(journal); - _mockRiscZeroVerify(VERIFIER_ID, output, proofBytes); - - vm.prank(submitter); - VerifierJournal memory result = verifier.verify(output, ZkCoProcessorType.RiscZero, proofBytes); - - assertEq(uint8(result.result), uint8(VerificationResult.RootCertNotTrusted)); + _assertVerificationResult(result, VerificationResult.RootCertNotTrusted); } function testVerifyJournalRootCertNotTrustedZeroPrefixLen() public { @@ -580,15 +525,9 @@ contract NitroEnclaveVerifierTest is Test { VerifierJournal memory journal = _createSuccessJournal(); journal.trustedCertsPrefixLen = 0; - bytes memory output = abi.encode(journal); - bytes memory proofBytes = abi.encodePacked(bytes4(0), bytes32(0)); - - _mockRiscZeroVerify(VERIFIER_ID, output, proofBytes); - - vm.prank(submitter); - VerifierJournal memory result = verifier.verify(output, ZkCoProcessorType.RiscZero, proofBytes); + VerifierJournal memory result = _verifyRiscZeroJournal(journal); - assertEq(uint8(result.result), uint8(VerificationResult.RootCertNotTrusted)); + _assertVerificationResult(result, VerificationResult.RootCertNotTrusted); } function testVerifyJournalIntermediateCertNotTrusted() public { @@ -597,15 +536,9 @@ contract NitroEnclaveVerifierTest is Test { VerifierJournal memory journal = _createSuccessJournal(); // Replace trusted intermediate with untrusted one, but keep trustedCertsPrefixLen = 2 journal.certs[1] = keccak256("untrusted-intermediate"); - bytes memory output = abi.encode(journal); - bytes memory proofBytes = abi.encodePacked(bytes4(0), bytes32(0)); - - _mockRiscZeroVerify(VERIFIER_ID, output, proofBytes); + VerifierJournal memory result = _verifyRiscZeroJournal(journal); - vm.prank(submitter); - VerifierJournal memory result = verifier.verify(output, ZkCoProcessorType.RiscZero, proofBytes); - - assertEq(uint8(result.result), uint8(VerificationResult.IntermediateCertsNotTrusted)); + _assertVerificationResult(result, VerificationResult.IntermediateCertsNotTrusted); } function testVerifyJournalInvalidTimestampTooOld() public { @@ -614,15 +547,9 @@ contract NitroEnclaveVerifierTest is Test { VerifierJournal memory journal = _createSuccessJournal(); // Set timestamp far in the past — more than maxTimeDiff seconds ago (in ms) journal.timestamp = uint64(block.timestamp - MAX_TIME_DIFF - 1) * 1000; - bytes memory output = abi.encode(journal); - bytes memory proofBytes = abi.encodePacked(bytes4(0), bytes32(0)); + VerifierJournal memory result = _verifyRiscZeroJournal(journal); - _mockRiscZeroVerify(VERIFIER_ID, output, proofBytes); - - vm.prank(submitter); - VerifierJournal memory result = verifier.verify(output, ZkCoProcessorType.RiscZero, proofBytes); - - assertEq(uint8(result.result), uint8(VerificationResult.InvalidTimestamp)); + _assertVerificationResult(result, VerificationResult.InvalidTimestamp); } function testVerifyJournalInvalidTimestampFuture() public { @@ -631,15 +558,9 @@ contract NitroEnclaveVerifierTest is Test { VerifierJournal memory journal = _createSuccessJournal(); // Set timestamp in the future (converted to ms) journal.timestamp = uint64(block.timestamp + 100) * 1000; - bytes memory output = abi.encode(journal); - bytes memory proofBytes = abi.encodePacked(bytes4(0), bytes32(0)); - - _mockRiscZeroVerify(VERIFIER_ID, output, proofBytes); + VerifierJournal memory result = _verifyRiscZeroJournal(journal); - vm.prank(submitter); - VerifierJournal memory result = verifier.verify(output, ZkCoProcessorType.RiscZero, proofBytes); - - assertEq(uint8(result.result), uint8(VerificationResult.InvalidTimestamp)); + _assertVerificationResult(result, VerificationResult.InvalidTimestamp); } function testVerifyCachesNewCerts() public { @@ -648,29 +569,8 @@ contract NitroEnclaveVerifierTest is Test { bytes32 newCert = keccak256("new-leaf-cert"); assertEq(verifier.trustedIntermediateCerts(newCert), 0); - VerifierJournal memory journal = _createSuccessJournal(); - // Add a new cert beyond the trusted prefix that will get cached - bytes32[] memory certs = new bytes32[](3); - certs[0] = ROOT_CERT; - certs[1] = INTERMEDIATE_CERT_1; - certs[2] = newCert; - journal.certs = certs; - - uint64[] memory expiries = new uint64[](3); - expiries[0] = INTERMEDIATE_CERT_1_EXPIRY + 100_000_000; // root expiry (doesn't matter for caching) - expiries[1] = INTERMEDIATE_CERT_1_EXPIRY; - expiries[2] = NEW_LEAF_CERT_EXPIRY; - journal.certExpiries = expiries; - - journal.trustedCertsPrefixLen = 2; // only root + 1 intermediate are pre-trusted - - bytes memory output = abi.encode(journal); - bytes memory proofBytes = abi.encodePacked(bytes4(0), bytes32(0)); - - _mockRiscZeroVerify(VERIFIER_ID, output, proofBytes); - - vm.prank(submitter); - verifier.verify(output, ZkCoProcessorType.RiscZero, proofBytes); + VerifierJournal memory journal = _createSuccessJournalWithLeaf(newCert, NEW_LEAF_CERT_EXPIRY); + _verifyRiscZeroJournal(journal); assertEq(verifier.trustedIntermediateCerts(newCert), NEW_LEAF_CERT_EXPIRY); } @@ -680,51 +580,30 @@ contract NitroEnclaveVerifierTest is Test { VerifierJournal memory journal = _createSuccessJournal(); journal.result = VerificationResult.IntermediateCertsNotTrusted; - bytes memory output = abi.encode(journal); - bytes memory proofBytes = abi.encodePacked(bytes4(0), bytes32(0)); - - _mockRiscZeroVerify(VERIFIER_ID, output, proofBytes); - - vm.prank(submitter); - VerifierJournal memory result = verifier.verify(output, ZkCoProcessorType.RiscZero, proofBytes); + VerifierJournal memory result = _verifyRiscZeroJournal(journal); - assertEq(uint8(result.result), uint8(VerificationResult.IntermediateCertsNotTrusted)); + _assertVerificationResult(result, VerificationResult.IntermediateCertsNotTrusted); } // ============ verify — Succinct SP1 happy path ============ function testVerifySuccessfulJournalSP1() public { - _setUpSP1Config(); + VerifierJournal memory result = _verifySP1Journal(_createSuccessJournal()); - VerifierJournal memory journal = _createSuccessJournal(); - bytes memory output = abi.encode(journal); - bytes memory proofBytes = abi.encodePacked(bytes4(0), bytes32(0)); - - _mockSP1Verify(VERIFIER_ID, output, proofBytes); - - vm.prank(submitter); - VerifierJournal memory result = verifier.verify(output, ZkCoProcessorType.Succinct, proofBytes); - - assertEq(uint8(result.result), uint8(VerificationResult.Success)); - } - - function testVerifyRevertsIfNotProofSubmitterSP1() public { - vm.expectRevert(NitroEnclaveVerifier.CallerNotProofSubmitter.selector); - verifier.verify("", ZkCoProcessorType.Succinct, ""); + _assertVerificationResult(result, VerificationResult.Success); } function testVerifyRevertsIfZkVerifierNotConfiguredSP1() public { - ZkCoProcessorConfig memory config = - ZkCoProcessorConfig({ verifierId: VERIFIER_ID, aggregatorId: AGGREGATOR_ID, zkVerifier: address(0) }); + ZkCoProcessorConfig memory config = _zkConfig(address(0)); verifier.setZkConfiguration(ZkCoProcessorType.Succinct, config, VERIFIER_PROOF_ID); - bytes memory proofBytes = abi.encodePacked(bytes4(0), bytes32(0)); + bytes memory proofBytes = _proofBytes(); vm.prank(submitter); vm.expectRevert( abi.encodeWithSelector(NitroEnclaveVerifier.ZkVerifierNotConfigured.selector, ZkCoProcessorType.Succinct) ); - verifier.verify(abi.encode(_createSuccessJournal()), ZkCoProcessorType.Succinct, proofBytes); + verifier.verify("", ZkCoProcessorType.Succinct, proofBytes); } // ============ batchVerify Tests ============ @@ -737,40 +616,19 @@ contract NitroEnclaveVerifierTest is Test { function testBatchVerifySuccess() public { _setUpRiscZeroConfig(); - VerifierJournal memory journal = _createSuccessJournal(); - VerifierJournal[] memory outputs = new VerifierJournal[](2); - outputs[0] = journal; - outputs[1] = journal; - - BatchVerifierJournal memory batchJournal = - BatchVerifierJournal({ verifierVk: VERIFIER_PROOF_ID, outputs: outputs }); - - bytes memory output = abi.encode(batchJournal); - bytes memory proofBytes = abi.encodePacked(bytes4(0), bytes32(0)); - - _mockRiscZeroVerify(AGGREGATOR_ID, output, proofBytes); - - vm.prank(submitter); - VerifierJournal[] memory results = verifier.batchVerify(output, ZkCoProcessorType.RiscZero, proofBytes); + VerifierJournal[] memory results = _batchVerifyRiscZero(_createBatchJournal(VERIFIER_PROOF_ID, 2)); assertEq(results.length, 2); - assertEq(uint8(results[0].result), uint8(VerificationResult.Success)); - assertEq(uint8(results[1].result), uint8(VerificationResult.Success)); + _assertVerificationResult(results[0], VerificationResult.Success); + _assertVerificationResult(results[1], VerificationResult.Success); } function testBatchVerifyRevertsIfVerifierVkMismatch() public { _setUpRiscZeroConfig(); bytes32 wrongVk = keccak256("wrong-vk"); - VerifierJournal[] memory outputs = new VerifierJournal[](1); - outputs[0] = _createSuccessJournal(); - - BatchVerifierJournal memory batchJournal = BatchVerifierJournal({ verifierVk: wrongVk, outputs: outputs }); - - bytes memory output = abi.encode(batchJournal); - bytes memory proofBytes = abi.encodePacked(bytes4(0), bytes32(0)); - - _mockRiscZeroVerify(AGGREGATOR_ID, output, proofBytes); + BatchVerifierJournal memory batchJournal = _createBatchJournal(wrongVk, 1); + (bytes memory output, bytes memory proofBytes) = _mockRiscZeroBatchVerify(batchJournal); vm.prank(submitter); vm.expectRevert( @@ -780,25 +638,10 @@ contract NitroEnclaveVerifierTest is Test { } function testBatchVerifySuccessSP1() public { - _setUpSP1Config(); - - VerifierJournal memory journal = _createSuccessJournal(); - VerifierJournal[] memory outputs = new VerifierJournal[](1); - outputs[0] = journal; - - BatchVerifierJournal memory batchJournal = - BatchVerifierJournal({ verifierVk: VERIFIER_PROOF_ID, outputs: outputs }); - - bytes memory output = abi.encode(batchJournal); - bytes memory proofBytes = abi.encodePacked(bytes4(0), bytes32(0)); - - _mockSP1Verify(AGGREGATOR_ID, output, proofBytes); - - vm.prank(submitter); - VerifierJournal[] memory results = verifier.batchVerify(output, ZkCoProcessorType.Succinct, proofBytes); + VerifierJournal[] memory results = _batchVerifySP1(_createBatchJournal(VERIFIER_PROOF_ID, 1)); assertEq(results.length, 1); - assertEq(uint8(results[0].result), uint8(VerificationResult.Success)); + _assertVerificationResult(results[0], VerificationResult.Success); } // ============ Revoked Cert Invalidates Journal ============ @@ -807,18 +650,12 @@ contract NitroEnclaveVerifierTest is Test { _setUpRiscZeroConfig(); VerifierJournal memory journal = _createSuccessJournal(); - bytes memory output = abi.encode(journal); - bytes memory proofBytes = abi.encodePacked(bytes4(0), bytes32(0)); - // Revoke the intermediate cert before verification verifier.revokeCert(INTERMEDIATE_CERT_1); - _mockRiscZeroVerify(VERIFIER_ID, output, proofBytes); - - vm.prank(submitter); - VerifierJournal memory result = verifier.verify(output, ZkCoProcessorType.RiscZero, proofBytes); + VerifierJournal memory result = _verifyRiscZeroJournal(journal); - assertEq(uint8(result.result), uint8(VerificationResult.IntermediateCertsNotTrusted)); + _assertVerificationResult(result, VerificationResult.IntermediateCertsNotTrusted); } // ============ Expiry-Aware Caching Tests ============ @@ -826,40 +663,25 @@ contract NitroEnclaveVerifierTest is Test { function testExpiredCachedCertFailsVerification() public { _setUpRiscZeroConfig(); - // Warp past the intermediate cert's expiry vm.warp(INTERMEDIATE_CERT_1_EXPIRY + 1); VerifierJournal memory journal = _createSuccessJournal(); - // Update timestamp to be valid at the new block.timestamp journal.timestamp = uint64(block.timestamp - 1) * 1000; - bytes memory output = abi.encode(journal); - bytes memory proofBytes = abi.encodePacked(bytes4(0), bytes32(0)); - - _mockRiscZeroVerify(VERIFIER_ID, output, proofBytes); - - vm.prank(submitter); - VerifierJournal memory result = verifier.verify(output, ZkCoProcessorType.RiscZero, proofBytes); + VerifierJournal memory result = _verifyRiscZeroJournal(journal); - assertEq(uint8(result.result), uint8(VerificationResult.IntermediateCertsNotTrusted)); + _assertVerificationResult(result, VerificationResult.IntermediateCertsNotTrusted); } function testNonExpiredCachedCertPassesVerification() public { _setUpRiscZeroConfig(); - // Warp to just before the intermediate cert's expiry vm.warp(INTERMEDIATE_CERT_1_EXPIRY - 1); VerifierJournal memory journal = _createSuccessJournal(); journal.timestamp = uint64(block.timestamp - 1) * 1000; - bytes memory output = abi.encode(journal); - bytes memory proofBytes = abi.encodePacked(bytes4(0), bytes32(0)); - - _mockRiscZeroVerify(VERIFIER_ID, output, proofBytes); + VerifierJournal memory result = _verifyRiscZeroJournal(journal); - vm.prank(submitter); - VerifierJournal memory result = verifier.verify(output, ZkCoProcessorType.RiscZero, proofBytes); - - assertEq(uint8(result.result), uint8(VerificationResult.Success)); + _assertVerificationResult(result, VerificationResult.Success); } // Untrusted chain certs (past trustedCertsPrefixLen): expired journal notAfter => InvalidTimestamp @@ -868,40 +690,20 @@ contract NitroEnclaveVerifierTest is Test { bytes32 expiredLeaf = keccak256("expired-untrusted-leaf"); - VerifierJournal memory journal = _createSuccessJournal(); - bytes32[] memory certs = new bytes32[](3); - certs[0] = ROOT_CERT; - certs[1] = INTERMEDIATE_CERT_1; - certs[2] = expiredLeaf; - journal.certs = certs; - - uint64[] memory expiries = new uint64[](3); - expiries[0] = INTERMEDIATE_CERT_1_EXPIRY + 100_000_000; - expiries[1] = INTERMEDIATE_CERT_1_EXPIRY; - expiries[2] = uint64(block.timestamp - 1); - journal.certExpiries = expiries; - journal.trustedCertsPrefixLen = 2; - - bytes memory output = abi.encode(journal); - bytes memory proofBytes = abi.encodePacked(bytes4(0), bytes32(0)); + VerifierJournal memory journal = _createSuccessJournalWithLeaf(expiredLeaf, uint64(block.timestamp - 1)); + VerifierJournal memory result = _verifyRiscZeroJournal(journal); - _mockRiscZeroVerify(VERIFIER_ID, output, proofBytes); - - vm.prank(submitter); - VerifierJournal memory result = verifier.verify(output, ZkCoProcessorType.RiscZero, proofBytes); - - assertEq(uint8(result.result), uint8(VerificationResult.InvalidTimestamp)); + _assertVerificationResult(result, VerificationResult.InvalidTimestamp); assertEq(verifier.trustedIntermediateCerts(expiredLeaf), 0); } function testCheckTrustedIntermediateCertsStopsAtExpiredCert() public { - // Warp past the intermediate cert's expiry vm.warp(INTERMEDIATE_CERT_1_EXPIRY + 1); bytes32[][] memory reportCerts = new bytes32[][](1); reportCerts[0] = new bytes32[](2); reportCerts[0][0] = ROOT_CERT; - reportCerts[0][1] = INTERMEDIATE_CERT_1; // expired + reportCerts[0][1] = INTERMEDIATE_CERT_1; uint8[] memory results = verifier.checkTrustedIntermediateCerts(reportCerts); assertEq(results[0], 1); // only root counted, expired intermediate skipped @@ -913,51 +715,109 @@ contract NitroEnclaveVerifierTest is Test { bytes32 newCert = keccak256("brand-new-cert"); uint64 newCertExpiry = uint64(REALISTIC_TIMESTAMP + 86_400); // 1 day from now - VerifierJournal memory journal = _createSuccessJournal(); - bytes32[] memory certs = new bytes32[](3); - certs[0] = ROOT_CERT; - certs[1] = INTERMEDIATE_CERT_1; - certs[2] = newCert; - journal.certs = certs; + VerifierJournal memory journal = _createSuccessJournalWithLeaf(newCert, newCertExpiry); + _verifyRiscZeroJournal(journal); - uint64[] memory expiries = new uint64[](3); - expiries[0] = INTERMEDIATE_CERT_1_EXPIRY + 100_000_000; - expiries[1] = INTERMEDIATE_CERT_1_EXPIRY; - expiries[2] = newCertExpiry; - journal.certExpiries = expiries; + assertEq(verifier.trustedIntermediateCerts(newCert), newCertExpiry); + } - journal.trustedCertsPrefixLen = 2; + // ============ Helpers ============ + + function _setUpRiscZeroConfig() internal { + verifier.setZkConfiguration(ZkCoProcessorType.RiscZero, _zkConfig(mockRiscZeroVerifier), VERIFIER_PROOF_ID); + } + + function _zkConfig(address zkVerifier) internal pure returns (ZkCoProcessorConfig memory) { + return ZkCoProcessorConfig({ verifierId: VERIFIER_ID, aggregatorId: AGGREGATOR_ID, zkVerifier: zkVerifier }); + } + + function _expectNotOwnerRevert(address caller) internal { + vm.prank(caller); + vm.expectRevert(Ownable.Unauthorized.selector); + } + + function _expectZkRouteFrozenRevert(ZkCoProcessorType zkType, bytes4 selector) internal { + vm.expectRevert(abi.encodeWithSelector(NitroEnclaveVerifier.ZkRouteFrozen.selector, zkType, selector)); + } + + function _addAndFreezeVerifyRoute(ZkCoProcessorType zkType, bytes4 selector, address routeVerifier) internal { + verifier.addVerifyRoute(zkType, selector, routeVerifier); + verifier.freezeVerifyRoute(zkType, selector); + } + + function _assertVerificationResult(VerifierJournal memory journal, VerificationResult expected) internal pure { + assertEq(uint8(journal.result), uint8(expected)); + } + function _verifyRiscZeroJournal(VerifierJournal memory journal) internal returns (VerifierJournal memory) { bytes memory output = abi.encode(journal); - bytes memory proofBytes = abi.encodePacked(bytes4(0), bytes32(0)); + bytes memory proofBytes = _proofBytes(); _mockRiscZeroVerify(VERIFIER_ID, output, proofBytes); vm.prank(submitter); - verifier.verify(output, ZkCoProcessorType.RiscZero, proofBytes); + return verifier.verify(output, ZkCoProcessorType.RiscZero, proofBytes); + } - assertEq(verifier.trustedIntermediateCerts(newCert), newCertExpiry); + function _verifySP1Journal(VerifierJournal memory journal) internal returns (VerifierJournal memory) { + bytes memory output = abi.encode(journal); + bytes memory proofBytes = _proofBytes(); + + _mockSP1Verify(VERIFIER_ID, output, proofBytes); + + vm.prank(submitter); + return verifier.verify(output, ZkCoProcessorType.Succinct, proofBytes); + } + + function _createBatchJournal( + bytes32 verifierVk, + uint256 outputCount + ) + internal + view + returns (BatchVerifierJournal memory) + { + VerifierJournal[] memory outputs = new VerifierJournal[](outputCount); + VerifierJournal memory journal = _createSuccessJournal(); + for (uint256 i; i < outputCount; ++i) { + outputs[i] = journal; + } + + return BatchVerifierJournal({ verifierVk: verifierVk, outputs: outputs }); } - function testRevokeCertSetsExpiryToZero() public { - assertEq(verifier.trustedIntermediateCerts(INTERMEDIATE_CERT_1), INTERMEDIATE_CERT_1_EXPIRY); - verifier.revokeCert(INTERMEDIATE_CERT_1); - assertEq(verifier.trustedIntermediateCerts(INTERMEDIATE_CERT_1), 0); + function _batchVerifyRiscZero(BatchVerifierJournal memory batchJournal) + internal + returns (VerifierJournal[] memory) + { + (bytes memory output, bytes memory proofBytes) = _mockRiscZeroBatchVerify(batchJournal); + + vm.prank(submitter); + return verifier.batchVerify(output, ZkCoProcessorType.RiscZero, proofBytes); } - // ============ Helpers ============ + function _batchVerifySP1(BatchVerifierJournal memory batchJournal) internal returns (VerifierJournal[] memory) { + bytes memory output = abi.encode(batchJournal); + bytes memory proofBytes = _proofBytes(); - function _setUpRiscZeroConfig() internal { - ZkCoProcessorConfig memory config = ZkCoProcessorConfig({ - verifierId: VERIFIER_ID, aggregatorId: AGGREGATOR_ID, zkVerifier: mockRiscZeroVerifier - }); - verifier.setZkConfiguration(ZkCoProcessorType.RiscZero, config, VERIFIER_PROOF_ID); + _mockSP1Verify(AGGREGATOR_ID, output, proofBytes); + + vm.prank(submitter); + return verifier.batchVerify(output, ZkCoProcessorType.Succinct, proofBytes); } - function _setUpSP1Config() internal { - ZkCoProcessorConfig memory config = - ZkCoProcessorConfig({ verifierId: VERIFIER_ID, aggregatorId: AGGREGATOR_ID, zkVerifier: mockSP1Verifier }); - verifier.setZkConfiguration(ZkCoProcessorType.Succinct, config, VERIFIER_PROOF_ID); + function _mockRiscZeroBatchVerify(BatchVerifierJournal memory batchJournal) + internal + returns (bytes memory output, bytes memory proofBytes) + { + output = abi.encode(batchJournal); + proofBytes = _proofBytes(); + + _mockRiscZeroVerify(AGGREGATOR_ID, output, proofBytes); + } + + function _proofBytes() internal pure returns (bytes memory) { + return abi.encodePacked(DEFAULT_PROOF_SELECTOR, bytes32(0)); } function _createSuccessJournal() internal view returns (VerifierJournal memory) { @@ -966,9 +826,41 @@ contract NitroEnclaveVerifierTest is Test { certs[1] = INTERMEDIATE_CERT_1; uint64[] memory expiries = new uint64[](2); - expiries[0] = INTERMEDIATE_CERT_1_EXPIRY + 100_000_000; // root expiry (far future) + expiries[0] = ROOT_CERT_EXPIRY; expiries[1] = INTERMEDIATE_CERT_1_EXPIRY; + return _successJournal(certs, expiries); + } + + function _createSuccessJournalWithLeaf( + bytes32 leafCert, + uint64 leafExpiry + ) + internal + view + returns (VerifierJournal memory) + { + bytes32[] memory certs = new bytes32[](3); + certs[0] = ROOT_CERT; + certs[1] = INTERMEDIATE_CERT_1; + certs[2] = leafCert; + + uint64[] memory expiries = new uint64[](3); + expiries[0] = ROOT_CERT_EXPIRY; + expiries[1] = INTERMEDIATE_CERT_1_EXPIRY; + expiries[2] = leafExpiry; + + return _successJournal(certs, expiries); + } + + function _successJournal( + bytes32[] memory certs, + uint64[] memory expiries + ) + internal + view + returns (VerifierJournal memory) + { Pcr[] memory pcrs = new Pcr[](0); return VerifierJournal({ @@ -986,24 +878,16 @@ contract NitroEnclaveVerifierTest is Test { } function _mockRiscZeroVerify(bytes32 programId, bytes memory output, bytes memory proofBytes) internal { - // IRiscZeroVerifier.verify(proofBytes, programId, sha256(output)) vm.mockCall( mockRiscZeroVerifier, - abi.encodeWithSelector( - bytes4(keccak256("verify(bytes,bytes32,bytes32)")), proofBytes, programId, sha256(output) - ), + abi.encodeWithSelector(RISC_ZERO_VERIFY_SELECTOR, proofBytes, programId, sha256(output)), "" ); } function _mockSP1Verify(bytes32 programId, bytes memory output, bytes memory proofBytes) internal { - // ISP1Verifier.verifyProof(programVKey, publicValues, proofBytes) vm.mockCall( - mockSP1Verifier, - abi.encodeWithSelector( - bytes4(keccak256("verifyProof(bytes32,bytes,bytes)")), programId, output, proofBytes - ), - "" + mockSP1Verifier, abi.encodeWithSelector(SP1_VERIFY_PROOF_SELECTOR, programId, output, proofBytes), "" ); } } diff --git a/test/L1/proofs/Nullify.t.sol b/test/L1/proofs/Nullify.t.sol index b70b74b8..d9f00a1e 100644 --- a/test/L1/proofs/Nullify.t.sol +++ b/test/L1/proofs/Nullify.t.sol @@ -10,235 +10,192 @@ import { AggregateVerifier } from "src/L1/proofs/AggregateVerifier.sol"; import { BaseTest } from "./BaseTest.t.sol"; contract NullifyTest is BaseTest { - function testNullifyWithTEEProof() public { - currentL2BlockNumber += BLOCK_INTERVAL; + uint256 private constant LAST_INTERMEDIATE_ROOT_INDEX = BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1; + uint256 private constant NO_PROOF_CREDIT_CLAIM_DELAY = 14 days; - Claim rootClaim1 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee1"))); - bytes memory teeProof1 = _generateProof("tee-proof-1", AggregateVerifier.ProofType.TEE); - - AggregateVerifier game = _createAggregateVerifierGame( - TEE_PROVER, rootClaim1, currentL2BlockNumber, address(anchorStateRegistry), teeProof1 + function testNullifyWithTEEProof() public { + AggregateVerifier game = _createGame( + TEE_PROVER, "tee1", "tee-proof-1", AggregateVerifier.ProofType.TEE, address(anchorStateRegistry) ); - Claim rootClaim2 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee2"))); - bytes memory teeProof2 = _generateProof("tee-proof-2", AggregateVerifier.ProofType.TEE); - - game.nullify(teeProof2, BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1, rootClaim2.raw()); - - assertEq(uint8(game.status()), uint8(GameStatus.IN_PROGRESS)); - assertEq(game.bondRecipient(), TEE_PROVER); - assertEq(game.proofCount(), 0); - assertEq(game.expectedResolution().raw(), type(uint64).max); - - // expectedResolution is uint64.max (no proofs left), so must wait 14 days from creation - vm.warp(block.timestamp + 14 days); + _nullify(game, "tee-proof-2", AggregateVerifier.ProofType.TEE, "tee2"); + _assertNullifiedToNoProofs(game, TEE_PROVER); - uint256 balanceBefore = game.gameCreator().balance; - game.claimCredit(); - vm.warp(block.timestamp + DELAYED_WETH_DELAY); - game.claimCredit(); - assertEq(game.gameCreator().balance, balanceBefore + INIT_BOND); - assertEq(delayedWETH.balanceOf(address(game)), 0); + vm.warp(block.timestamp + NO_PROOF_CREDIT_CLAIM_DELAY); + _claimCreditAfterDelay(game); } function testNullifyWithZKProof() public { - currentL2BlockNumber += BLOCK_INTERVAL; - - Claim rootClaim1 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "zk1"))); - bytes memory zkProof1 = _generateProof("zk-proof-1", AggregateVerifier.ProofType.ZK); - - AggregateVerifier game1 = _createAggregateVerifierGame( - ZK_PROVER, rootClaim1, currentL2BlockNumber, address(anchorStateRegistry), zkProof1 - ); - - Claim rootClaim2 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "zk2"))); - bytes memory zkProof2 = _generateProof("zk-proof-2", AggregateVerifier.ProofType.ZK); - - game1.nullify(zkProof2, BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1, rootClaim2.raw()); - - assertEq(uint8(game1.status()), uint8(GameStatus.IN_PROGRESS)); - assertEq(game1.bondRecipient(), ZK_PROVER); - assertEq(game1.proofCount(), 0); - assertEq(game1.expectedResolution().raw(), type(uint64).max); + AggregateVerifier game = + _createGame(ZK_PROVER, "zk1", "zk-proof-1", AggregateVerifier.ProofType.ZK, address(anchorStateRegistry)); - // expectedResolution is uint64.max (no proofs left), so must wait 14 days from creation - vm.warp(block.timestamp + 14 days); + _nullify(game, "zk-proof-2", AggregateVerifier.ProofType.ZK, "zk2"); + _assertNullifiedToNoProofs(game, ZK_PROVER); - uint256 balanceBefore = game1.gameCreator().balance; - game1.claimCredit(); - vm.warp(block.timestamp + DELAYED_WETH_DELAY); - game1.claimCredit(); - assertEq(game1.gameCreator().balance, balanceBefore + INIT_BOND); - assertEq(delayedWETH.balanceOf(address(game1)), 0); + vm.warp(block.timestamp + NO_PROOF_CREDIT_CLAIM_DELAY); + _claimCreditAfterDelay(game); } function testNullifyWithTEEProofWhenTEEAndZKProofsAreProvided() public { - currentL2BlockNumber += BLOCK_INTERVAL; - - Claim rootClaim1 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee1"))); - bytes memory teeProof1 = _generateProof("tee-proof-1", AggregateVerifier.ProofType.TEE); - - AggregateVerifier game = _createAggregateVerifierGame( - TEE_PROVER, rootClaim1, currentL2BlockNumber, address(anchorStateRegistry), teeProof1 + AggregateVerifier game = _createGame( + TEE_PROVER, "tee1", "tee-proof-1", AggregateVerifier.ProofType.TEE, address(anchorStateRegistry) ); - bytes memory zkProof = _generateProof("zk-proof-2", AggregateVerifier.ProofType.ZK); - game.verifyProposalProof(zkProof); + _provideProof(game, ZK_PROVER, _generateProof("zk-proof-2", AggregateVerifier.ProofType.ZK)); - assertEq(game.expectedResolution().raw(), block.timestamp + 1 days); + assertEq(game.expectedResolution().raw(), block.timestamp + game.FAST_FINALIZATION_DELAY()); - Claim rootClaim2 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee2"))); - bytes memory teeProof2 = _generateProof("tee-proof-2", AggregateVerifier.ProofType.TEE); - game.nullify(teeProof2, BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1, rootClaim2.raw()); + _nullify(game, "tee-proof-2", AggregateVerifier.ProofType.TEE, "tee2"); - assertEq(uint8(game.status()), uint8(GameStatus.IN_PROGRESS)); + _assertStatus(game, GameStatus.IN_PROGRESS); assertEq(game.bondRecipient(), TEE_PROVER); assertEq(game.proofCount(), 1); - assertEq(game.expectedResolution().raw(), block.timestamp + 7 days); + assertEq(game.expectedResolution().raw(), block.timestamp + game.SLOW_FINALIZATION_DELAY()); } function testZKNullifyFailsIfNoZKProof() public { - currentL2BlockNumber += BLOCK_INTERVAL; - - Claim rootClaim1 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee1"))); - bytes memory teeProof = _generateProof("tee-proof", AggregateVerifier.ProofType.TEE); - - AggregateVerifier game1 = _createAggregateVerifierGame( - TEE_PROVER, rootClaim1, currentL2BlockNumber, address(anchorStateRegistry), teeProof - ); - - Claim rootClaim2 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee2"))); - bytes memory zkProof = _generateProof("zk-proof", AggregateVerifier.ProofType.ZK); + AggregateVerifier game = + _createGame(TEE_PROVER, "tee1", "tee-proof", AggregateVerifier.ProofType.TEE, address(anchorStateRegistry)); vm.expectRevert(abi.encodeWithSelector(AggregateVerifier.MissingProof.selector, AggregateVerifier.ProofType.ZK)); - game1.nullify(zkProof, BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1, rootClaim2.raw()); + _nullify(game, "zk-proof", AggregateVerifier.ProofType.ZK, "tee2"); } function testNullifyFailsIfGameAlreadyResolved() public { - currentL2BlockNumber += BLOCK_INTERVAL; - - Claim rootClaim1 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee1"))); - bytes memory teeProof1 = _generateProof("tee-proof-1", AggregateVerifier.ProofType.TEE); - - AggregateVerifier game1 = _createAggregateVerifierGame( - TEE_PROVER, rootClaim1, currentL2BlockNumber, address(anchorStateRegistry), teeProof1 + AggregateVerifier game = _createGame( + TEE_PROVER, "tee1", "tee-proof-1", AggregateVerifier.ProofType.TEE, address(anchorStateRegistry) ); - // Resolve game1 - vm.warp(block.timestamp + 7 days); - game1.resolve(); - - // Try to nullify game1 - Claim rootClaim2 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "zk"))); - bytes memory teeProof2 = _generateProof("tee-proof-2", AggregateVerifier.ProofType.TEE); + vm.warp(block.timestamp + game.SLOW_FINALIZATION_DELAY()); + game.resolve(); vm.expectRevert(ClaimAlreadyResolved.selector); - game1.nullify(teeProof2, BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1, rootClaim2.raw()); + _nullify(game, "tee-proof-2", AggregateVerifier.ProofType.TEE, "zk"); } function testNullifyCanOverrideChallenge() public { - currentL2BlockNumber += BLOCK_INTERVAL; - - Claim rootClaim1 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee1"))); - bytes memory teeProof1 = _generateProof("tee-proof-1", AggregateVerifier.ProofType.TEE); - - AggregateVerifier game1 = _createAggregateVerifierGame( - TEE_PROVER, rootClaim1, currentL2BlockNumber, address(anchorStateRegistry), teeProof1 + AggregateVerifier game = _createGame( + TEE_PROVER, "tee1", "tee-proof-1", AggregateVerifier.ProofType.TEE, address(anchorStateRegistry) ); - // Challenge game1 with ZK proof - Claim rootClaim2 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "zk"))); - bytes memory zkProof = _generateProof("zk-proof", AggregateVerifier.ProofType.ZK); - vm.prank(ZK_PROVER); - game1.challenge(zkProof, BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1, rootClaim2.raw()); - - // Nullify can override challenge - bytes memory zkProof2 = _generateProof("zk-proof-2", AggregateVerifier.ProofType.ZK); - game1.nullify(zkProof2, BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1, rootClaim1.raw()); + game.challenge( + _generateProof("zk-proof", AggregateVerifier.ProofType.ZK), LAST_INTERMEDIATE_ROOT_INDEX, _claim("zk").raw() + ); - assertEq(game1.bondRecipient(), TEE_PROVER); + _nullify(game, "zk-proof-2", AggregateVerifier.ProofType.ZK, "tee1"); - // After nullify, only TEE proof remains; expectedResolution = now + 7 days - vm.warp(block.timestamp + 7 days); - game1.resolve(); + assertEq(game.bondRecipient(), TEE_PROVER); + assertEq(game.expectedResolution().raw(), block.timestamp + game.SLOW_FINALIZATION_DELAY()); - uint256 balanceBefore = game1.gameCreator().balance; - game1.claimCredit(); - vm.warp(block.timestamp + DELAYED_WETH_DELAY); - game1.claimCredit(); - assertEq(game1.gameCreator().balance, balanceBefore + INIT_BOND); - assertEq(delayedWETH.balanceOf(address(game1)), 0); + vm.warp(block.timestamp + game.SLOW_FINALIZATION_DELAY()); + game.resolve(); + _claimCreditAfterDelay(game); } - /// @notice `resolve` runs `_updateProofCount`; when the shared TEE verifier was nullified by another game, - /// refutation persists and `resolve` returns early `IN_PROGRESS` (no `Resolved` event) instead of - /// reverting. @dev All clones share the same `MockVerifier` TEE instance; `Verifier.nullify` requires a proper - /// factory game. function testResolveEarlyReturnWhenSharedTeeVerifierNullifiedByAnotherGame() public { - currentL2BlockNumber += BLOCK_INTERVAL; + _assertResolveEarlyReturnWhenSharedVerifierNullifiedByAnotherGame(TEE_PROVER, AggregateVerifier.ProofType.TEE); + } + + function testResolveEarlyReturnWhenSharedZkVerifierNullifiedByAnotherGame() public { + _assertResolveEarlyReturnWhenSharedVerifierNullifiedByAnotherGame(ZK_PROVER, AggregateVerifier.ProofType.ZK); + } - Claim rootClaimA = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "game-a"))); - bytes memory teeProofA = _generateProof("tee-proof-a", AggregateVerifier.ProofType.TEE); - AggregateVerifier gameA = _createAggregateVerifierGame( - TEE_PROVER, rootClaimA, currentL2BlockNumber, address(anchorStateRegistry), teeProofA + /// @notice With TEE + ZK, the fast window is 1 day. Another game nullifies the shared ZK verifier; the first + /// `resolve` persists the ZK refutation and returns `IN_PROGRESS`. After `SLOW_FINALIZATION_DELAY` + /// (7 days) from that moment, a second `resolve` finalizes with only the TEE proof. + function testTwoProofsResolveDelayedAfterExternalVerifierNullify() public { + AggregateVerifier gameA = _createGame( + TEE_PROVER, "dual-a", "tee-dual-a", AggregateVerifier.ProofType.TEE, address(anchorStateRegistry) ); - currentL2BlockNumber += BLOCK_INTERVAL; + _provideProof(gameA, ZK_PROVER, _generateProof("zk-dual-a", AggregateVerifier.ProofType.ZK)); - Claim rootClaimB = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "game-b"))); - bytes memory teeProofB = _generateProof("tee-proof-b", AggregateVerifier.ProofType.TEE); - AggregateVerifier gameB = - _createAggregateVerifierGame(TEE_PROVER, rootClaimB, currentL2BlockNumber, address(gameA), teeProofB); + assertEq(gameA.proofCount(), 2); + assertEq(gameA.expectedResolution().raw(), block.timestamp + gameA.FAST_FINALIZATION_DELAY()); - vm.warp(block.timestamp + 7 days); + vm.warp(block.timestamp + gameA.FAST_FINALIZATION_DELAY()); assertTrue(gameA.gameOver()); - assertEq(gameA.proofCount(), 1); - Claim rootClaimNullify = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "nullify-b"))); - bytes memory teeProofNullify = _generateProof("tee-nullify-b", AggregateVerifier.ProofType.TEE); - uint256 lastIntermediateIdx = BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1; - gameB.nullify(teeProofNullify, lastIntermediateIdx, rootClaimNullify.raw()); + AggregateVerifier gameB = + _createGame(ZK_PROVER, "dual-b", "zk-dual-b", AggregateVerifier.ProofType.ZK, address(gameA)); - assertTrue(teeVerifier.nullified()); - assertEq(gameA.proofCount(), 1); + _nullify(gameB, "zk-nullify-dual", AggregateVerifier.ProofType.ZK, "dual-nullify-b"); + assertTrue(zkVerifier.nullified()); assertEq(uint8(gameA.resolve()), uint8(GameStatus.IN_PROGRESS)); - assertEq(gameA.proofCount(), 0); - assertEq(gameA.expectedResolution().raw(), type(uint64).max); + assertEq(gameA.proofCount(), 1); + assertEq(gameA.expectedResolution().raw(), block.timestamp + gameA.SLOW_FINALIZATION_DELAY()); - vm.expectRevert(AggregateVerifier.GameNotOver.selector); - gameA.resolve(); + vm.warp(block.timestamp + gameA.SLOW_FINALIZATION_DELAY()); + assertEq(uint8(gameA.resolve()), uint8(GameStatus.DEFENDER_WINS)); + _assertStatus(gameA, GameStatus.DEFENDER_WINS); } - /// @notice Same as `testResolveEarlyReturnWhenSharedTeeVerifierNullifiedByAnotherGame` but for the shared ZK - /// verifier. - function testResolveEarlyReturnWhenSharedZkVerifierNullifiedByAnotherGame() public { + function _createGame( + address prover, + bytes memory claimSalt, + bytes memory proofSalt, + AggregateVerifier.ProofType proofType, + address parent + ) + private + returns (AggregateVerifier) + { currentL2BlockNumber += BLOCK_INTERVAL; - - Claim rootClaimA = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "zk-game-a"))); - bytes memory zkProofA = _generateProof("zk-proof-a", AggregateVerifier.ProofType.ZK); - AggregateVerifier gameA = _createAggregateVerifierGame( - ZK_PROVER, rootClaimA, currentL2BlockNumber, address(anchorStateRegistry), zkProofA + return _createAggregateVerifierGame( + prover, _claim(claimSalt), currentL2BlockNumber, parent, _generateProof(proofSalt, proofType) ); + } - currentL2BlockNumber += BLOCK_INTERVAL; + function _claim(bytes memory salt) private view returns (Claim) { + return Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, salt))); + } - Claim rootClaimB = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "zk-game-b"))); - bytes memory zkProofB = _generateProof("zk-proof-b", AggregateVerifier.ProofType.ZK); - AggregateVerifier gameB = - _createAggregateVerifierGame(ZK_PROVER, rootClaimB, currentL2BlockNumber, address(gameA), zkProofB); + function _nullify( + AggregateVerifier game, + bytes memory proofSalt, + AggregateVerifier.ProofType proofType, + bytes memory claimSalt + ) + private + { + game.nullify(_generateProof(proofSalt, proofType), LAST_INTERMEDIATE_ROOT_INDEX, _claim(claimSalt).raw()); + } - vm.warp(block.timestamp + 7 days); + function _assertNullifiedToNoProofs(AggregateVerifier game, address expectedBondRecipient) private view { + _assertStatus(game, GameStatus.IN_PROGRESS); + assertEq(game.bondRecipient(), expectedBondRecipient); + assertEq(game.proofCount(), 0); + assertEq(game.expectedResolution().raw(), type(uint64).max); + } + + function _assertStatus(AggregateVerifier game, GameStatus expectedStatus) private view { + assertEq(uint8(game.status()), uint8(expectedStatus)); + } + + /// @notice When a shared verifier is nullified by another game, `resolve` persists the refutation and returns + /// early `IN_PROGRESS` instead of reverting. + function _assertResolveEarlyReturnWhenSharedVerifierNullifiedByAnotherGame( + address prover, + AggregateVerifier.ProofType proofType + ) + private + { + AggregateVerifier gameA = _createGame(prover, "game-a", "proof-a", proofType, address(anchorStateRegistry)); + AggregateVerifier gameB = _createGame(prover, "game-b", "proof-b", proofType, address(gameA)); + + vm.warp(block.timestamp + gameA.SLOW_FINALIZATION_DELAY()); assertTrue(gameA.gameOver()); assertEq(gameA.proofCount(), 1); - Claim rootClaimNullify = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "zk-nullify-b"))); - bytes memory zkProofNullify = _generateProof("zk-nullify-b", AggregateVerifier.ProofType.ZK); - uint256 lastIntermediateIdx = BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1; - gameB.nullify(zkProofNullify, lastIntermediateIdx, rootClaimNullify.raw()); + _nullify(gameB, "nullify-proof", proofType, "nullify-claim"); - assertTrue(zkVerifier.nullified()); + if (proofType == AggregateVerifier.ProofType.TEE) { + assertTrue(teeVerifier.nullified()); + } else { + assertTrue(zkVerifier.nullified()); + } assertEq(gameA.proofCount(), 1); assertEq(uint8(gameA.resolve()), uint8(GameStatus.IN_PROGRESS)); @@ -249,46 +206,13 @@ contract NullifyTest is BaseTest { gameA.resolve(); } - /// @notice With TEE + ZK, the fast window is 1 day. Another game nullifies the shared ZK verifier; the first - /// `resolve` persists the ZK refutation and returns `IN_PROGRESS`. After `SLOW_FINALIZATION_DELAY` - /// (7 days) from that moment, a second `resolve` finalizes with only the TEE proof. - function testTwoProofsResolveDelayedAfterExternalVerifierNullify() public { - currentL2BlockNumber += BLOCK_INTERVAL; - - Claim rootClaimA = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "dual-a"))); - bytes memory teeProofA = _generateProof("tee-dual-a", AggregateVerifier.ProofType.TEE); - AggregateVerifier gameA = _createAggregateVerifierGame( - TEE_PROVER, rootClaimA, currentL2BlockNumber, address(anchorStateRegistry), teeProofA - ); - - bytes memory zkProofA = _generateProof("zk-dual-a", AggregateVerifier.ProofType.ZK); - vm.prank(ZK_PROVER); - gameA.verifyProposalProof(zkProofA); - - assertEq(gameA.proofCount(), 2); - assertEq(gameA.expectedResolution().raw(), block.timestamp + 1 days); - - vm.warp(block.timestamp + 1 days); - assertTrue(gameA.gameOver()); - - currentL2BlockNumber += BLOCK_INTERVAL; - Claim rootClaimB = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "dual-b"))); - bytes memory zkProofB = _generateProof("zk-dual-b", AggregateVerifier.ProofType.ZK); - AggregateVerifier gameB = - _createAggregateVerifierGame(ZK_PROVER, rootClaimB, currentL2BlockNumber, address(gameA), zkProofB); - - Claim rootClaimNullify = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "dual-nullify-b"))); - bytes memory zkProofNullify = _generateProof("zk-nullify-dual", AggregateVerifier.ProofType.ZK); - uint256 lastIntermediateIdx = BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1; - gameB.nullify(zkProofNullify, lastIntermediateIdx, rootClaimNullify.raw()); - assertTrue(zkVerifier.nullified()); - - assertEq(uint8(gameA.resolve()), uint8(GameStatus.IN_PROGRESS)); - assertEq(gameA.proofCount(), 1); - assertEq(gameA.expectedResolution().raw(), block.timestamp + 7 days); - - vm.warp(block.timestamp + 7 days); - assertEq(uint8(gameA.resolve()), uint8(GameStatus.DEFENDER_WINS)); - assertEq(uint8(gameA.status()), uint8(GameStatus.DEFENDER_WINS)); + function _claimCreditAfterDelay(AggregateVerifier game) private { + address recipient = game.gameCreator(); + uint256 balanceBefore = recipient.balance; + game.claimCredit(); + vm.warp(block.timestamp + DELAYED_WETH_DELAY); + game.claimCredit(); + assertEq(recipient.balance, balanceBefore + INIT_BOND); + assertEq(delayedWETH.balanceOf(address(game)), 0); } } diff --git a/test/L1/proofs/TEEProverRegistry.t.sol b/test/L1/proofs/TEEProverRegistry.t.sol index c9c4c74b..b0df4960 100644 --- a/test/L1/proofs/TEEProverRegistry.t.sol +++ b/test/L1/proofs/TEEProverRegistry.t.sol @@ -6,7 +6,6 @@ import { Test } from "lib/forge-std/src/Test.sol"; import { TransparentUpgradeableProxy } from "lib/openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; -import { ProxyAdmin } from "src/universal/ProxyAdmin.sol"; import { INitroEnclaveVerifier } from "interfaces/L1/proofs/tee/INitroEnclaveVerifier.sol"; import { IDisputeGameFactory } from "interfaces/L1/proofs/IDisputeGameFactory.sol"; @@ -17,48 +16,38 @@ import { IDisputeGame } from "interfaces/L1/proofs/IDisputeGame.sol"; import { DevTEEProverRegistry } from "test/mocks/MockDevTEEProverRegistry.sol"; import { TEEProverRegistry } from "src/L1/proofs/tee/TEEProverRegistry.sol"; -/// @notice Mock AggregateVerifier that returns a configurable TEE_IMAGE_HASH. contract MockAggregateVerifierForRegistry { - bytes32 public TEE_IMAGE_HASH; + bytes32 public immutable TEE_IMAGE_HASH; constructor(bytes32 imageHash) { TEE_IMAGE_HASH = imageHash; } } -/// @notice Mock DisputeGameFactory that returns a fixed game implementation. contract MockDisputeGameFactoryForRegistry { - mapping(uint32 => address) internal _impls; + IDisputeGame internal immutable _impl; - function setImpl(uint32 gameType, address impl) external { - _impls[gameType] = impl; + constructor(address impl) { + _impl = IDisputeGame(impl); } - function gameImpls(GameType gameType) external view returns (IDisputeGame) { - return IDisputeGame(_impls[GameType.unwrap(gameType)]); + function gameImpls(GameType) external view returns (IDisputeGame) { + return _impl; } } -/// @notice Tests for TEEProverRegistry and DevTEEProverRegistry contracts. -/// @dev IMPORTANT: This test file uses DevTEEProverRegistry as the implementation because -/// registering signers on the production TEEProverRegistry requires a ZK proof of a valid -/// AWS Nitro attestation, which cannot be generated in a test environment. DevTEEProverRegistry extends -/// TEEProverRegistry with an `addDevSigner` function that bypasses attestation verification, -/// allowing us to test all signer-related functionality. All tests for base TEEProverRegistry -/// functionality (ownership, proposer, etc.) are equally valid since -/// DevTEEProverRegistry inherits from TEEProverRegistry without modifying those functions. +/// @dev Uses DevTEEProverRegistry because production signer registration requires a Nitro attestation proof. contract TEEProverRegistryTest is Test { DevTEEProverRegistry public teeProverRegistry; - ProxyAdmin public proxyAdmin; - MockDisputeGameFactoryForRegistry public mockFactory; - MockAggregateVerifierForRegistry public mockVerifier; address public owner; address public manager; address public unauthorized; bytes32 public constant TEST_IMAGE_HASH = keccak256("test-image-hash"); - uint32 public constant TEST_GAME_TYPE = 621; + GameType public constant TEST_GAME_TYPE = GameType.wrap(621); + string internal constant NOT_OWNER = "OwnableManaged: caller is not the owner"; + string internal constant NOT_OWNER_OR_MANAGER = "OwnableManaged: caller is not the owner or the manager"; // Events must be redeclared here because Solidity 0.8.15 doesn't support // referencing events from other contracts via qualified names (requires 0.8.21+) @@ -71,31 +60,55 @@ contract TEEProverRegistryTest is Test { manager = makeAddr("manager"); unauthorized = makeAddr("unauthorized"); - // Deploy mock factory and verifier - mockVerifier = new MockAggregateVerifierForRegistry(TEST_IMAGE_HASH); - mockFactory = new MockDisputeGameFactoryForRegistry(); - mockFactory.setImpl(TEST_GAME_TYPE, address(mockVerifier)); + teeProverRegistry = _deployRegistry(new address[](0), TEST_GAME_TYPE); + } - // Deploy implementation (using DevTEEProverRegistry for test flexibility) - DevTEEProverRegistry impl = - new DevTEEProverRegistry(INitroEnclaveVerifier(address(0)), IDisputeGameFactory(address(mockFactory))); + function _deployRegistry(address[] memory proposers, GameType gameType) internal returns (DevTEEProverRegistry) { + MockAggregateVerifierForRegistry verifier = new MockAggregateVerifierForRegistry(TEST_IMAGE_HASH); + MockDisputeGameFactoryForRegistry factory = new MockDisputeGameFactoryForRegistry(address(verifier)); - // Deploy proxy admin - proxyAdmin = new ProxyAdmin(address(this)); + DevTEEProverRegistry impl = + new DevTEEProverRegistry(INitroEnclaveVerifier(address(0)), IDisputeGameFactory(address(factory))); - // Deploy proxy TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy( address(impl), - address(proxyAdmin), - abi.encodeCall( - TEEProverRegistry.initialize, (owner, manager, new address[](0), GameType.wrap(TEST_GAME_TYPE)) - ) + makeAddr("proxy-admin"), + abi.encodeCall(TEEProverRegistry.initialize, (owner, manager, proposers, gameType)) ); - teeProverRegistry = DevTEEProverRegistry(address(proxy)); + return DevTEEProverRegistry(address(proxy)); } - // ============ Initialization Tests ============ + function _addDevSigner(address signer) internal { + vm.prank(owner); + teeProverRegistry.addDevSigner(signer, TEST_IMAGE_HASH); + } + + function _addDevSigners(address signer1, address signer2, address signer3) internal { + vm.startPrank(owner); + teeProverRegistry.addDevSigner(signer1, TEST_IMAGE_HASH); + teeProverRegistry.addDevSigner(signer2, TEST_IMAGE_HASH); + teeProverRegistry.addDevSigner(signer3, TEST_IMAGE_HASH); + vm.stopPrank(); + } + + function _expectNotOwnerRevert(address caller) internal { + vm.prank(caller); + vm.expectRevert(bytes(NOT_OWNER)); + } + + function _expectNotOwnerOrManagerRevert(address caller) internal { + vm.prank(caller); + vm.expectRevert(bytes(NOT_OWNER_OR_MANAGER)); + } + + function _assertContains(address[] memory values, address expected) internal { + for (uint256 i = 0; i < values.length; i++) { + if (values[i] == expected) return; + } + + fail(); + } function testInitialization() public view { assertEq(teeProverRegistry.owner(), owner); @@ -107,43 +120,27 @@ contract TEEProverRegistryTest is Test { address proposer1 = makeAddr("proposer1"); address proposer2 = makeAddr("proposer2"); address proposer3 = makeAddr("proposer3"); - DevTEEProverRegistry impl2 = - new DevTEEProverRegistry(INitroEnclaveVerifier(address(0)), IDisputeGameFactory(address(1))); - ProxyAdmin proxyAdmin2 = new ProxyAdmin(address(this)); address[] memory proposers = new address[](3); proposers[0] = proposer1; proposers[1] = proposer2; proposers[2] = proposer3; - TransparentUpgradeableProxy proxy2 = new TransparentUpgradeableProxy( - address(impl2), - address(proxyAdmin2), - abi.encodeCall(TEEProverRegistry.initialize, (owner, manager, proposers, GameType.wrap(0))) - ); - DevTEEProverRegistry registry2 = DevTEEProverRegistry(address(proxy2)); + + DevTEEProverRegistry registry2 = _deployRegistry(proposers, TEST_GAME_TYPE); assertTrue(registry2.isValidProposer(proposer1)); assertTrue(registry2.isValidProposer(proposer2)); assertTrue(registry2.isValidProposer(proposer3)); } function testInitializationWithEmptyProposers() public view { - // Default setUp uses an empty proposer array — no proposer should be set assertFalse(teeProverRegistry.isValidProposer(address(0))); } - // ============ Signer Deregistration Tests ============ - function testDeregisterSignerAsOwner() public { address signer = makeAddr("signer"); - // Add signer via DevTEEProverRegistry - vm.prank(owner); - teeProverRegistry.addDevSigner(signer, TEST_IMAGE_HASH); - - // Verify signer is registered - assertTrue(teeProverRegistry.isValidSigner(signer)); + _addDevSigner(signer); - // Deregister as owner - vm.expectEmit(true, false, false, false); + vm.expectEmit(true, false, false, false, address(teeProverRegistry)); emit SignerDeregistered(signer); vm.prank(owner); @@ -155,11 +152,7 @@ contract TEEProverRegistryTest is Test { function testDeregisterSignerAsManager() public { address signer = makeAddr("signer"); - // Add signer via DevTEEProverRegistry - vm.prank(owner); - teeProverRegistry.addDevSigner(signer, TEST_IMAGE_HASH); - - assertTrue(teeProverRegistry.isValidSigner(signer)); + _addDevSigner(signer); vm.prank(manager); teeProverRegistry.deregisterSigner(signer); @@ -170,17 +163,14 @@ contract TEEProverRegistryTest is Test { function testDeregisterSignerFailsIfUnauthorized() public { address signer = makeAddr("signer"); - vm.prank(unauthorized); - vm.expectRevert("OwnableManaged: caller is not the owner or the manager"); + _expectNotOwnerOrManagerRevert(unauthorized); teeProverRegistry.deregisterSigner(signer); } - // ============ Proposer Tests ============ - function testSetProposer() public { address newProposer = makeAddr("proposer"); - vm.expectEmit(true, false, false, false); + vm.expectEmit(true, false, false, false, address(teeProverRegistry)); emit ProposerSet(newProposer, true); vm.prank(owner); @@ -192,17 +182,13 @@ contract TEEProverRegistryTest is Test { function testSetProposerFailsIfNotOwner() public { address newProposer = makeAddr("proposer"); - vm.prank(manager); - vm.expectRevert("OwnableManaged: caller is not the owner"); + _expectNotOwnerRevert(manager); teeProverRegistry.setProposer(newProposer, true); - vm.prank(unauthorized); - vm.expectRevert("OwnableManaged: caller is not the owner"); + _expectNotOwnerRevert(unauthorized); teeProverRegistry.setProposer(newProposer, true); } - // ============ isValidSigner Tests ============ - function testIsValidSignerReturnsFalseForUnregistered() public { address unregistered = makeAddr("unregistered"); assertFalse(teeProverRegistry.isValidSigner(unregistered)); @@ -211,20 +197,15 @@ contract TEEProverRegistryTest is Test { function testIsValidSignerReturnsTrueForRegistered() public { address signer = makeAddr("signer"); - vm.prank(owner); - teeProverRegistry.addDevSigner(signer, TEST_IMAGE_HASH); + _addDevSigner(signer); assertTrue(teeProverRegistry.isValidSigner(signer)); } - // ============ MAX_AGE Tests ============ - function testMaxAgeConstant() public view { assertEq(teeProverRegistry.MAX_AGE(), 60 minutes); } - // ============ Ownership Transfer Tests ============ - function testTransferOwnership() public { address newOwner = makeAddr("newOwner"); @@ -237,12 +218,10 @@ contract TEEProverRegistryTest is Test { function testTransferOwnershipFailsIfNotOwner() public { address newOwner = makeAddr("newOwner"); - vm.prank(manager); - vm.expectRevert("OwnableManaged: caller is not the owner"); + _expectNotOwnerRevert(manager); teeProverRegistry.transferOwnership(newOwner); - vm.prank(unauthorized); - vm.expectRevert("OwnableManaged: caller is not the owner"); + _expectNotOwnerRevert(unauthorized); teeProverRegistry.transferOwnership(newOwner); } @@ -252,8 +231,6 @@ contract TEEProverRegistryTest is Test { teeProverRegistry.transferOwnership(address(0)); } - // ============ Management Transfer Tests ============ - function testTransferManagementAsOwner() public { address newManager = makeAddr("newManager"); @@ -275,8 +252,7 @@ contract TEEProverRegistryTest is Test { function testTransferManagementFailsIfUnauthorized() public { address newManager = makeAddr("newManager"); - vm.prank(unauthorized); - vm.expectRevert("OwnableManaged: caller is not the owner or the manager"); + _expectNotOwnerOrManagerRevert(unauthorized); teeProverRegistry.transferManagement(newManager); } @@ -286,8 +262,6 @@ contract TEEProverRegistryTest is Test { teeProverRegistry.transferManagement(address(0)); } - // ============ Renounce Tests ============ - function testRenounceOwnership() public { vm.prank(owner); teeProverRegistry.renounceOwnership(); @@ -309,12 +283,10 @@ contract TEEProverRegistryTest is Test { assertEq(teeProverRegistry.manager(), address(0)); } - // ============ DevTEEProverRegistry: addDevSigner Tests ============ - function testAddDevSigner() public { address signer = makeAddr("dev-signer"); - vm.expectEmit(true, false, false, false); + vm.expectEmit(true, false, false, false, address(teeProverRegistry)); emit SignerRegistered(signer); vm.prank(owner); @@ -326,25 +298,19 @@ contract TEEProverRegistryTest is Test { function testAddDevSignerFailsIfNotOwner() public { address signer = makeAddr("dev-signer"); - vm.prank(manager); - vm.expectRevert("OwnableManaged: caller is not the owner"); + _expectNotOwnerRevert(manager); teeProverRegistry.addDevSigner(signer, TEST_IMAGE_HASH); - vm.prank(unauthorized); - vm.expectRevert("OwnableManaged: caller is not the owner"); + _expectNotOwnerRevert(unauthorized); teeProverRegistry.addDevSigner(signer, TEST_IMAGE_HASH); } function testAddDevSignerIdempotent() public { address signer = makeAddr("dev-signer"); - vm.prank(owner); - teeProverRegistry.addDevSigner(signer, TEST_IMAGE_HASH); - assertTrue(teeProverRegistry.isValidSigner(signer)); + _addDevSigner(signer); + _addDevSigner(signer); - // Adding again should not revert - vm.prank(owner); - teeProverRegistry.addDevSigner(signer, TEST_IMAGE_HASH); assertTrue(teeProverRegistry.isValidSigner(signer)); } @@ -353,29 +319,21 @@ contract TEEProverRegistryTest is Test { address signer2 = makeAddr("dev-signer-2"); address signer3 = makeAddr("dev-signer-3"); - vm.startPrank(owner); - teeProverRegistry.addDevSigner(signer1, TEST_IMAGE_HASH); - teeProverRegistry.addDevSigner(signer2, TEST_IMAGE_HASH); - teeProverRegistry.addDevSigner(signer3, TEST_IMAGE_HASH); - vm.stopPrank(); + _addDevSigners(signer1, signer2, signer3); assertTrue(teeProverRegistry.isValidSigner(signer1)); assertTrue(teeProverRegistry.isValidSigner(signer2)); assertTrue(teeProverRegistry.isValidSigner(signer3)); } - // ============ getRegisteredSigners Tests ============ - function testGetRegisteredSignersEmpty() public view { - address[] memory signers = teeProverRegistry.getRegisteredSigners(); - assertEq(signers.length, 0); + assertEq(teeProverRegistry.getRegisteredSigners().length, 0); } function testGetRegisteredSignersAfterRegister() public { address signer = makeAddr("signer"); - vm.prank(owner); - teeProverRegistry.addDevSigner(signer, TEST_IMAGE_HASH); + _addDevSigner(signer); address[] memory signers = teeProverRegistry.getRegisteredSigners(); assertEq(signers.length, 1); @@ -385,16 +343,12 @@ contract TEEProverRegistryTest is Test { function testGetRegisteredSignersAfterDeregister() public { address signer = makeAddr("signer"); - vm.prank(owner); - teeProverRegistry.addDevSigner(signer, TEST_IMAGE_HASH); - - assertEq(teeProverRegistry.getRegisteredSigners().length, 1); + _addDevSigner(signer); vm.prank(owner); teeProverRegistry.deregisterSigner(signer); - address[] memory signers = teeProverRegistry.getRegisteredSigners(); - assertEq(signers.length, 0); + assertEq(teeProverRegistry.getRegisteredSigners().length, 0); } function testGetRegisteredSignersMultiple() public { @@ -402,27 +356,14 @@ contract TEEProverRegistryTest is Test { address signer2 = makeAddr("signer-2"); address signer3 = makeAddr("signer-3"); - vm.startPrank(owner); - teeProverRegistry.addDevSigner(signer1, TEST_IMAGE_HASH); - teeProverRegistry.addDevSigner(signer2, TEST_IMAGE_HASH); - teeProverRegistry.addDevSigner(signer3, TEST_IMAGE_HASH); - vm.stopPrank(); + _addDevSigners(signer1, signer2, signer3); address[] memory signers = teeProverRegistry.getRegisteredSigners(); assertEq(signers.length, 3); - // Verify all three are present (order not guaranteed) - bool foundSigner1; - bool foundSigner2; - bool foundSigner3; - for (uint256 i = 0; i < signers.length; i++) { - if (signers[i] == signer1) foundSigner1 = true; - if (signers[i] == signer2) foundSigner2 = true; - if (signers[i] == signer3) foundSigner3 = true; - } - assertTrue(foundSigner1); - assertTrue(foundSigner2); - assertTrue(foundSigner3); + _assertContains(signers, signer1); + _assertContains(signers, signer2); + _assertContains(signers, signer3); } function testGetRegisteredSignersConsistencyAfterMixedOperations() public { @@ -430,42 +371,26 @@ contract TEEProverRegistryTest is Test { address signer2 = makeAddr("signer-2"); address signer3 = makeAddr("signer-3"); - // Register three signers - vm.startPrank(owner); - teeProverRegistry.addDevSigner(signer1, TEST_IMAGE_HASH); - teeProverRegistry.addDevSigner(signer2, TEST_IMAGE_HASH); - teeProverRegistry.addDevSigner(signer3, TEST_IMAGE_HASH); - vm.stopPrank(); - - assertEq(teeProverRegistry.getRegisteredSigners().length, 3); + _addDevSigners(signer1, signer2, signer3); - // Deregister the middle one vm.prank(manager); teeProverRegistry.deregisterSigner(signer2); address[] memory signers = teeProverRegistry.getRegisteredSigners(); assertEq(signers.length, 2); - - // Mapping and set stay consistent - for (uint256 i = 0; i < signers.length; i++) { - assertTrue(teeProverRegistry.isValidSigner(signers[i])); - assertNotEq(signers[i], signer2); - } - - // Deregistered signer not in mapping either + _assertContains(signers, signer1); + _assertContains(signers, signer3); assertFalse(teeProverRegistry.isValidSigner(signer2)); } function testGetRegisteredSignersDeregisterIdempotent() public { address signer = makeAddr("signer"); - vm.prank(owner); - teeProverRegistry.addDevSigner(signer, TEST_IMAGE_HASH); + _addDevSigner(signer); vm.prank(owner); teeProverRegistry.deregisterSigner(signer); - // Deregistering again should not revert and set should still be empty vm.prank(owner); teeProverRegistry.deregisterSigner(signer); diff --git a/test/L1/proofs/TEEVerifier.t.sol b/test/L1/proofs/TEEVerifier.t.sol index 66ab646a..56f6968c 100644 --- a/test/L1/proofs/TEEVerifier.t.sol +++ b/test/L1/proofs/TEEVerifier.t.sol @@ -6,7 +6,6 @@ import { Test } from "lib/forge-std/src/Test.sol"; import { TransparentUpgradeableProxy } from "lib/openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; -import { ProxyAdmin } from "src/universal/ProxyAdmin.sol"; import { INitroEnclaveVerifier } from "interfaces/L1/proofs/tee/INitroEnclaveVerifier.sol"; import { IAnchorStateRegistry } from "interfaces/L1/proofs/IAnchorStateRegistry.sol"; @@ -19,171 +18,108 @@ import { DevTEEProverRegistry } from "test/mocks/MockDevTEEProverRegistry.sol"; import { TEEProverRegistry } from "src/L1/proofs/tee/TEEProverRegistry.sol"; import { TEEVerifier } from "src/L1/proofs/tee/TEEVerifier.sol"; -/// @notice Mock AggregateVerifier that returns a configurable TEE_IMAGE_HASH. contract MockAggregateVerifierForVerifier { - bytes32 public TEE_IMAGE_HASH; + bytes32 public immutable TEE_IMAGE_HASH; constructor(bytes32 imageHash) { TEE_IMAGE_HASH = imageHash; } } -/// @notice Mock DisputeGameFactory that returns a fixed game implementation. contract MockDisputeGameFactoryForVerifier { - mapping(uint32 => address) internal _impls; + IDisputeGame internal immutable _impl; - function setImpl(uint32 gameType_, address impl) external { - _impls[gameType_] = impl; + constructor(address impl) { + _impl = IDisputeGame(impl); } - function gameImpls(GameType gameType_) external view returns (IDisputeGame) { - return IDisputeGame(_impls[GameType.unwrap(gameType_)]); + function gameImpls(GameType) external view returns (IDisputeGame) { + return _impl; } } contract TEEVerifierTest is Test { TEEVerifier public verifier; DevTEEProverRegistry public teeProverRegistry; - ProxyAdmin public proxyAdmin; - MockAnchorStateRegistry public anchorStateRegistry; - // Test signer - we'll derive address from private key uint256 internal constant SIGNER_PRIVATE_KEY = 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef; - address internal signerAddress; - bytes32 internal constant IMAGE_ID = keccak256("test-image-id"); - uint32 internal constant TEST_GAME_TYPE = 621; + bytes32 internal constant TEST_JOURNAL = keccak256("test-journal"); + GameType internal constant TEST_GAME_TYPE = GameType.wrap(621); address internal immutable PROPOSER = makeAddr("proposer"); - address internal owner; - function setUp() public { - owner = address(this); - - // Derive signer address from private key - signerAddress = vm.addr(SIGNER_PRIVATE_KEY); - - // Deploy mock factory and verifier + address signerAddress = vm.addr(SIGNER_PRIVATE_KEY); MockAggregateVerifierForVerifier mockVerifier = new MockAggregateVerifierForVerifier(IMAGE_ID); - MockDisputeGameFactoryForVerifier mockFactory = new MockDisputeGameFactoryForVerifier(); - mockFactory.setImpl(TEST_GAME_TYPE, address(mockVerifier)); + MockDisputeGameFactoryForVerifier mockFactory = new MockDisputeGameFactoryForVerifier(address(mockVerifier)); - // Deploy implementation (NitroEnclaveVerifier not needed for dev signer tests) + // DevTEEProverRegistry keeps these tests focused on verifier behavior without Nitro attestation setup. DevTEEProverRegistry impl = new DevTEEProverRegistry(INitroEnclaveVerifier(address(0)), IDisputeGameFactory(address(mockFactory))); - // Deploy proxy admin - proxyAdmin = new ProxyAdmin(address(this)); - - // Deploy proxy TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy( address(impl), - address(proxyAdmin), + makeAddr("proxy-admin"), abi.encodeCall( - TEEProverRegistry.initialize, (owner, owner, new address[](0), GameType.wrap(TEST_GAME_TYPE)) + TEEProverRegistry.initialize, (address(this), address(this), new address[](0), TEST_GAME_TYPE) ) ); teeProverRegistry = DevTEEProverRegistry(address(proxy)); - - // Register the signer with the correct image hash teeProverRegistry.addDevSigner(signerAddress, IMAGE_ID); - - // Set the proposer as valid teeProverRegistry.setProposer(PROPOSER, true); - // Deploy TEEVerifier - anchorStateRegistry = new MockAnchorStateRegistry(); + MockAnchorStateRegistry anchorStateRegistry = new MockAnchorStateRegistry(); verifier = new TEEVerifier( TEEProverRegistry(address(teeProverRegistry)), IAnchorStateRegistry(address(anchorStateRegistry)) ); } function testVerifyValidSignature() public view { - // Create a journal hash - bytes32 journal = keccak256("test-journal"); - - // Sign the journal with the signer's private key - (uint8 v, bytes32 r, bytes32 s) = vm.sign(SIGNER_PRIVATE_KEY, journal); - bytes memory signature = abi.encodePacked(r, s, v); - - // Construct proof: proposer(20) + signature(65) = 85 bytes - bytes memory proofBytes = abi.encodePacked(PROPOSER, signature); - - // Verify should return true regardless of imageId (enforced via journal hash, not registry) - bool result = verifier.verify(proofBytes, IMAGE_ID, journal); - assertTrue(result); + assertTrue(verifier.verify(_proofFor(PROPOSER, SIGNER_PRIVATE_KEY, TEST_JOURNAL), IMAGE_ID, TEST_JOURNAL)); } function testVerifyFailsWithInvalidSignature() public { - bytes32 journal = keccak256("test-journal"); - - // Create an invalid signature (all zeros except v) - bytes memory invalidSignature = new bytes(65); - invalidSignature[64] = bytes1(uint8(27)); // Set v to 27 - - bytes memory proofBytes = abi.encodePacked(PROPOSER, invalidSignature); + bytes memory proofBytes = abi.encodePacked(PROPOSER, bytes32(0), bytes32(0), uint8(27)); vm.expectRevert(TEEVerifier.InvalidSignature.selector); - verifier.verify(proofBytes, IMAGE_ID, journal); + verifier.verify(proofBytes, IMAGE_ID, TEST_JOURNAL); } function testVerifyFailsWithInvalidProposer() public { - // Create a journal hash - bytes32 journal = keccak256("test-journal"); - - // Sign the journal with the signer's private key - (uint8 v, bytes32 r, bytes32 s) = vm.sign(SIGNER_PRIVATE_KEY, journal); - bytes memory signature = abi.encodePacked(r, s, v); - - // Construct proof: proposer(20) + signature(65) = 85 bytes - bytes memory proofBytes = abi.encodePacked(address(0), signature); + bytes memory proofBytes = _proofFor(address(0), SIGNER_PRIVATE_KEY, TEST_JOURNAL); vm.expectRevert(abi.encodeWithSelector(TEEVerifier.InvalidProposer.selector, address(0))); - verifier.verify(proofBytes, IMAGE_ID, journal); + verifier.verify(proofBytes, IMAGE_ID, TEST_JOURNAL); } function testVerifyFailsWithUnregisteredSigner() public { - // Use a different private key that's not registered uint256 unregisteredKey = 0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef; address unregisteredSigner = vm.addr(unregisteredKey); - bytes32 journal = keccak256("test-journal"); - - (uint8 v, bytes32 r, bytes32 s) = vm.sign(unregisteredKey, journal); - bytes memory signature = abi.encodePacked(r, s, v); - - bytes memory proofBytes = abi.encodePacked(PROPOSER, signature); - vm.expectRevert(abi.encodeWithSelector(TEEVerifier.InvalidSigner.selector, unregisteredSigner)); - verifier.verify(proofBytes, IMAGE_ID, journal); + verifier.verify(_proofFor(PROPOSER, unregisteredKey, TEST_JOURNAL), IMAGE_ID, TEST_JOURNAL); } function testVerifyFailsWithImageIdMismatch() public { - bytes32 journal = keccak256("test-journal"); - - (uint8 v, bytes32 r, bytes32 s) = vm.sign(SIGNER_PRIVATE_KEY, journal); - bytes memory signature = abi.encodePacked(r, s, v); - - bytes memory proofBytes = abi.encodePacked(PROPOSER, signature); - - // Different imageId should fail — signer was registered with IMAGE_ID bytes32 wrongImageId = keccak256("different-image"); vm.expectRevert(abi.encodeWithSelector(TEEVerifier.ImageIdMismatch.selector, IMAGE_ID, wrongImageId)); - verifier.verify(proofBytes, wrongImageId, journal); + verifier.verify(_proofFor(PROPOSER, SIGNER_PRIVATE_KEY, TEST_JOURNAL), wrongImageId, TEST_JOURNAL); } function testVerifyFailsWithInvalidProofFormat() public { - bytes32 journal = keccak256("test-journal"); - - // Proof too short (less than 85 bytes) bytes memory shortProof = new bytes(50); vm.expectRevert(TEEVerifier.InvalidProofFormat.selector); - verifier.verify(shortProof, IMAGE_ID, journal); + verifier.verify(shortProof, IMAGE_ID, TEST_JOURNAL); } function testConstants() public view { assertEq(address(verifier.TEE_PROVER_REGISTRY()), address(teeProverRegistry)); } + + function _proofFor(address proposer, uint256 privateKey, bytes32 journal) internal pure returns (bytes memory) { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, journal); + return abi.encodePacked(proposer, r, s, v); + } } diff --git a/test/L2/BaseFeeVault.t.sol b/test/L2/BaseFeeVault.t.sol index 48b48648..4abf4424 100644 --- a/test/L2/BaseFeeVault.t.sol +++ b/test/L2/BaseFeeVault.t.sol @@ -18,7 +18,6 @@ contract BaseFeeVault_Uncategorized_Test is FeeVault_Uncategorized_Test { function setUp() public virtual override { super.setUp(); recipient = deploy.cfg().baseFeeVaultRecipient(); - feeVaultName = "BaseFeeVault"; minWithdrawalAmount = deploy.cfg().baseFeeVaultMinimumWithdrawalAmount(); feeVault = IFeeVault(payable(Predeploys.BASE_FEE_VAULT)); withdrawalNetwork = Types.WithdrawalNetwork(uint8(deploy.cfg().baseFeeVaultWithdrawalNetwork())); diff --git a/test/L2/FeeDisburser.t.sol b/test/L2/FeeDisburser.t.sol index fea12580..75287a33 100644 --- a/test/L2/FeeDisburser.t.sol +++ b/test/L2/FeeDisburser.t.sol @@ -2,10 +2,10 @@ pragma solidity 0.8.25; import { Test } from "lib/forge-std/src/Test.sol"; -import { console } from "lib/forge-std/src/console.sol"; import { FeeDisburser } from "src/L2/FeeDisburser.sol"; import { IFeeVault, Types } from "interfaces/L2/IFeeVault.sol"; +import { IStandardBridge } from "interfaces/universal/IStandardBridge.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; /// @title FeeDisburserTest @@ -20,18 +20,19 @@ contract FeeDisburserTest is Test { uint32 constant WITHDRAWAL_MIN_GAS = 35_000; uint256 constant DEFAULT_MIN_WITHDRAWAL = 10 ether; uint256 constant DEFAULT_DISBURSEMENT_INTERVAL = 24 hours; + uint256 constant MAX_TEST_FEE = 10 ** 27; // Test addresses - address payable l1Wallet = payable(address(0x1001)); - address alice = address(0xA11CE); - address bob = address(0xB0B); + address payable constant L1_WALLET = payable(address(0x1001)); + address constant ALICE = address(0xA11CE); + address constant BOB = address(0xB0B); // Contract instances FeeDisburser feeDisburser; function setUp() public virtual { // Deploy FeeDisburser - feeDisburser = new FeeDisburser(l1Wallet, DEFAULT_DISBURSEMENT_INTERVAL); + feeDisburser = new FeeDisburser(L1_WALLET, DEFAULT_DISBURSEMENT_INTERVAL); // Warp time to allow first disbursement vm.warp(DEFAULT_DISBURSEMENT_INTERVAL + 1); @@ -57,27 +58,43 @@ contract FeeDisburserTest is Test { ) internal { - vm.mockCall(vault, abi.encodeWithSelector(IFeeVault.WITHDRAWAL_NETWORK.selector), abi.encode(network)); - vm.mockCall(vault, abi.encodeWithSelector(IFeeVault.RECIPIENT.selector), abi.encode(recipient)); - vm.mockCall(vault, abi.encodeWithSelector(IFeeVault.MIN_WITHDRAWAL_AMOUNT.selector), abi.encode(minWithdrawal)); - } - - /// @notice Helper to setup a FeeVault withdrawal (vault sends ETH to recipient on withdraw()) - function _setupVaultWithdrawal(address vault, uint256 balance) internal { - vm.deal(vault, balance); - // Mock withdraw() to send ETH to the feeDisburser - vm.mockCall(vault, abi.encodeWithSelector(IFeeVault.withdraw.selector), abi.encode(balance)); - // Make the vault actually send ETH on the mock call - vm.deal(address(feeDisburser), address(feeDisburser).balance + balance); - } - - /// @notice Helper to setup bridge mock - function _mockBridge() internal { - vm.mockCall( - Predeploys.L2_STANDARD_BRIDGE, - abi.encodeWithSignature("bridgeETHTo(address,uint32,bytes)", l1Wallet, WITHDRAWAL_MIN_GAS, bytes("")), - bytes("") - ); + vm.mockCall(vault, abi.encodeCall(IFeeVault.WITHDRAWAL_NETWORK, ()), abi.encode(network)); + vm.mockCall(vault, abi.encodeCall(IFeeVault.RECIPIENT, ()), abi.encode(recipient)); + vm.mockCall(vault, abi.encodeCall(IFeeVault.MIN_WITHDRAWAL_AMOUNT, ()), abi.encode(minWithdrawal)); + } + + function _mockVaultWithdrawal(address vault, uint256 amount) internal { + vm.deal(vault, amount); + vm.mockCall(vault, abi.encodeCall(IFeeVault.withdraw, ()), abi.encode(amount)); + vm.deal(address(feeDisburser), address(feeDisburser).balance + amount); + } + + function _expectBridgeETH(uint256 amount) internal { + bytes memory bridgeCall = + abi.encodeCall(IStandardBridge.bridgeETHTo, (L1_WALLET, WITHDRAWAL_MIN_GAS, bytes(""))); + + vm.mockCall(Predeploys.L2_STANDARD_BRIDGE, bridgeCall, bytes("")); + vm.expectCall(Predeploys.L2_STANDARD_BRIDGE, amount, bridgeCall); + } + + function _resetAfterMockedBridge() internal { + // vm.mockCall returns without moving ETH, so mirror the real bridge transfer between disbursements. + vm.deal(address(feeDisburser), 0); + } + + function _expectFeesDisbursed(uint256 totalFees) internal { + vm.expectEmit(address(feeDisburser)); + emit FeesDisbursed(block.timestamp, 0, totalFees); + } + + function _expectFeesReceived(address sender, uint256 amount) internal { + vm.expectEmit(address(feeDisburser)); + emit FeesReceived(sender, amount); + } + + function _expectNoFeesCollected() internal { + vm.expectEmit(address(feeDisburser)); + emit NoFeesCollected(); } // ============================================================ @@ -85,22 +102,22 @@ contract FeeDisburserTest is Test { // ============================================================ function test_constructor_success() public { - FeeDisburser newDisburser = new FeeDisburser(l1Wallet, DEFAULT_DISBURSEMENT_INTERVAL); + FeeDisburser newDisburser = new FeeDisburser(L1_WALLET, DEFAULT_DISBURSEMENT_INTERVAL); - assertEq(newDisburser.L1_WALLET(), l1Wallet); + assertEq(newDisburser.L1_WALLET(), L1_WALLET); assertEq(newDisburser.FEE_DISBURSEMENT_INTERVAL(), DEFAULT_DISBURSEMENT_INTERVAL); assertEq(newDisburser.WITHDRAWAL_MIN_GAS(), WITHDRAWAL_MIN_GAS); assertEq(newDisburser.lastDisbursementTime(), 0); } function test_constructor_success_exactlyMinimumInterval() public { - FeeDisburser newDisburser = new FeeDisburser(l1Wallet, 24 hours); + FeeDisburser newDisburser = new FeeDisburser(L1_WALLET, 24 hours); assertEq(newDisburser.FEE_DISBURSEMENT_INTERVAL(), 24 hours); } function test_constructor_success_largeInterval() public { uint256 largeInterval = 365 days; - FeeDisburser newDisburser = new FeeDisburser(l1Wallet, largeInterval); + FeeDisburser newDisburser = new FeeDisburser(L1_WALLET, largeInterval); assertEq(newDisburser.FEE_DISBURSEMENT_INTERVAL(), largeInterval); } @@ -111,12 +128,12 @@ contract FeeDisburserTest is Test { function test_constructor_revert_intervalTooLow() public { vm.expectRevert(FeeDisburser.IntervalTooLow.selector); - new FeeDisburser(l1Wallet, 24 hours - 1); + new FeeDisburser(L1_WALLET, 24 hours - 1); } function test_constructor_revert_intervalZero() public { vm.expectRevert(FeeDisburser.IntervalTooLow.selector); - new FeeDisburser(l1Wallet, 0); + new FeeDisburser(L1_WALLET, 0); } // ============================================================ @@ -129,8 +146,7 @@ contract FeeDisburserTest is Test { vm.deal(Predeploys.BASE_FEE_VAULT, 0); vm.deal(Predeploys.L1_FEE_VAULT, 0); - vm.expectEmit(true, true, true, true, address(feeDisburser)); - emit NoFeesCollected(); + _expectNoFeesCollected(); feeDisburser.disburseFees(); @@ -141,21 +157,13 @@ contract FeeDisburserTest is Test { function test_disburseFees_success_sequencerFeesOnly() public { uint256 feeAmount = DEFAULT_MIN_WITHDRAWAL; - // Setup sequencer vault withdrawal - vm.deal(Predeploys.SEQUENCER_FEE_WALLET, feeAmount); - vm.mockCall( - Predeploys.SEQUENCER_FEE_WALLET, abi.encodeWithSelector(IFeeVault.withdraw.selector), abi.encode(feeAmount) - ); - vm.deal(address(feeDisburser), feeAmount); - - // Other vaults below minimum + _mockVaultWithdrawal(Predeploys.SEQUENCER_FEE_WALLET, feeAmount); vm.deal(Predeploys.BASE_FEE_VAULT, 0); vm.deal(Predeploys.L1_FEE_VAULT, 0); - _mockBridge(); + _expectBridgeETH(feeAmount); - vm.expectEmit(true, true, true, true, address(feeDisburser)); - emit FeesDisbursed(block.timestamp, 0, feeAmount); + _expectFeesDisbursed(feeAmount); feeDisburser.disburseFees(); @@ -165,19 +173,13 @@ contract FeeDisburserTest is Test { function test_disburseFees_success_baseFeesOnly() public { uint256 feeAmount = DEFAULT_MIN_WITHDRAWAL; - // Setup base vault withdrawal vm.deal(Predeploys.SEQUENCER_FEE_WALLET, 0); - vm.deal(Predeploys.BASE_FEE_VAULT, feeAmount); - vm.mockCall( - Predeploys.BASE_FEE_VAULT, abi.encodeWithSelector(IFeeVault.withdraw.selector), abi.encode(feeAmount) - ); - vm.deal(address(feeDisburser), feeAmount); + _mockVaultWithdrawal(Predeploys.BASE_FEE_VAULT, feeAmount); vm.deal(Predeploys.L1_FEE_VAULT, 0); - _mockBridge(); + _expectBridgeETH(feeAmount); - vm.expectEmit(true, true, true, true, address(feeDisburser)); - emit FeesDisbursed(block.timestamp, 0, feeAmount); + _expectFeesDisbursed(feeAmount); feeDisburser.disburseFees(); @@ -187,17 +189,13 @@ contract FeeDisburserTest is Test { function test_disburseFees_success_l1FeesOnly() public { uint256 feeAmount = DEFAULT_MIN_WITHDRAWAL; - // Setup l1 fee vault withdrawal vm.deal(Predeploys.SEQUENCER_FEE_WALLET, 0); vm.deal(Predeploys.BASE_FEE_VAULT, 0); - vm.deal(Predeploys.L1_FEE_VAULT, feeAmount); - vm.mockCall(Predeploys.L1_FEE_VAULT, abi.encodeWithSelector(IFeeVault.withdraw.selector), abi.encode(feeAmount)); - vm.deal(address(feeDisburser), feeAmount); + _mockVaultWithdrawal(Predeploys.L1_FEE_VAULT, feeAmount); - _mockBridge(); + _expectBridgeETH(feeAmount); - vm.expectEmit(true, true, true, true, address(feeDisburser)); - emit FeesDisbursed(block.timestamp, 0, feeAmount); + _expectFeesDisbursed(feeAmount); feeDisburser.disburseFees(); @@ -210,29 +208,13 @@ contract FeeDisburserTest is Test { uint256 l1Fees = DEFAULT_MIN_WITHDRAWAL * 3; uint256 totalFees = sequencerFees + baseFees + l1Fees; - // Setup all vault withdrawals - vm.deal(Predeploys.SEQUENCER_FEE_WALLET, sequencerFees); - vm.mockCall( - Predeploys.SEQUENCER_FEE_WALLET, - abi.encodeWithSelector(IFeeVault.withdraw.selector), - abi.encode(sequencerFees) - ); - - vm.deal(Predeploys.BASE_FEE_VAULT, baseFees); - vm.mockCall( - Predeploys.BASE_FEE_VAULT, abi.encodeWithSelector(IFeeVault.withdraw.selector), abi.encode(baseFees) - ); + _mockVaultWithdrawal(Predeploys.SEQUENCER_FEE_WALLET, sequencerFees); + _mockVaultWithdrawal(Predeploys.BASE_FEE_VAULT, baseFees); + _mockVaultWithdrawal(Predeploys.L1_FEE_VAULT, l1Fees); - vm.deal(Predeploys.L1_FEE_VAULT, l1Fees); - vm.mockCall(Predeploys.L1_FEE_VAULT, abi.encodeWithSelector(IFeeVault.withdraw.selector), abi.encode(l1Fees)); + _expectBridgeETH(totalFees); - // FeeDisburser receives all the fees - vm.deal(address(feeDisburser), totalFees); - - _mockBridge(); - - vm.expectEmit(true, true, true, true, address(feeDisburser)); - emit FeesDisbursed(block.timestamp, 0, totalFees); + _expectFeesDisbursed(totalFees); feeDisburser.disburseFees(); @@ -245,8 +227,7 @@ contract FeeDisburserTest is Test { vm.deal(Predeploys.BASE_FEE_VAULT, DEFAULT_MIN_WITHDRAWAL - 1); vm.deal(Predeploys.L1_FEE_VAULT, DEFAULT_MIN_WITHDRAWAL - 1); - vm.expectEmit(true, true, true, true, address(feeDisburser)); - emit NoFeesCollected(); + _expectNoFeesCollected(); feeDisburser.disburseFees(); @@ -257,39 +238,24 @@ contract FeeDisburserTest is Test { // Only sequencer vault has enough uint256 sequencerFees = DEFAULT_MIN_WITHDRAWAL; - vm.deal(Predeploys.SEQUENCER_FEE_WALLET, sequencerFees); - vm.mockCall( - Predeploys.SEQUENCER_FEE_WALLET, - abi.encodeWithSelector(IFeeVault.withdraw.selector), - abi.encode(sequencerFees) - ); - vm.deal(address(feeDisburser), sequencerFees); - + _mockVaultWithdrawal(Predeploys.SEQUENCER_FEE_WALLET, sequencerFees); vm.deal(Predeploys.BASE_FEE_VAULT, DEFAULT_MIN_WITHDRAWAL - 1); vm.deal(Predeploys.L1_FEE_VAULT, DEFAULT_MIN_WITHDRAWAL - 1); - _mockBridge(); + _expectBridgeETH(sequencerFees); - vm.expectEmit(true, true, true, true, address(feeDisburser)); - emit FeesDisbursed(block.timestamp, 0, sequencerFees); + _expectFeesDisbursed(sequencerFees); feeDisburser.disburseFees(); } function test_disburseFees_revert_intervalNotReached() public { // First successful disbursal - vm.deal(Predeploys.SEQUENCER_FEE_WALLET, DEFAULT_MIN_WITHDRAWAL); - vm.mockCall( - Predeploys.SEQUENCER_FEE_WALLET, - abi.encodeWithSelector(IFeeVault.withdraw.selector), - abi.encode(DEFAULT_MIN_WITHDRAWAL) - ); - vm.deal(address(feeDisburser), DEFAULT_MIN_WITHDRAWAL); - + _mockVaultWithdrawal(Predeploys.SEQUENCER_FEE_WALLET, DEFAULT_MIN_WITHDRAWAL); vm.deal(Predeploys.BASE_FEE_VAULT, 0); vm.deal(Predeploys.L1_FEE_VAULT, 0); - _mockBridge(); + _expectBridgeETH(DEFAULT_MIN_WITHDRAWAL); feeDisburser.disburseFees(); @@ -300,17 +266,11 @@ contract FeeDisburserTest is Test { function test_disburseFees_revert_intervalNotReached_oneSecondBefore() public { // First disbursal - vm.deal(Predeploys.SEQUENCER_FEE_WALLET, DEFAULT_MIN_WITHDRAWAL); - vm.mockCall( - Predeploys.SEQUENCER_FEE_WALLET, - abi.encodeWithSelector(IFeeVault.withdraw.selector), - abi.encode(DEFAULT_MIN_WITHDRAWAL) - ); - vm.deal(address(feeDisburser), DEFAULT_MIN_WITHDRAWAL); + _mockVaultWithdrawal(Predeploys.SEQUENCER_FEE_WALLET, DEFAULT_MIN_WITHDRAWAL); vm.deal(Predeploys.BASE_FEE_VAULT, 0); vm.deal(Predeploys.L1_FEE_VAULT, 0); - _mockBridge(); + _expectBridgeETH(DEFAULT_MIN_WITHDRAWAL); feeDisburser.disburseFees(); @@ -323,54 +283,41 @@ contract FeeDisburserTest is Test { function test_disburseFees_success_exactlyAtInterval() public { // First disbursal - vm.deal(Predeploys.SEQUENCER_FEE_WALLET, DEFAULT_MIN_WITHDRAWAL); - vm.mockCall( - Predeploys.SEQUENCER_FEE_WALLET, - abi.encodeWithSelector(IFeeVault.withdraw.selector), - abi.encode(DEFAULT_MIN_WITHDRAWAL) - ); - vm.deal(address(feeDisburser), DEFAULT_MIN_WITHDRAWAL); + _mockVaultWithdrawal(Predeploys.SEQUENCER_FEE_WALLET, DEFAULT_MIN_WITHDRAWAL); vm.deal(Predeploys.BASE_FEE_VAULT, 0); vm.deal(Predeploys.L1_FEE_VAULT, 0); - _mockBridge(); + _expectBridgeETH(DEFAULT_MIN_WITHDRAWAL); feeDisburser.disburseFees(); uint256 firstDisbursementTime = block.timestamp; + _resetAfterMockedBridge(); // Warp exactly to the interval vm.warp(firstDisbursementTime + DEFAULT_DISBURSEMENT_INTERVAL); // Fund vaults again - vm.deal(Predeploys.SEQUENCER_FEE_WALLET, DEFAULT_MIN_WITHDRAWAL); - vm.deal(address(feeDisburser), DEFAULT_MIN_WITHDRAWAL); + _mockVaultWithdrawal(Predeploys.SEQUENCER_FEE_WALLET, DEFAULT_MIN_WITHDRAWAL); // Should succeed + _expectBridgeETH(DEFAULT_MIN_WITHDRAWAL); feeDisburser.disburseFees(); assertEq(feeDisburser.lastDisbursementTime(), firstDisbursementTime + DEFAULT_DISBURSEMENT_INTERVAL); } function test_disburseFees_success_multipleDisbursements() public { - _mockBridge(); - - uint256 currentTime = block.timestamp; for (uint256 i = 0; i < 5; i++) { - vm.deal(Predeploys.SEQUENCER_FEE_WALLET, DEFAULT_MIN_WITHDRAWAL); - vm.mockCall( - Predeploys.SEQUENCER_FEE_WALLET, - abi.encodeWithSelector(IFeeVault.withdraw.selector), - abi.encode(DEFAULT_MIN_WITHDRAWAL) - ); - vm.deal(address(feeDisburser), DEFAULT_MIN_WITHDRAWAL); + _mockVaultWithdrawal(Predeploys.SEQUENCER_FEE_WALLET, DEFAULT_MIN_WITHDRAWAL); vm.deal(Predeploys.BASE_FEE_VAULT, 0); vm.deal(Predeploys.L1_FEE_VAULT, 0); + _expectBridgeETH(DEFAULT_MIN_WITHDRAWAL); feeDisburser.disburseFees(); - assertEq(feeDisburser.lastDisbursementTime(), currentTime); + assertEq(feeDisburser.lastDisbursementTime(), block.timestamp); + _resetAfterMockedBridge(); // Warp past interval for next iteration - currentTime += DEFAULT_DISBURSEMENT_INTERVAL; - vm.warp(currentTime); + vm.warp(block.timestamp + DEFAULT_DISBURSEMENT_INTERVAL); } } @@ -385,8 +332,8 @@ contract FeeDisburserTest is Test { } function test_disburseFees_revert_feeVaultWrongRecipient() public { - // Mock sequencer vault with wrong recipient (alice instead of feeDisburser) - _mockFeeVault(Predeploys.SEQUENCER_FEE_WALLET, alice, DEFAULT_MIN_WITHDRAWAL, Types.WithdrawalNetwork.L2); + // Mock sequencer vault with wrong recipient (ALICE instead of feeDisburser) + _mockFeeVault(Predeploys.SEQUENCER_FEE_WALLET, ALICE, DEFAULT_MIN_WITHDRAWAL, Types.WithdrawalNetwork.L2); vm.expectRevert(FeeDisburser.FeeVaultMustWithdrawToFeeDisburser.selector); feeDisburser.disburseFees(); @@ -414,7 +361,7 @@ contract FeeDisburserTest is Test { vm.deal(Predeploys.BASE_FEE_VAULT, 0); // L1 vault has wrong recipient - _mockFeeVault(Predeploys.L1_FEE_VAULT, bob, DEFAULT_MIN_WITHDRAWAL, Types.WithdrawalNetwork.L2); + _mockFeeVault(Predeploys.L1_FEE_VAULT, BOB, DEFAULT_MIN_WITHDRAWAL, Types.WithdrawalNetwork.L2); vm.expectRevert(FeeDisburser.FeeVaultMustWithdrawToFeeDisburser.selector); feeDisburser.disburseFees(); @@ -426,12 +373,11 @@ contract FeeDisburserTest is Test { function test_receive_success_anyAddress() public { uint256 amount = 1 ether; - vm.deal(alice, amount); + vm.deal(ALICE, amount); - vm.expectEmit(true, true, true, true, address(feeDisburser)); - emit FeesReceived(alice, amount); + _expectFeesReceived(ALICE, amount); - vm.prank(alice); + vm.prank(ALICE); (bool success,) = address(feeDisburser).call{ value: amount }(""); assertTrue(success); @@ -442,8 +388,7 @@ contract FeeDisburserTest is Test { uint256 amount = 5 ether; vm.deal(Predeploys.SEQUENCER_FEE_WALLET, amount); - vm.expectEmit(true, true, true, true, address(feeDisburser)); - emit FeesReceived(Predeploys.SEQUENCER_FEE_WALLET, amount); + _expectFeesReceived(Predeploys.SEQUENCER_FEE_WALLET, amount); vm.prank(Predeploys.SEQUENCER_FEE_WALLET); (bool success,) = address(feeDisburser).call{ value: amount }(""); @@ -454,8 +399,7 @@ contract FeeDisburserTest is Test { uint256 amount = 5 ether; vm.deal(Predeploys.BASE_FEE_VAULT, amount); - vm.expectEmit(true, true, true, true, address(feeDisburser)); - emit FeesReceived(Predeploys.BASE_FEE_VAULT, amount); + _expectFeesReceived(Predeploys.BASE_FEE_VAULT, amount); vm.prank(Predeploys.BASE_FEE_VAULT); (bool success,) = address(feeDisburser).call{ value: amount }(""); @@ -466,8 +410,7 @@ contract FeeDisburserTest is Test { uint256 amount = 5 ether; vm.deal(Predeploys.L1_FEE_VAULT, amount); - vm.expectEmit(true, true, true, true, address(feeDisburser)); - emit FeesReceived(Predeploys.L1_FEE_VAULT, amount); + _expectFeesReceived(Predeploys.L1_FEE_VAULT, amount); vm.prank(Predeploys.L1_FEE_VAULT); (bool success,) = address(feeDisburser).call{ value: amount }(""); @@ -475,10 +418,9 @@ contract FeeDisburserTest is Test { } function test_receive_success_zeroValue() public { - vm.expectEmit(true, true, true, true, address(feeDisburser)); - emit FeesReceived(alice, 0); + _expectFeesReceived(ALICE, 0); - vm.prank(alice); + vm.prank(ALICE); (bool success,) = address(feeDisburser).call{ value: 0 }(""); assertTrue(success); } @@ -489,9 +431,9 @@ contract FeeDisburserTest is Test { for (uint256 i = 1; i <= 5; i++) { uint256 amount = i * 1 ether; totalExpected += amount; - vm.deal(alice, amount); + vm.deal(ALICE, amount); - vm.prank(alice); + vm.prank(ALICE); (bool success,) = address(feeDisburser).call{ value: amount }(""); assertTrue(success); } @@ -517,14 +459,14 @@ contract FeeDisburserTest is Test { vm.assume(_interval < 24 hours); vm.expectRevert(FeeDisburser.IntervalTooLow.selector); - new FeeDisburser(l1Wallet, _interval); + new FeeDisburser(L1_WALLET, _interval); } function testFuzz_disburseFees_varyingAmounts(uint256 _sequencerFees, uint256 _baseFees, uint256 _l1Fees) public { // Bound fees to reasonable values to avoid overflow - _sequencerFees = bound(_sequencerFees, 0, 10 ** 27); - _baseFees = bound(_baseFees, 0, 10 ** 27); - _l1Fees = bound(_l1Fees, 0, 10 ** 27); + _sequencerFees = bound(_sequencerFees, 0, MAX_TEST_FEE); + _baseFees = bound(_baseFees, 0, MAX_TEST_FEE); + _l1Fees = bound(_l1Fees, 0, MAX_TEST_FEE); // Setup vault balances and mocks vm.deal(Predeploys.SEQUENCER_FEE_WALLET, _sequencerFees); @@ -534,37 +476,23 @@ contract FeeDisburserTest is Test { // Calculate expected total (only vaults >= MIN_WITHDRAWAL_AMOUNT will be withdrawn) uint256 expectedTotal = 0; if (_sequencerFees >= DEFAULT_MIN_WITHDRAWAL) { - vm.mockCall( - Predeploys.SEQUENCER_FEE_WALLET, - abi.encodeWithSelector(IFeeVault.withdraw.selector), - abi.encode(_sequencerFees) - ); + _mockVaultWithdrawal(Predeploys.SEQUENCER_FEE_WALLET, _sequencerFees); expectedTotal += _sequencerFees; } if (_baseFees >= DEFAULT_MIN_WITHDRAWAL) { - vm.mockCall( - Predeploys.BASE_FEE_VAULT, abi.encodeWithSelector(IFeeVault.withdraw.selector), abi.encode(_baseFees) - ); + _mockVaultWithdrawal(Predeploys.BASE_FEE_VAULT, _baseFees); expectedTotal += _baseFees; } if (_l1Fees >= DEFAULT_MIN_WITHDRAWAL) { - vm.mockCall( - Predeploys.L1_FEE_VAULT, abi.encodeWithSelector(IFeeVault.withdraw.selector), abi.encode(_l1Fees) - ); + _mockVaultWithdrawal(Predeploys.L1_FEE_VAULT, _l1Fees); expectedTotal += _l1Fees; } - // Give FeeDisburser the expected total - vm.deal(address(feeDisburser), expectedTotal); - - _mockBridge(); - if (expectedTotal == 0) { - vm.expectEmit(true, true, true, true, address(feeDisburser)); - emit NoFeesCollected(); + _expectNoFeesCollected(); } else { - vm.expectEmit(true, true, true, true, address(feeDisburser)); - emit FeesDisbursed(block.timestamp, 0, expectedTotal); + _expectBridgeETH(expectedTotal); + _expectFeesDisbursed(expectedTotal); } feeDisburser.disburseFees(); @@ -577,38 +505,29 @@ contract FeeDisburserTest is Test { } function testFuzz_disburseFees_multipleIntervals(uint8 _numDisbursements) public { - vm.assume(_numDisbursements > 0 && _numDisbursements <= 50); - - _mockBridge(); - - uint256 currentTime = block.timestamp; - for (uint256 i = 0; i < _numDisbursements; i++) { - vm.deal(Predeploys.SEQUENCER_FEE_WALLET, DEFAULT_MIN_WITHDRAWAL); - vm.mockCall( - Predeploys.SEQUENCER_FEE_WALLET, - abi.encodeWithSelector(IFeeVault.withdraw.selector), - abi.encode(DEFAULT_MIN_WITHDRAWAL) - ); - vm.deal(address(feeDisburser), DEFAULT_MIN_WITHDRAWAL); + uint256 numDisbursements = bound(_numDisbursements, 1, 10); + + for (uint256 i = 0; i < numDisbursements; i++) { + _mockVaultWithdrawal(Predeploys.SEQUENCER_FEE_WALLET, DEFAULT_MIN_WITHDRAWAL); vm.deal(Predeploys.BASE_FEE_VAULT, 0); vm.deal(Predeploys.L1_FEE_VAULT, 0); + _expectBridgeETH(DEFAULT_MIN_WITHDRAWAL); feeDisburser.disburseFees(); - assertEq(feeDisburser.lastDisbursementTime(), currentTime); + assertEq(feeDisburser.lastDisbursementTime(), block.timestamp); + _resetAfterMockedBridge(); // Warp past interval for next iteration - currentTime += DEFAULT_DISBURSEMENT_INTERVAL; - vm.warp(currentTime); + vm.warp(block.timestamp + DEFAULT_DISBURSEMENT_INTERVAL); } } function testFuzz_receive_anyAmount(address _sender, uint256 _amount) public { vm.assume(_sender != address(0)); - _amount = bound(_amount, 0, 10 ** 27); + _amount = bound(_amount, 0, MAX_TEST_FEE); vm.deal(_sender, _amount); - vm.expectEmit(true, true, true, true, address(feeDisburser)); - emit FeesReceived(_sender, _amount); + _expectFeesReceived(_sender, _amount); vm.prank(_sender); (bool success,) = address(feeDisburser).call{ value: _amount }(""); @@ -619,35 +538,30 @@ contract FeeDisburserTest is Test { function testFuzz_intervalEnforcement(uint256 _timeWarp) public { // First disbursal - vm.deal(Predeploys.SEQUENCER_FEE_WALLET, DEFAULT_MIN_WITHDRAWAL); - vm.mockCall( - Predeploys.SEQUENCER_FEE_WALLET, - abi.encodeWithSelector(IFeeVault.withdraw.selector), - abi.encode(DEFAULT_MIN_WITHDRAWAL) - ); - vm.deal(address(feeDisburser), DEFAULT_MIN_WITHDRAWAL); + _mockVaultWithdrawal(Predeploys.SEQUENCER_FEE_WALLET, DEFAULT_MIN_WITHDRAWAL); vm.deal(Predeploys.BASE_FEE_VAULT, 0); vm.deal(Predeploys.L1_FEE_VAULT, 0); - _mockBridge(); + _expectBridgeETH(DEFAULT_MIN_WITHDRAWAL); feeDisburser.disburseFees(); uint256 lastTime = feeDisburser.lastDisbursementTime(); + _resetAfterMockedBridge(); // Bound time warp to avoid overflow _timeWarp = bound(_timeWarp, 0, 365 days * 100); vm.warp(lastTime + _timeWarp); // Fund again - vm.deal(Predeploys.SEQUENCER_FEE_WALLET, DEFAULT_MIN_WITHDRAWAL); - vm.deal(address(feeDisburser), DEFAULT_MIN_WITHDRAWAL); + _mockVaultWithdrawal(Predeploys.SEQUENCER_FEE_WALLET, DEFAULT_MIN_WITHDRAWAL); if (_timeWarp < DEFAULT_DISBURSEMENT_INTERVAL) { vm.expectRevert(FeeDisburser.IntervalNotReached.selector); feeDisburser.disburseFees(); } else { // Should succeed + _expectBridgeETH(DEFAULT_MIN_WITHDRAWAL); feeDisburser.disburseFees(); assertEq(feeDisburser.lastDisbursementTime(), lastTime + _timeWarp); } @@ -664,33 +578,17 @@ contract FeeDisburserTest is Test { uint256 l1Fees = 200 ether; uint256 totalFees = sequencerFees + baseFees + l1Fees; - // Setup all vault withdrawals - vm.deal(Predeploys.SEQUENCER_FEE_WALLET, sequencerFees); - vm.mockCall( - Predeploys.SEQUENCER_FEE_WALLET, - abi.encodeWithSelector(IFeeVault.withdraw.selector), - abi.encode(sequencerFees) - ); - - vm.deal(Predeploys.BASE_FEE_VAULT, baseFees); - vm.mockCall( - Predeploys.BASE_FEE_VAULT, abi.encodeWithSelector(IFeeVault.withdraw.selector), abi.encode(baseFees) - ); - - vm.deal(Predeploys.L1_FEE_VAULT, l1Fees); - vm.mockCall(Predeploys.L1_FEE_VAULT, abi.encodeWithSelector(IFeeVault.withdraw.selector), abi.encode(l1Fees)); - - // FeeDisburser receives all the fees - vm.deal(address(feeDisburser), totalFees); + _mockVaultWithdrawal(Predeploys.SEQUENCER_FEE_WALLET, sequencerFees); + _mockVaultWithdrawal(Predeploys.BASE_FEE_VAULT, baseFees); + _mockVaultWithdrawal(Predeploys.L1_FEE_VAULT, l1Fees); - _mockBridge(); + _expectBridgeETH(totalFees); // Verify initial state assertEq(feeDisburser.lastDisbursementTime(), 0); // Execute disbursement - vm.expectEmit(true, true, true, true, address(feeDisburser)); - emit FeesDisbursed(block.timestamp, 0, totalFees); + _expectFeesDisbursed(totalFees); feeDisburser.disburseFees(); @@ -725,20 +623,13 @@ contract FeeDisburserTest is Test { function test_edge_exactMinWithdrawalAmount() public { // Test with exactly the minimum withdrawal amount - vm.deal(Predeploys.SEQUENCER_FEE_WALLET, DEFAULT_MIN_WITHDRAWAL); - vm.mockCall( - Predeploys.SEQUENCER_FEE_WALLET, - abi.encodeWithSelector(IFeeVault.withdraw.selector), - abi.encode(DEFAULT_MIN_WITHDRAWAL) - ); - vm.deal(address(feeDisburser), DEFAULT_MIN_WITHDRAWAL); + _mockVaultWithdrawal(Predeploys.SEQUENCER_FEE_WALLET, DEFAULT_MIN_WITHDRAWAL); vm.deal(Predeploys.BASE_FEE_VAULT, 0); vm.deal(Predeploys.L1_FEE_VAULT, 0); - _mockBridge(); + _expectBridgeETH(DEFAULT_MIN_WITHDRAWAL); - vm.expectEmit(true, true, true, true, address(feeDisburser)); - emit FeesDisbursed(block.timestamp, 0, DEFAULT_MIN_WITHDRAWAL); + _expectFeesDisbursed(DEFAULT_MIN_WITHDRAWAL); feeDisburser.disburseFees(); } @@ -749,36 +640,24 @@ contract FeeDisburserTest is Test { vm.deal(Predeploys.BASE_FEE_VAULT, DEFAULT_MIN_WITHDRAWAL - 1); vm.deal(Predeploys.L1_FEE_VAULT, DEFAULT_MIN_WITHDRAWAL - 1); - vm.expectEmit(true, true, true, true, address(feeDisburser)); - emit NoFeesCollected(); + _expectNoFeesCollected(); feeDisburser.disburseFees(); } function test_edge_veryLargeFees() public { // Test with very large fee amounts - uint256 largeFee = 10 ** 27; // 1 billion ETH - - vm.deal(Predeploys.SEQUENCER_FEE_WALLET, largeFee); - vm.mockCall( - Predeploys.SEQUENCER_FEE_WALLET, abi.encodeWithSelector(IFeeVault.withdraw.selector), abi.encode(largeFee) - ); - - vm.deal(Predeploys.BASE_FEE_VAULT, largeFee); - vm.mockCall( - Predeploys.BASE_FEE_VAULT, abi.encodeWithSelector(IFeeVault.withdraw.selector), abi.encode(largeFee) - ); + uint256 largeFee = MAX_TEST_FEE; // 1 billion ETH - vm.deal(Predeploys.L1_FEE_VAULT, largeFee); - vm.mockCall(Predeploys.L1_FEE_VAULT, abi.encodeWithSelector(IFeeVault.withdraw.selector), abi.encode(largeFee)); + _mockVaultWithdrawal(Predeploys.SEQUENCER_FEE_WALLET, largeFee); + _mockVaultWithdrawal(Predeploys.BASE_FEE_VAULT, largeFee); + _mockVaultWithdrawal(Predeploys.L1_FEE_VAULT, largeFee); uint256 totalFees = largeFee * 3; - vm.deal(address(feeDisburser), totalFees); - _mockBridge(); + _expectBridgeETH(totalFees); - vm.expectEmit(true, true, true, true, address(feeDisburser)); - emit FeesDisbursed(block.timestamp, 0, totalFees); + _expectFeesDisbursed(totalFees); feeDisburser.disburseFees(); } @@ -788,17 +667,11 @@ contract FeeDisburserTest is Test { uint256 maxSafeTimestamp = type(uint256).max - DEFAULT_DISBURSEMENT_INTERVAL; vm.warp(maxSafeTimestamp); - vm.deal(Predeploys.SEQUENCER_FEE_WALLET, DEFAULT_MIN_WITHDRAWAL); - vm.mockCall( - Predeploys.SEQUENCER_FEE_WALLET, - abi.encodeWithSelector(IFeeVault.withdraw.selector), - abi.encode(DEFAULT_MIN_WITHDRAWAL) - ); - vm.deal(address(feeDisburser), DEFAULT_MIN_WITHDRAWAL); + _mockVaultWithdrawal(Predeploys.SEQUENCER_FEE_WALLET, DEFAULT_MIN_WITHDRAWAL); vm.deal(Predeploys.BASE_FEE_VAULT, 0); vm.deal(Predeploys.L1_FEE_VAULT, 0); - _mockBridge(); + _expectBridgeETH(DEFAULT_MIN_WITHDRAWAL); feeDisburser.disburseFees(); assertEq(feeDisburser.lastDisbursementTime(), maxSafeTimestamp); @@ -809,54 +682,38 @@ contract FeeDisburserTest is Test { // ============================================================ function test_invariant_lastDisbursementTimeNeverDecreases() public { - _mockBridge(); - uint256 previousTime = 0; - uint256 currentWarpTime = block.timestamp; for (uint256 i = 0; i < 10; i++) { - vm.deal(Predeploys.SEQUENCER_FEE_WALLET, DEFAULT_MIN_WITHDRAWAL); - vm.mockCall( - Predeploys.SEQUENCER_FEE_WALLET, - abi.encodeWithSelector(IFeeVault.withdraw.selector), - abi.encode(DEFAULT_MIN_WITHDRAWAL) - ); - vm.deal(address(feeDisburser), DEFAULT_MIN_WITHDRAWAL); + _mockVaultWithdrawal(Predeploys.SEQUENCER_FEE_WALLET, DEFAULT_MIN_WITHDRAWAL); vm.deal(Predeploys.BASE_FEE_VAULT, 0); vm.deal(Predeploys.L1_FEE_VAULT, 0); + _expectBridgeETH(DEFAULT_MIN_WITHDRAWAL); feeDisburser.disburseFees(); uint256 currentTime = feeDisburser.lastDisbursementTime(); assertTrue(currentTime >= previousTime, "lastDisbursementTime decreased!"); previousTime = currentTime; + _resetAfterMockedBridge(); - currentWarpTime += DEFAULT_DISBURSEMENT_INTERVAL; - vm.warp(currentWarpTime); + vm.warp(block.timestamp + DEFAULT_DISBURSEMENT_INTERVAL); } } function test_invariant_l1WalletNeverChanges() public { address initialL1Wallet = feeDisburser.L1_WALLET(); - _mockBridge(); - - uint256 currentWarpTime = block.timestamp; // Perform multiple operations for (uint256 i = 0; i < 5; i++) { - vm.deal(Predeploys.SEQUENCER_FEE_WALLET, DEFAULT_MIN_WITHDRAWAL); - vm.mockCall( - Predeploys.SEQUENCER_FEE_WALLET, - abi.encodeWithSelector(IFeeVault.withdraw.selector), - abi.encode(DEFAULT_MIN_WITHDRAWAL) - ); - vm.deal(address(feeDisburser), DEFAULT_MIN_WITHDRAWAL); + _mockVaultWithdrawal(Predeploys.SEQUENCER_FEE_WALLET, DEFAULT_MIN_WITHDRAWAL); vm.deal(Predeploys.BASE_FEE_VAULT, 0); vm.deal(Predeploys.L1_FEE_VAULT, 0); + _expectBridgeETH(DEFAULT_MIN_WITHDRAWAL); feeDisburser.disburseFees(); - currentWarpTime += DEFAULT_DISBURSEMENT_INTERVAL; - vm.warp(currentWarpTime); + _resetAfterMockedBridge(); + vm.warp(block.timestamp + DEFAULT_DISBURSEMENT_INTERVAL); } assertEq(feeDisburser.L1_WALLET(), initialL1Wallet, "L1_WALLET changed!"); @@ -865,24 +722,16 @@ contract FeeDisburserTest is Test { function test_invariant_disbursementIntervalNeverChanges() public { uint256 initialInterval = feeDisburser.FEE_DISBURSEMENT_INTERVAL(); - _mockBridge(); - - uint256 currentWarpTime = block.timestamp; // Perform multiple operations for (uint256 i = 0; i < 5; i++) { - vm.deal(Predeploys.SEQUENCER_FEE_WALLET, DEFAULT_MIN_WITHDRAWAL); - vm.mockCall( - Predeploys.SEQUENCER_FEE_WALLET, - abi.encodeWithSelector(IFeeVault.withdraw.selector), - abi.encode(DEFAULT_MIN_WITHDRAWAL) - ); - vm.deal(address(feeDisburser), DEFAULT_MIN_WITHDRAWAL); + _mockVaultWithdrawal(Predeploys.SEQUENCER_FEE_WALLET, DEFAULT_MIN_WITHDRAWAL); vm.deal(Predeploys.BASE_FEE_VAULT, 0); vm.deal(Predeploys.L1_FEE_VAULT, 0); + _expectBridgeETH(DEFAULT_MIN_WITHDRAWAL); feeDisburser.disburseFees(); - currentWarpTime += DEFAULT_DISBURSEMENT_INTERVAL; - vm.warp(currentWarpTime); + _resetAfterMockedBridge(); + vm.warp(block.timestamp + DEFAULT_DISBURSEMENT_INTERVAL); } assertEq(feeDisburser.FEE_DISBURSEMENT_INTERVAL(), initialInterval, "FEE_DISBURSEMENT_INTERVAL changed!"); @@ -901,33 +750,22 @@ contract FeeDisburserTest is Test { feeDisburser.disburseFees(); uint256 gasUsed = gasBefore - gasleft(); - // Log for visibility (optional) - console.log("Gas used for no-fee disbursement:", gasUsed); assertTrue(gasUsed < 100_000, "Gas usage too high for no-fee case"); } function test_gas_disburseFees_allVaults() public { uint256 fee = DEFAULT_MIN_WITHDRAWAL; - vm.deal(Predeploys.SEQUENCER_FEE_WALLET, fee); - vm.mockCall( - Predeploys.SEQUENCER_FEE_WALLET, abi.encodeWithSelector(IFeeVault.withdraw.selector), abi.encode(fee) - ); - - vm.deal(Predeploys.BASE_FEE_VAULT, fee); - vm.mockCall(Predeploys.BASE_FEE_VAULT, abi.encodeWithSelector(IFeeVault.withdraw.selector), abi.encode(fee)); - - vm.deal(Predeploys.L1_FEE_VAULT, fee); - vm.mockCall(Predeploys.L1_FEE_VAULT, abi.encodeWithSelector(IFeeVault.withdraw.selector), abi.encode(fee)); + _mockVaultWithdrawal(Predeploys.SEQUENCER_FEE_WALLET, fee); + _mockVaultWithdrawal(Predeploys.BASE_FEE_VAULT, fee); + _mockVaultWithdrawal(Predeploys.L1_FEE_VAULT, fee); - vm.deal(address(feeDisburser), fee * 3); - _mockBridge(); + _expectBridgeETH(fee * 3); uint256 gasBefore = gasleft(); feeDisburser.disburseFees(); uint256 gasUsed = gasBefore - gasleft(); - console.log("Gas used for all-vault disbursement:", gasUsed); assertTrue(gasUsed < 200_000, "Gas usage too high for all-vault case"); } } diff --git a/test/L2/FeeVault.t.sol b/test/L2/FeeVault.t.sol index e6faca53..d424c4c2 100644 --- a/test/L2/FeeVault.t.sol +++ b/test/L2/FeeVault.t.sol @@ -6,7 +6,6 @@ import { CommonTest } from "test/setup/CommonTest.sol"; import { Reverter } from "test/mocks/Callers.sol"; // Interfaces -import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; import { IFeeVault } from "interfaces/L2/IFeeVault.sol"; import { IL2ToL1MessagePasser } from "interfaces/L2/IL2ToL1MessagePasser.sol"; @@ -17,9 +16,11 @@ import { Predeploys } from "src/libraries/Predeploys.sol"; import { Features } from "src/libraries/Features.sol"; /// @title FeeVault_Uncategorized_Test -/// @notice Abstract test contract for fee feeVault testing. +/// @notice Abstract test contract for fee vault testing. /// Subclasses can override the feeVault-specific variables. abstract contract FeeVault_Uncategorized_Test is CommonTest { + uint32 internal constant WITHDRAWAL_MIN_GAS = 400_000; + // Variables that can be overridden by concrete test contracts address recipient; IFeeVault feeVault; @@ -29,11 +30,17 @@ abstract contract FeeVault_Uncategorized_Test is CommonTest { /// @notice Helper function to set up L2 withdrawal configuration. function _setupL2Withdrawal() internal { - // Set the withdrawal network to L2 - vm.prank(IProxyAdmin(Predeploys.PROXY_ADMIN).owner()); + vm.prank(proxyAdminOwner); feeVault.setWithdrawalNetwork(Types.WithdrawalNetwork.L2); } + function _expectWithdrawalEvents(uint256 _amount, address _recipient, Types.WithdrawalNetwork _network) internal { + vm.expectEmit(address(feeVault)); + emit Withdrawal(_amount, _recipient, address(this)); + vm.expectEmit(address(feeVault)); + emit Withdrawal(_amount, _recipient, address(this), _network); + } + /// @notice Tests that the initialize function succeeds. function test_initialize_succeeds() external view { assertEq(feeVault.recipient(), recipient); @@ -63,19 +70,17 @@ abstract contract FeeVault_Uncategorized_Test is CommonTest { vm.prank(alice); (bool success,) = address(feeVault).call{ value: 100 }(hex""); - assertEq(success, true); + assertTrue(success); assertEq(address(feeVault).balance, balance + 100); } /// @notice Tests that `withdraw` reverts if the balance is less than the minimum withdrawal /// amount. function testFuzz_withdraw_notEnough_reverts(uint256 _minWithdrawalAmount) external { - // Set the minimum withdrawal amount _minWithdrawalAmount = bound(_minWithdrawalAmount, 1, type(uint256).max); - vm.prank(IProxyAdmin(Predeploys.PROXY_ADMIN).owner()); + vm.prank(proxyAdminOwner); feeVault.setMinWithdrawalAmount(_minWithdrawalAmount); - // Set the balance to be less than the minimum withdrawal amount vm.deal(address(feeVault), _minWithdrawalAmount - 1); vm.expectRevert("FeeVault: withdrawal amount must be greater than minimum withdrawal amount"); @@ -86,61 +91,38 @@ abstract contract FeeVault_Uncategorized_Test is CommonTest { function test_withdraw_toL1_succeeds() external { skipIfSysFeatureEnabled(Features.CUSTOM_GAS_TOKEN); - // Setup L1 withdrawal - vm.prank(IProxyAdmin(Predeploys.PROXY_ADMIN).owner()); + vm.prank(proxyAdminOwner); feeVault.setWithdrawalNetwork(Types.WithdrawalNetwork.L1); - // Set recipient - vm.prank(IProxyAdmin(Predeploys.PROXY_ADMIN).owner()); - feeVault.setRecipient(recipient); - - // Set minimum withdrawal amount - vm.prank(IProxyAdmin(Predeploys.PROXY_ADMIN).owner()); - feeVault.setMinWithdrawalAmount(minWithdrawalAmount); - - // Set the balance to be greater than the minimum withdrawal amount - uint256 amount = feeVault.minWithdrawalAmount() + 1; + uint256 amount = minWithdrawalAmount + 1; vm.deal(address(feeVault), amount); - // No ether has been withdrawn yet assertEq(feeVault.totalProcessed(), 0); + _expectWithdrawalEvents(amount, recipient, Types.WithdrawalNetwork.L1); - vm.expectEmit(address(address(feeVault))); - emit Withdrawal(address(feeVault).balance, recipient, address(this)); - vm.expectEmit(address(address(feeVault))); - emit Withdrawal(address(feeVault).balance, recipient, address(this), Types.WithdrawalNetwork.L1); - - // The entire feeVault's balance is withdrawn vm.expectCall( Predeploys.L2_TO_L1_MESSAGE_PASSER, - address(feeVault).balance, - abi.encodeCall(IL2ToL1MessagePasser.initiateWithdrawal, (recipient, 400_000, hex"")) + amount, + abi.encodeCall(IL2ToL1MessagePasser.initiateWithdrawal, (recipient, WITHDRAWAL_MIN_GAS, hex"")) ); - // The message is passed to the correct recipient - vm.expectEmit(Predeploys.L2_TO_L1_MESSAGE_PASSER); - emit MessagePassed( - l2ToL1MessagePasser.messageNonce(), - address(feeVault), - recipient, - amount, - 400_000, - hex"", - Hashing.hashWithdrawal( - Types.WithdrawalTransaction({ - nonce: l2ToL1MessagePasser.messageNonce(), - sender: address(feeVault), - target: recipient, - value: amount, - gasLimit: 400_000, - data: hex"" - }) - ) + uint256 nonce = l2ToL1MessagePasser.messageNonce(); + bytes32 withdrawalHash = Hashing.hashWithdrawal( + Types.WithdrawalTransaction({ + nonce: nonce, + sender: address(feeVault), + target: recipient, + value: amount, + gasLimit: WITHDRAWAL_MIN_GAS, + data: hex"" + }) ); + vm.expectEmit(Predeploys.L2_TO_L1_MESSAGE_PASSER); + emit MessagePassed(nonce, address(feeVault), recipient, amount, WITHDRAWAL_MIN_GAS, hex"", withdrawalHash); + feeVault.withdraw(); - // The withdrawal was successful assertEq(feeVault.totalProcessed(), amount); assertEq(address(feeVault).balance, 0); assertEq(Predeploys.L2_TO_L1_MESSAGE_PASSER.balance, amount); @@ -150,23 +132,16 @@ abstract contract FeeVault_Uncategorized_Test is CommonTest { function test_withdraw_toL2_succeeds() public { _setupL2Withdrawal(); - uint256 amount = feeVault.minWithdrawalAmount() + 1; + uint256 amount = minWithdrawalAmount + 1; vm.deal(address(feeVault), amount); - // No ether has been withdrawn yet assertEq(feeVault.totalProcessed(), 0); + _expectWithdrawalEvents(amount, recipient, Types.WithdrawalNetwork.L2); - vm.expectEmit(address(address(feeVault))); - emit Withdrawal(address(feeVault).balance, feeVault.RECIPIENT(), address(this)); - vm.expectEmit(address(address(feeVault))); - emit Withdrawal(address(feeVault).balance, feeVault.RECIPIENT(), address(this), Types.WithdrawalNetwork.L2); - - // The entire feeVault's balance is withdrawn - vm.expectCall(recipient, address(feeVault).balance, bytes("")); + vm.expectCall(recipient, amount, bytes("")); uint256 withdrawnAmount = feeVault.withdraw(); - // The withdrawal was successful assertEq(withdrawnAmount, amount); assertEq(feeVault.totalProcessed(), amount); assertEq(address(feeVault).balance, 0); @@ -178,17 +153,15 @@ abstract contract FeeVault_Uncategorized_Test is CommonTest { function test_withdraw_toL2recipientReverts_fails() external { _setupL2Withdrawal(); - uint256 amount = feeVault.minWithdrawalAmount(); + uint256 amount = minWithdrawalAmount; vm.deal(address(feeVault), amount); - // No ether has been withdrawn yet assertEq(feeVault.totalProcessed(), 0); // Ensure the RECIPIENT reverts - vm.etch(feeVault.RECIPIENT(), type(Reverter).runtimeCode); + vm.etch(recipient, type(Reverter).runtimeCode); - // The entire feeVault's balance is withdrawn - vm.expectCall(recipient, address(feeVault).balance, bytes("")); + vm.expectCall(recipient, amount, bytes("")); vm.expectRevert("FeeVault: failed to send ETH to L2 fee recipient"); feeVault.withdraw(); assertEq(feeVault.totalProcessed(), 0); @@ -196,77 +169,61 @@ abstract contract FeeVault_Uncategorized_Test is CommonTest { /// @notice Tests that the owner can successfully set minimum withdrawal amount with fuzz testing. function testFuzz_setMinWithdrawalAmount_succeeds(uint256 _newMinWithdrawalAmount) external { - address owner = IProxyAdmin(Predeploys.PROXY_ADMIN).owner(); + vm.prank(proxyAdminOwner); + feeVault.setMinWithdrawalAmount(_newMinWithdrawalAmount); - vm.prank(owner); - IFeeVault(payable(address(feeVault))).setMinWithdrawalAmount(_newMinWithdrawalAmount); - - // Verify the value was updated assertEq(feeVault.minWithdrawalAmount(), _newMinWithdrawalAmount); } /// @notice Tests that non-owner cannot set minimum withdrawal amount with fuzz testing. function testFuzz_setMinWithdrawalAmount_onlyOwner_reverts(address _caller, uint256 _newAmount) external { - address owner = IProxyAdmin(Predeploys.PROXY_ADMIN).owner(); - vm.assume(_caller != owner); + vm.assume(_caller != proxyAdminOwner); uint256 initialAmount = feeVault.minWithdrawalAmount(); vm.prank(_caller); vm.expectRevert(IFeeVault.FeeVault_OnlyProxyAdminOwner.selector); - IFeeVault(payable(address(feeVault))).setMinWithdrawalAmount(_newAmount); + feeVault.setMinWithdrawalAmount(_newAmount); - // Verify the value and boolean flag were NOT changed assertEq(feeVault.minWithdrawalAmount(), initialAmount); } /// @notice Tests that the owner can successfully set recipient with fuzz testing. function testFuzz_setRecipient_succeeds(address _newRecipient) external { - address owner = IProxyAdmin(Predeploys.PROXY_ADMIN).owner(); - - vm.prank(owner); - IFeeVault(payable(address(feeVault))).setRecipient(_newRecipient); + vm.prank(proxyAdminOwner); + feeVault.setRecipient(_newRecipient); - // Verify the value was updated assertEq(feeVault.recipient(), _newRecipient); } /// @notice Tests that non-owner cannot set recipient with fuzz testing. function testFuzz_setRecipient_onlyOwner_reverts(address _caller, address _newRecipient) external { - address owner = IProxyAdmin(Predeploys.PROXY_ADMIN).owner(); - vm.assume(_caller != owner); + vm.assume(_caller != proxyAdminOwner); address initialRecipient = feeVault.recipient(); vm.prank(_caller); vm.expectRevert(IFeeVault.FeeVault_OnlyProxyAdminOwner.selector); - IFeeVault(payable(address(feeVault))).setRecipient(_newRecipient); + feeVault.setRecipient(_newRecipient); - // Verify the value and boolean flag were NOT changed assertEq(feeVault.recipient(), initialRecipient); } /// @notice Tests that the owner can successfully set withdrawal network with fuzz testing. function testFuzz_setWithdrawalNetwork_succeeds(uint8 _networkValue) external { - // Bound to valid enum values (0 = L1, 1 = L2) _networkValue = uint8(bound(_networkValue, 0, 1)); Types.WithdrawalNetwork newNetwork = Types.WithdrawalNetwork(_networkValue); - address owner = IProxyAdmin(Predeploys.PROXY_ADMIN).owner(); - - vm.prank(owner); - IFeeVault(payable(address(feeVault))).setWithdrawalNetwork(newNetwork); + vm.prank(proxyAdminOwner); + feeVault.setWithdrawalNetwork(newNetwork); - // Verify the value was updated assertEq(uint8(feeVault.withdrawalNetwork()), uint8(newNetwork)); } /// @notice Tests that non-owner cannot set withdrawal network with fuzz testing. function testFuzz_setWithdrawalNetwork_onlyOwner_reverts(address _caller, uint8 _networkValue) external { - address owner = IProxyAdmin(Predeploys.PROXY_ADMIN).owner(); - vm.assume(_caller != owner); + vm.assume(_caller != proxyAdminOwner); - // Bound to valid enum values _networkValue = uint8(bound(_networkValue, 0, 1)); Types.WithdrawalNetwork newNetwork = Types.WithdrawalNetwork(_networkValue); @@ -274,9 +231,8 @@ abstract contract FeeVault_Uncategorized_Test is CommonTest { vm.prank(_caller); vm.expectRevert(IFeeVault.FeeVault_OnlyProxyAdminOwner.selector); - IFeeVault(payable(address(feeVault))).setWithdrawalNetwork(newNetwork); + feeVault.setWithdrawalNetwork(newNetwork); - // Verify the value and boolean flag were NOT changed assertEq(uint8(feeVault.withdrawalNetwork()), uint8(initialNetwork)); } } diff --git a/test/L2/GasPriceOracle.t.sol b/test/L2/GasPriceOracle.t.sol index 428ac245..fac45944 100644 --- a/test/L2/GasPriceOracle.t.sol +++ b/test/L2/GasPriceOracle.t.sol @@ -12,7 +12,6 @@ import { stdError } from "lib/forge-std/src/Test.sol"; contract GasPriceOracle_Test is CommonTest { address depositor; - // The initial L1 context values uint64 constant number = 10; uint64 constant timestamp = 11; uint256 constant baseFee = 2 * (10 ** 6); @@ -31,15 +30,43 @@ contract GasPriceOracle_Test is CommonTest { uint64 constant MAX_UINT64 = type(uint64).max; uint32 constant MAX_UINT32 = type(uint32).max; - /// @dev Sets up the test suite. function setUp() public virtual override { super.setUp(); depositor = l1Block.DEPOSITOR_ACCOUNT(); } + + function _setEcotoneL1BlockValues() internal { + bytes memory calldataPacked = Encoding.encodeSetL1BlockValuesEcotone( + baseFeeScalar, blobBaseFeeScalar, sequenceNumber, timestamp, number, baseFee, blobBaseFee, hash, batcherHash + ); + + vm.prank(depositor); + (bool success,) = address(l1Block).call(calldataPacked); + require(success, "GasPriceOracle_Test: L1Block setup failed"); + } + + function _setIsthmusL1BlockValues(uint32 _operatorFeeScalar, uint64 _operatorFeeConstant) internal { + bytes memory calldataPacked = Encoding.encodeSetL1BlockValuesIsthmus( + baseFeeScalar, + blobBaseFeeScalar, + sequenceNumber, + timestamp, + number, + baseFee, + blobBaseFee, + hash, + batcherHash, + _operatorFeeScalar, + _operatorFeeConstant + ); + + vm.prank(depositor); + (bool success,) = address(l1Block).call(calldataPacked); + require(success, "GasPriceOracle_Test: L1Block setup failed"); + } } contract GasPriceOracleBedrock_Test is GasPriceOracle_Test { - /// @dev Sets up the test suite. function setUp() public virtual override { // The gasPriceOracle tests rely on an L2 genesis that is not past Ecotone. l2Fork = Fork.DELTA; @@ -59,42 +86,35 @@ contract GasPriceOracleBedrock_Test is GasPriceOracle_Test { }); } - /// @dev Tests that `l1BaseFee` is set correctly. function test_l1BaseFee_succeeds() external view { assertEq(gasPriceOracle.l1BaseFee(), baseFee); } - /// @dev Tests that `gasPrice` is set correctly. function test_gasPrice_succeeds() external { vm.fee(100); uint256 gasPrice = gasPriceOracle.gasPrice(); assertEq(gasPrice, 100); } - /// @dev Tests that `baseFee` is set correctly. function test_baseFee_succeeds() external { vm.fee(64); uint256 gasPrice = gasPriceOracle.baseFee(); assertEq(gasPrice, 64); } - /// @dev Tests that `scalar` is set correctly. function test_scalar_succeeds() external view { assertEq(gasPriceOracle.scalar(), l1FeeScalar); } - /// @dev Tests that `overhead` is set correctly. function test_overhead_succeeds() external view { assertEq(gasPriceOracle.overhead(), l1FeeOverhead); } - /// @dev Tests that `decimals` is set correctly. function test_decimals_succeeds() external view { assertEq(gasPriceOracle.decimals(), 6); assertEq(gasPriceOracle.DECIMALS(), 6); } - /// @dev Tests that `setGasPrice` reverts since it was removed in bedrock. function test_setGasPrice_doesNotExist_reverts() external { // nosemgrep: sol-style-use-abi-encodecall (bool success, bytes memory returndata) = @@ -104,7 +124,6 @@ contract GasPriceOracleBedrock_Test is GasPriceOracle_Test { assertEq(returndata, hex""); } - /// @dev Tests that `setL1BaseFee` reverts since it was removed in bedrock. function test_setL1BaseFee_doesNotExist_reverts() external { // nosemgrep: sol-style-use-abi-encodecall (bool success, bytes memory returndata) = @@ -114,21 +133,18 @@ contract GasPriceOracleBedrock_Test is GasPriceOracle_Test { assertEq(returndata, hex""); } - /// @dev Tests that Fjord cannot be activated without activating Ecotone function test_setFjord_withoutEcotone_reverts() external { vm.prank(depositor); vm.expectRevert("GasPriceOracle: Fjord can only be activated after Ecotone"); gasPriceOracle.setFjord(); } - /// @dev Tests that Jovian activation requires Isthmus to be active first. function test_setJovian_requiresIsthmus_reverts() external { vm.prank(depositor); vm.expectRevert("GasPriceOracle: Jovian can only be activated after Isthmus"); gasPriceOracle.setJovian(); } - /// @dev Tests that `getL1Fee` returns the expected value when both fjord and ecotone are not active function test_getL1Fee_whenFjordAndEcotoneNotActive_succeeds() external { vm.store(address(gasPriceOracle), bytes32(uint256(0)), bytes32(0)); bytes memory data = hex"1111"; @@ -139,7 +155,6 @@ contract GasPriceOracleBedrock_Test is GasPriceOracle_Test { // l1FeeScalar(i.e. 10)) / 1e6 } - /// @dev Tests that `getL1GasUsed` returns the expected value when both fjord and ecotone are not active function test_getL1GasUsed_whenFjordAndEcotoneNotActive_succeeds() external { vm.store(address(gasPriceOracle), bytes32(uint256(0)), bytes32(0)); bytes memory data = hex"1111"; @@ -150,81 +165,62 @@ contract GasPriceOracleBedrock_Test is GasPriceOracle_Test { } contract GasPriceOracleEcotone_Test is GasPriceOracle_Test { - /// @dev Sets up the test suite. function setUp() public virtual override { l2Fork = Fork.ECOTONE; super.setUp(); assertEq(gasPriceOracle.isEcotone(), true); - bytes memory calldataPacked = Encoding.encodeSetL1BlockValuesEcotone( - baseFeeScalar, blobBaseFeeScalar, sequenceNumber, timestamp, number, baseFee, blobBaseFee, hash, batcherHash - ); - - // Execute the function call - vm.prank(depositor); - (bool success,) = address(l1Block).call(calldataPacked); - require(success, "GasPriceOracleEcotone_Test: Function call failed"); + _setEcotoneL1BlockValues(); } - /// @dev Tests that `setEcotone` is only callable by the depositor. function test_setEcotone_wrongCaller_reverts() external { vm.expectRevert("GasPriceOracle: only the depositor account can set isEcotone flag"); gasPriceOracle.setEcotone(); } - /// @dev Tests that `gasPrice` is set correctly. function test_gasPrice_succeeds() external { vm.fee(100); uint256 gasPrice = gasPriceOracle.gasPrice(); assertEq(gasPrice, 100); } - /// @dev Tests that `baseFee` is set correctly. function test_baseFee_succeeds() external { vm.fee(64); uint256 gasPrice = gasPriceOracle.baseFee(); assertEq(gasPrice, 64); } - /// @dev Tests that `overhead` reverts since it was removed in ecotone. function test_overhead_legacyFunction_reverts() external { vm.expectRevert("GasPriceOracle: overhead() is deprecated"); gasPriceOracle.overhead(); } - /// @dev Tests that `scalar` reverts since it was removed in ecotone. function test_scalar_legacyFunction_reverts() external { vm.expectRevert("GasPriceOracle: scalar() is deprecated"); gasPriceOracle.scalar(); } - /// @dev Tests that `l1BaseFee` is set correctly. function test_l1BaseFee_succeeds() external view { assertEq(gasPriceOracle.l1BaseFee(), baseFee); } - /// @dev Tests that `blobBaseFee` is set correctly. function test_blobBaseFee_succeeds() external view { assertEq(gasPriceOracle.blobBaseFee(), blobBaseFee); } - /// @dev Tests that `baseFeeScalar` is set correctly. function test_baseFeeScalar_succeeds() external view { assertEq(gasPriceOracle.baseFeeScalar(), baseFeeScalar); } - /// @dev Tests that `blobBaseFeeScalar` is set correctly. function test_blobBaseFeeScalar_succeeds() external view { assertEq(gasPriceOracle.blobBaseFeeScalar(), blobBaseFeeScalar); } - /// @dev Tests that `decimals` is set correctly. function test_decimals_succeeds() external view { assertEq(gasPriceOracle.decimals(), 6); assertEq(gasPriceOracle.DECIMALS(), 6); } - /// @dev Tests that `getL1GasUsed` and `getL1Fee` return expected values function test_getL1Fee_succeeds() external view { bytes memory data = hex"0000010203"; // 2 zero bytes, 3 non-zero bytes // (2*4) + (3*16) + (68*16) == 1144 @@ -235,7 +231,6 @@ contract GasPriceOracleEcotone_Test is GasPriceOracle_Test { assertEq(price, 48977); } - /// @dev Tests that `setFjord` is only callable by the depositor. function test_setFjord_wrongCaller_reverts() external { vm.expectRevert("GasPriceOracle: only the depositor account can set isFjord flag"); gasPriceOracle.setFjord(); @@ -243,81 +238,62 @@ contract GasPriceOracleEcotone_Test is GasPriceOracle_Test { } contract GasPriceOracleFjordActive_Test is GasPriceOracle_Test { - /// @dev Sets up the test suite. function setUp() public virtual override { l2Fork = Fork.FJORD; super.setUp(); - bytes memory calldataPacked = Encoding.encodeSetL1BlockValuesEcotone( - baseFeeScalar, blobBaseFeeScalar, sequenceNumber, timestamp, number, baseFee, blobBaseFee, hash, batcherHash - ); - - vm.prank(depositor); - (bool success,) = address(l1Block).call(calldataPacked); - require(success, "GasPriceOracleFjordActive_Test: Function call failed"); + _setEcotoneL1BlockValues(); } - /// @dev Tests that `setFjord` cannot be called when Fjord is already activate function test_setFjord_whenFjordActive_reverts() external { vm.expectRevert("GasPriceOracle: Fjord already active"); vm.prank(depositor); gasPriceOracle.setFjord(); } - /// @dev Tests that `gasPrice` is set correctly. function test_gasPrice_succeeds() external { vm.fee(100); uint256 gasPrice = gasPriceOracle.gasPrice(); assertEq(gasPrice, 100); } - /// @dev Tests that `baseFee` is set correctly. function test_baseFee_succeeds() external { vm.fee(64); uint256 gasPrice = gasPriceOracle.baseFee(); assertEq(gasPrice, 64); } - /// @dev Tests that `overhead` reverts since it was removed in ecotone. function test_overhead_legacyFunction_reverts() external { vm.expectRevert("GasPriceOracle: overhead() is deprecated"); gasPriceOracle.overhead(); } - /// @dev Tests that `scalar` reverts since it was removed in ecotone. function test_scalar_legacyFunction_reverts() external { vm.expectRevert("GasPriceOracle: scalar() is deprecated"); gasPriceOracle.scalar(); } - /// @dev Tests that `l1BaseFee` is set correctly. function test_l1BaseFee_succeeds() external view { assertEq(gasPriceOracle.l1BaseFee(), baseFee); } - /// @dev Tests that `blobBaseFee` is set correctly. function test_blobBaseFee_succeeds() external view { assertEq(gasPriceOracle.blobBaseFee(), blobBaseFee); } - /// @dev Tests that `baseFeeScalar` is set correctly. function test_baseFeeScalar_succeeds() external view { assertEq(gasPriceOracle.baseFeeScalar(), baseFeeScalar); } - /// @dev Tests that `blobBaseFeeScalar` is set correctly. function test_blobBaseFeeScalar_succeeds() external view { assertEq(gasPriceOracle.blobBaseFeeScalar(), blobBaseFeeScalar); } - /// @dev Tests that `decimals` is set correctly. function test_decimals_succeeds() external view { assertEq(gasPriceOracle.decimals(), 6); assertEq(gasPriceOracle.DECIMALS(), 6); } - /// @dev Tests that `getL1GasUsed`, `getL1Fee` and `getL1FeeUpperBound` return expected values - /// for the minimum bound of the linear regression function test_getL1FeeMinimumBound_succeeds() external view { bytes memory data = hex"0000010203"; // fastlzSize: 74, inc signature uint256 gas = gasPriceOracle.getL1GasUsed(data); @@ -337,8 +313,6 @@ contract GasPriceOracleFjordActive_Test is GasPriceOracle_Test { assertEq(upperBound, 68500); } - /// @dev Tests that `getL1GasUsed`, `getL1Fee` and `getL1FeeUpperBound` return expected values - /// for a specific test transaction function test_getL1FeeRegression_succeeds() external view { // fastlzSize: 235, inc signature bytes memory data = hex"1d2c3ec4f5a9b3f3cd2c024e455c1143a74bbd637c324adcbd4f74e346786ac44e23e78f47d932abedd8d1" @@ -361,7 +335,6 @@ contract GasPriceOracleFjordActive_Test is GasPriceOracle_Test { assertEq(upperBound, 111214); } - /// @dev Tests that `operatorFee` is 0 is Isthmus is not activated. function test_getOperatorFee_succeeds() external view { assertEq(gasPriceOracle.isIsthmus(), false); assertEq(gasPriceOracle.getOperatorFee(10), 0); @@ -369,46 +342,25 @@ contract GasPriceOracleFjordActive_Test is GasPriceOracle_Test { } contract GasPriceOracleIsthmus_Test is GasPriceOracle_Test { - /// @dev Sets up the test suite. function setUp() public virtual override { l2Fork = Fork.ISTHMUS; super.setUp(); - bytes memory calldataPacked = Encoding.encodeSetL1BlockValuesIsthmus( - baseFeeScalar, - blobBaseFeeScalar, - sequenceNumber, - timestamp, - number, - baseFee, - blobBaseFee, - hash, - batcherHash, - operatorFeeScalar, - operatorFeeConstant - ); - - vm.prank(depositor); - (bool success,) = address(l1Block).call(calldataPacked); - require(success, "GasPriceOracleIsthmus_Test: Function call failed"); + _setIsthmusL1BlockValues(operatorFeeScalar, operatorFeeConstant); } - /// @dev Tests that `operatorFee` is set correctly using the Isthmus formula (divide by 1e6). function test_getOperatorFee_succeeds() external view { assertEq(gasPriceOracle.getOperatorFee(10), 10 * operatorFeeScalar / 1e6 + operatorFeeConstant); } - /// @dev Tests that `setIsthmus` is only callable by the depositor. function test_setIsthmus_wrongCaller_reverts() external { vm.expectRevert("GasPriceOracle: only the depositor account can set isIsthmus flag"); gasPriceOracle.setIsthmus(); } - /// @dev Tests that Jovian cannot be activated yet (since it's not activated by default). function test_setJovian_notActivated_succeeds() external { assertEq(gasPriceOracle.isJovian(), false); - // Activate Jovian vm.prank(depositor); gasPriceOracle.setJovian(); @@ -417,32 +369,14 @@ contract GasPriceOracleIsthmus_Test is GasPriceOracle_Test { } contract GasPriceOracleJovian_Test is GasPriceOracle_Test { - /// @dev Sets up the test suite with Isthmus parameters configured. function setUp() public virtual override { l2Fork = Fork.ISTHMUS; super.setUp(); - // Configure Isthmus state on the L1 block. - bytes memory calldataPacked = Encoding.encodeSetL1BlockValuesIsthmus( - baseFeeScalar, - blobBaseFeeScalar, - sequenceNumber, - timestamp, - number, - baseFee, - blobBaseFee, - hash, - batcherHash, - operatorFeeScalar, - operatorFeeConstant - ); - - vm.prank(depositor); - (bool success,) = address(l1Block).call(calldataPacked); - require(success, "GasPriceOracleJovian_Test: L1Block setup failed"); + _setIsthmusL1BlockValues(operatorFeeScalar, operatorFeeConstant); assertEq(gasPriceOracle.isIsthmus(), true, "Isthmus should be active before enabling Jovian"); - assertEq(gasPriceOracle.isJovian(), false, "Jovian starts active"); + assertEq(gasPriceOracle.isJovian(), false, "Jovian starts inactive"); } function _activateJovian() internal { @@ -451,40 +385,16 @@ contract GasPriceOracleJovian_Test is GasPriceOracle_Test { assertEq(gasPriceOracle.isJovian(), true, "Jovian activation failed"); } - function _setOperatorFeeParams(uint32 _operatorFeeScalar, uint64 _operatorFeeConstant) internal { - vm.prank(depositor); - (bool success,) = address(l1Block) - .call( - Encoding.encodeSetL1BlockValuesIsthmus( - baseFeeScalar, - blobBaseFeeScalar, - sequenceNumber, - timestamp, - number, - baseFee, - blobBaseFee, - hash, - batcherHash, - _operatorFeeScalar, - _operatorFeeConstant - ) - ); - require(success, "GasPriceOracleJovian_Test: L1Block setup failed"); - } - - /// @dev Tests that `operatorFee` is set correctly using the new Jovian formula (multiply by 100). function test_getOperatorFee_succeeds() external { _activateJovian(); assertEq(gasPriceOracle.getOperatorFee(10), 10 * operatorFeeScalar * 100 + operatorFeeConstant); } - /// @dev Tests that `setJovian` is only callable by the depositor. function test_setJovian_wrongCaller_reverts() external { vm.expectRevert("GasPriceOracle: only the depositor account can set isJovian flag"); gasPriceOracle.setJovian(); } - /// @dev Tests that `setJovian` cannot be activated twice. function test_setJovian_alreadyActive_reverts() external { _activateJovian(); vm.prank(depositor); @@ -492,10 +402,9 @@ contract GasPriceOracleJovian_Test is GasPriceOracle_Test { gasPriceOracle.setJovian(); } - /// @dev Tests the transition from Isthmus formula to Jovian formula. function test_formulaTransition_edgeCases_works() external { // Check Isthmus formula with a low gasUsed value (divide by 1e6) - _setOperatorFeeParams(operatorFeeScalar, operatorFeeConstant); + _setIsthmusL1BlockValues(operatorFeeScalar, operatorFeeConstant); uint256 isthmusFee = gasPriceOracle.getOperatorFee(10); assertEq( isthmusFee, @@ -506,7 +415,7 @@ contract GasPriceOracleJovian_Test is GasPriceOracle_Test { // Use maximum values permitted by data types for scalars. // Use maximum value for gasUsed according to client implementations. // Assert that the fee is as expected (no overflow). - _setOperatorFeeParams(MAX_UINT32, MAX_UINT64); + _setIsthmusL1BlockValues(MAX_UINT32, MAX_UINT64); isthmusFee = gasPriceOracle.getOperatorFee(MAX_UINT64); assertEq( isthmusFee, @@ -516,19 +425,18 @@ contract GasPriceOracleJovian_Test is GasPriceOracle_Test { // Show that the math saturates if the maximum // value for gasUsed (according to data type) is used. - _setOperatorFeeParams(1e6, 1); + _setIsthmusL1BlockValues(1e6, 1); uint256 saturatedIsthmusFee = gasPriceOracle.getOperatorFee(MAX_UINT256); assertEq( saturatedIsthmusFee, - 115792089237316195423570985008687907853269984665640564039457584007913130, + MAX_UINT256 / 1e6 + 1, "Incorrect value for fee under Isthmus (saturating arithmetic triggered)" ); - // Activate Jovian _activateJovian(); // Check Jovian formula with a low gasUsed value (multiply by 100) - _setOperatorFeeParams(operatorFeeScalar, operatorFeeConstant); + _setIsthmusL1BlockValues(operatorFeeScalar, operatorFeeConstant); uint256 jovianFee = gasPriceOracle.getOperatorFee(10); assertEq( jovianFee, @@ -539,7 +447,7 @@ contract GasPriceOracleJovian_Test is GasPriceOracle_Test { // Use maximum values permitted by data types for scalars. // Use maximum value for gasUsed according to client implementations. // Assert that the fee is as expected (no overflow). - _setOperatorFeeParams(MAX_UINT32, MAX_UINT64); + _setIsthmusL1BlockValues(MAX_UINT32, MAX_UINT64); jovianFee = gasPriceOracle.getOperatorFee(MAX_UINT64); assertEq( jovianFee, @@ -549,7 +457,7 @@ contract GasPriceOracleJovian_Test is GasPriceOracle_Test { // Show that a revert is possible if the maximum // value for gasUsed (according to data type) is used. - _setOperatorFeeParams(1, 1); + _setIsthmusL1BlockValues(1, 1); vm.expectRevert(stdError.arithmeticError); gasPriceOracle.getOperatorFee(MAX_UINT256); diff --git a/test/L2/L1Block.t.sol b/test/L2/L1Block.t.sol index d58f17b8..bddc94cd 100644 --- a/test/L2/L1Block.t.sol +++ b/test/L2/L1Block.t.sol @@ -3,16 +3,17 @@ pragma solidity 0.8.15; // Testing import { CommonTest } from "test/setup/CommonTest.sol"; -import { stdStorage, StdStorage } from "lib/forge-std/src/Test.sol"; // Libraries import { Encoding } from "src/libraries/Encoding.sol"; -import { Constants } from "src/libraries/Constants.sol"; -import { Features } from "src/libraries/Features.sol"; /// @title L1Block_ TestInit /// @notice Reusable test initialization for `L1Block` tests. abstract contract L1Block_TestInit is CommonTest { + bytes4 internal constant NOT_DEPOSITOR_SELECTOR = 0x3cc50b45; + bytes32 internal constant LEGACY_HIGH_BITS_MASK = + hex"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000000000"; + address depositor; /// @notice Sets up the test suite. @@ -23,14 +24,23 @@ abstract contract L1Block_TestInit is CommonTest { /// @notice Asserts that legacy high 128-bit ranges in key storage slots remain zeroed. function assertEmptyLegacySlotRanges() internal view { - // 128 high bits mask for 32-byte word - bytes32 mask128 = hex"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000000000"; - // Check scalars and sequenceNumber slot (slot 3) bytes32 scalarsSlot = vm.load(address(l1Block), bytes32(uint256(3))); - assertEq(0, scalarsSlot & mask128); - // Check number and timestamp slot (slot 0) + assertEq(0, scalarsSlot & LEGACY_HIGH_BITS_MASK); + bytes32 numberTimestampSlot = vm.load(address(l1Block), bytes32(uint256(0))); - assertEq(0, numberTimestampSlot & mask128); + assertEq(0, numberTimestampSlot & LEGACY_HIGH_BITS_MASK); + } + + function assertDepositorCallSucceeds(bytes memory callData) internal { + vm.prank(depositor); + (bool success,) = address(l1Block).call(callData); + assertTrue(success, "function call failed"); + } + + function assertNotDepositorReverts(bytes memory callData) internal { + (bool success, bytes memory data) = address(l1Block).call(callData); + assertFalse(success, "function call should have failed"); + assertEq(data, abi.encodeWithSelector(NOT_DEPOSITOR_SELECTOR)); } } @@ -40,7 +50,7 @@ contract L1Block_Version_Test is L1Block_TestInit { /// @notice Tests that the version function returns a valid string. We avoid testing the /// specific value of the string as it changes frequently. function test_version_succeeds() external view { - assert(bytes(l1Block.version()).length > 0); + assertGt(bytes(l1Block.version()).length, 0); } } @@ -106,6 +116,20 @@ contract L1Block_SetL1BlockValues_Test is L1Block_TestInit { /// @title L1Block_SetL1BlockValuesEcotone_Test /// @notice Tests the `setL1BlockValuesEcotone` function of the `L1Block` contract. contract L1Block_SetL1BlockValuesEcotone_Test is L1Block_TestInit { + function encodeMaxSetL1BlockValuesEcotone() internal pure returns (bytes memory) { + return Encoding.encodeSetL1BlockValuesEcotone( + type(uint32).max, + type(uint32).max, + type(uint64).max, + type(uint64).max, + type(uint64).max, + type(uint256).max, + type(uint256).max, + bytes32(type(uint256).max), + bytes32(type(uint256).max) + ); + } + /// @notice Tests that setL1BlockValuesEcotone updates the values appropriately. function testFuzz_setL1BlockValuesEcotone_succeeds( uint32 baseFeeScalar, @@ -124,9 +148,7 @@ contract L1Block_SetL1BlockValuesEcotone_Test is L1Block_TestInit { baseFeeScalar, blobBaseFeeScalar, sequenceNumber, timestamp, number, baseFee, blobBaseFee, hash, batcherHash ); - vm.prank(depositor); - (bool success,) = address(l1Block).call(functionCallDataPacked); - assertTrue(success, "Function call failed"); + assertDepositorCallSucceeds(functionCallDataPacked); assertEq(l1Block.baseFeeScalar(), baseFeeScalar); assertEq(l1Block.blobBaseFeeScalar(), blobBaseFeeScalar); @@ -143,26 +165,20 @@ contract L1Block_SetL1BlockValuesEcotone_Test is L1Block_TestInit { /// @notice Tests that `setL1BlockValuesEcotone` succeeds with max values function test_setL1BlockValuesEcotone_isDepositorMax_succeeds() external { - bytes memory functionCallDataPacked = Encoding.encodeSetL1BlockValuesEcotone( - type(uint32).max, - type(uint32).max, - type(uint64).max, - type(uint64).max, - type(uint64).max, - type(uint256).max, - type(uint256).max, - bytes32(type(uint256).max), - bytes32(type(uint256).max) - ); - - vm.prank(depositor); - (bool success,) = address(l1Block).call(functionCallDataPacked); - assertTrue(success, "function call failed"); + assertDepositorCallSucceeds(encodeMaxSetL1BlockValuesEcotone()); } /// @notice Tests that `setL1BlockValuesEcotone` reverts if sender address is not the depositor function test_setL1BlockValuesEcotone_notDepositor_reverts() external { - bytes memory functionCallDataPacked = Encoding.encodeSetL1BlockValuesEcotone( + assertNotDepositorReverts(encodeMaxSetL1BlockValuesEcotone()); + } +} + +/// @title L1Block_SetL1BlockValuesIsthmus_Test +/// @notice Tests the `setL1BlockValuesIsthmus` function of the `L1Block` contract. +contract L1Block_SetL1BlockValuesIsthmus_Test is L1Block_TestInit { + function encodeMaxSetL1BlockValuesIsthmus() internal pure returns (bytes memory) { + return Encoding.encodeSetL1BlockValuesIsthmus( type(uint32).max, type(uint32).max, type(uint64).max, @@ -171,20 +187,12 @@ contract L1Block_SetL1BlockValuesEcotone_Test is L1Block_TestInit { type(uint256).max, type(uint256).max, bytes32(type(uint256).max), - bytes32(type(uint256).max) + bytes32(type(uint256).max), + type(uint32).max, + type(uint64).max ); - - (bool success, bytes memory data) = address(l1Block).call(functionCallDataPacked); - assertTrue(!success, "function call should have failed"); - // make sure return value is the expected function selector for "NotDepositor()" - bytes memory expReturn = hex"3cc50b45"; - assertEq(data, expReturn); } -} -/// @title L1Block_SetL1BlockValuesIsthmus_Test -/// @notice Tests the `setL1BlockValuesIsthmus` function of the `L1Block` contract. -contract L1Block_SetL1BlockValuesIsthmus_Test is L1Block_TestInit { /// @notice Tests that setL1BlockValuesIsthmus updates the values appropriately. function testFuzz_setL1BlockValuesIsthmus_succeeds( uint32 baseFeeScalar, @@ -215,9 +223,7 @@ contract L1Block_SetL1BlockValuesIsthmus_Test is L1Block_TestInit { operatorFeeConstant ); - vm.prank(depositor); - (bool success,) = address(l1Block).call(functionCallDataPacked); - assertTrue(success, "Function call failed"); + assertDepositorCallSucceeds(functionCallDataPacked); assertEq(l1Block.baseFeeScalar(), baseFeeScalar); assertEq(l1Block.blobBaseFeeScalar(), blobBaseFeeScalar); @@ -236,46 +242,12 @@ contract L1Block_SetL1BlockValuesIsthmus_Test is L1Block_TestInit { /// @notice Tests that `setL1BlockValuesIsthmus` succeeds with max values function test_setL1BlockValuesIsthmus_isDepositorMax_succeeds() external { - bytes memory functionCallDataPacked = Encoding.encodeSetL1BlockValuesIsthmus( - type(uint32).max, - type(uint32).max, - type(uint64).max, - type(uint64).max, - type(uint64).max, - type(uint256).max, - type(uint256).max, - bytes32(type(uint256).max), - bytes32(type(uint256).max), - type(uint32).max, - type(uint64).max - ); - - vm.prank(depositor); - (bool success,) = address(l1Block).call(functionCallDataPacked); - assertTrue(success, "function call failed"); + assertDepositorCallSucceeds(encodeMaxSetL1BlockValuesIsthmus()); } /// @notice Tests that `setL1BlockValuesIsthmus` reverts if sender address is not the depositor function test_setL1BlockValuesIsthmus_notDepositor_reverts() external { - bytes memory functionCallDataPacked = Encoding.encodeSetL1BlockValuesIsthmus( - type(uint32).max, - type(uint32).max, - type(uint64).max, - type(uint64).max, - type(uint64).max, - type(uint256).max, - type(uint256).max, - bytes32(type(uint256).max), - bytes32(type(uint256).max), - type(uint32).max, - type(uint64).max - ); - - (bool success, bytes memory data) = address(l1Block).call(functionCallDataPacked); - assertTrue(!success, "function call should have failed"); - // make sure return value is the expected function selector for "NotDepositor()" - bytes memory expReturn = hex"3cc50b45"; - assertEq(data, expReturn); + assertNotDepositorReverts(encodeMaxSetL1BlockValuesIsthmus()); } } @@ -332,9 +304,7 @@ contract L1Block_SetL1BlockValuesJovian_Test is L1Block_TestInit { params.daFootprintGasScalar ); - vm.prank(depositor); - (bool success,) = address(l1Block).call(functionCallDataPacked); - assertTrue(success, "Function call failed"); + assertDepositorCallSucceeds(functionCallDataPacked); assertEq(l1Block.baseFeeScalar(), params.baseFeeScalar); assertEq(l1Block.blobBaseFeeScalar(), params.blobBaseFeeScalar); @@ -354,11 +324,7 @@ contract L1Block_SetL1BlockValuesJovian_Test is L1Block_TestInit { /// @notice Tests that `setL1BlockValuesJovian` succeeds with max values function test_setL1BlockValuesJovian_isDepositorMax_succeeds() external { - bytes memory functionCallDataPacked = encodeMaxSetL1BlockValuesJovian(); - - vm.prank(depositor); - (bool success,) = address(l1Block).call(functionCallDataPacked); - assertTrue(success, "function call failed"); + assertDepositorCallSucceeds(encodeMaxSetL1BlockValuesJovian()); } /// @notice Prevents the L1 attributes system transaction from approaching its gas limit. @@ -367,20 +333,12 @@ contract L1Block_SetL1BlockValuesJovian_Test is L1Block_TestInit { bytes memory functionCallDataPacked = encodeMaxSetL1BlockValuesJovian(); - vm.prank(depositor); - (bool success,) = address(l1Block).call(functionCallDataPacked); - assertTrue(success, "function call failed"); + assertDepositorCallSucceeds(functionCallDataPacked); assertLt(vm.lastCallGas().gasTotalUsed, 200_000); } /// @notice Tests that `setL1BlockValuesJovian` reverts if sender address is not the depositor function test_setL1BlockValuesJovian_notDepositor_reverts() external { - bytes memory functionCallDataPacked = encodeMaxSetL1BlockValuesJovian(); - - (bool success, bytes memory data) = address(l1Block).call(functionCallDataPacked); - assertTrue(!success, "function call should have failed"); - // make sure return value is the expected function selector for "NotDepositor()" - bytes memory expReturn = hex"3cc50b45"; - assertEq(data, expReturn); + assertNotDepositorReverts(encodeMaxSetL1BlockValuesJovian()); } } diff --git a/test/L2/L1FeeVault.t.sol b/test/L2/L1FeeVault.t.sol index 3e920254..d12077ba 100644 --- a/test/L2/L1FeeVault.t.sol +++ b/test/L2/L1FeeVault.t.sol @@ -1,35 +1,25 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -// Interfaces import { IFeeVault } from "interfaces/L2/IFeeVault.sol"; - -// Libraries import { Predeploys } from "src/libraries/Predeploys.sol"; import { FeeVault_Uncategorized_Test } from "test/L2/FeeVault.t.sol"; import { Types } from "src/libraries/Types.sol"; import { SemverComp } from "src/libraries/SemverComp.sol"; import { CommonTest } from "test/setup/CommonTest.sol"; -/// @title L1FeeVault_Version_Test -/// @notice Tests the `version` function of the `L1FeeVault` contract. contract L1FeeVault_Version_Test is CommonTest { - /// @notice Tests that the version returns a valid semver string. function test_version_succeeds() external view { SemverComp.parse(l1FeeVault.version()); } } -/// @title L1FeeVault_Uncategorized_Test -/// @notice Test contract for the L1FeeVault contract's functionality contract L1FeeVault_Uncategorized_Test is FeeVault_Uncategorized_Test { - /// @dev Sets up the test suite. function setUp() public virtual override { super.setUp(); recipient = deploy.cfg().l1FeeVaultRecipient(); - feeVaultName = "L1FeeVault"; minWithdrawalAmount = deploy.cfg().l1FeeVaultMinimumWithdrawalAmount(); feeVault = IFeeVault(payable(Predeploys.L1_FEE_VAULT)); - withdrawalNetwork = Types.WithdrawalNetwork(uint8(deploy.cfg().l1FeeVaultWithdrawalNetwork())); + withdrawalNetwork = Types.WithdrawalNetwork(deploy.cfg().l1FeeVaultWithdrawalNetwork()); } } diff --git a/test/L2/L2CrossDomainMessenger.t.sol b/test/L2/L2CrossDomainMessenger.t.sol index 73a03480..51625e76 100644 --- a/test/L2/L2CrossDomainMessenger.t.sol +++ b/test/L2/L2CrossDomainMessenger.t.sol @@ -12,7 +12,6 @@ import { Hashing } from "src/libraries/Hashing.sol"; import { Encoding } from "src/libraries/Encoding.sol"; import { Types } from "src/libraries/Types.sol"; import { AddressAliasHelper } from "src/vendor/AddressAliasHelper.sol"; -import { ForgeArtifacts, StorageSlot } from "scripts/libraries/ForgeArtifacts.sol"; // Interfaces import { IL2CrossDomainMessenger } from "interfaces/L2/IL2CrossDomainMessenger.sol"; @@ -21,8 +20,7 @@ import { IL2ToL1MessagePasser } from "interfaces/L2/IL2ToL1MessagePasser.sol"; /// @title L2CrossDomainMessenger_TestInit /// @notice Reusable test initialization for `L2CrossDomainMessenger` tests. abstract contract L2CrossDomainMessenger_TestInit is CommonTest { - /// @notice Receiver address for testing - address recipient = address(0xabbaacdc); + address internal constant recipient = address(0xabbaacdc); } /// @title L2CrossDomainMessenger_Constructor_Test @@ -73,38 +71,42 @@ contract L2CrossDomainMessenger_SendMessage_Test is L2CrossDomainMessenger_TestI /// @notice Tests that `sendMessage` executes successfully with the original test case. function test_sendMessage_succeeds() external { + uint32 minGasLimit = 100; + bytes memory message = hex"ff"; + uint256 nonce = l2CrossDomainMessenger.messageNonce(); + uint256 withdrawalNonce = l2ToL1MessagePasser.messageNonce(); + uint64 baseGas = l2CrossDomainMessenger.baseGas(message, minGasLimit); bytes memory xDomainCallData = - Encoding.encodeCrossDomainMessage(l2CrossDomainMessenger.messageNonce(), alice, recipient, 0, 100, hex"ff"); + Encoding.encodeCrossDomainMessage(nonce, alice, recipient, 0, minGasLimit, message); vm.expectCall( address(l2ToL1MessagePasser), abi.encodeCall( - IL2ToL1MessagePasser.initiateWithdrawal, - (address(l1CrossDomainMessenger), l2CrossDomainMessenger.baseGas(hex"ff", 100), xDomainCallData) + IL2ToL1MessagePasser.initiateWithdrawal, (address(l1CrossDomainMessenger), baseGas, xDomainCallData) ) ); vm.expectEmit(true, true, true, true); emit MessagePassed( - l2ToL1MessagePasser.messageNonce(), + withdrawalNonce, address(l2CrossDomainMessenger), address(l1CrossDomainMessenger), 0, - l2CrossDomainMessenger.baseGas(hex"ff", 100), + baseGas, xDomainCallData, Hashing.hashWithdrawal( Types.WithdrawalTransaction({ - nonce: l2ToL1MessagePasser.messageNonce(), + nonce: withdrawalNonce, sender: address(l2CrossDomainMessenger), target: address(l1CrossDomainMessenger), value: 0, - gasLimit: l2CrossDomainMessenger.baseGas(hex"ff", 100), + gasLimit: baseGas, data: xDomainCallData }) ) ); vm.prank(alice); - l2CrossDomainMessenger.sendMessage(recipient, hex"ff", uint32(100)); + l2CrossDomainMessenger.sendMessage(recipient, message, minGasLimit); } /// @notice Tests that `sendMessage` can be called twice and that the nonce increments correctly. @@ -120,6 +122,25 @@ contract L2CrossDomainMessenger_SendMessage_Test is L2CrossDomainMessenger_TestI /// @notice General tests that are not testing any function directly of the /// `L2CrossDomainMessenger` contract. contract L2CrossDomainMessenger_Uncategorized_Test is L2CrossDomainMessenger_TestInit { + uint256 internal constant l2SenderSlotIndex = 50; + + function _versionedNonce(uint16 _version) internal pure returns (uint256) { + return Encoding.encodeVersionedNonce({ _nonce: 0, _version: _version }); + } + + function _l1MessengerAlias() internal view returns (address) { + return AddressAliasHelper.applyL1ToL2Alias(address(l1CrossDomainMessenger)); + } + + function _setPortalL2Sender(address _sender) internal { + vm.store(address(optimismPortal2), bytes32(l2SenderSlotIndex), bytes32(abi.encode(_sender))); + } + + function _assertMessageStatus(bytes32 _hash, bool _successful, bool _failed) internal view { + assertEq(l2CrossDomainMessenger.successfulMessages(_hash), _successful); + assertEq(l2CrossDomainMessenger.failedMessages(_hash), _failed); + } + /// @notice Tests that `messageNonce` can be decoded correctly. function test_messageVersion_succeeds() external view { (, uint16 version) = Encoding.decodeVersionedNonce(l2CrossDomainMessenger.messageNonce()); @@ -136,28 +157,20 @@ contract L2CrossDomainMessenger_Uncategorized_Test is L2CrossDomainMessenger_Tes function test_relayMessage_v2_reverts() external { address target = address(0xabcd); address sender = address(l1CrossDomainMessenger); - address caller = AddressAliasHelper.applyL1ToL2Alias(address(l1CrossDomainMessenger)); + address caller = _l1MessengerAlias(); - // Expect a revert. vm.expectRevert("CrossDomainMessenger: only version 0 or 1 messages are supported at this time"); - // Try to relay a v2 message. vm.prank(caller); - l2CrossDomainMessenger.relayMessage( - Encoding.encodeVersionedNonce(0, 2), // nonce - sender, - target, - 0, // value - 0, - hex"1111" - ); + l2CrossDomainMessenger.relayMessage(_versionedNonce(2), sender, target, 0, 0, hex"1111"); } /// @notice Tests that `relayMessage` executes successfully. function test_relayMessage_succeeds() external { address target = address(0xabcd); address sender = address(l1CrossDomainMessenger); - address caller = AddressAliasHelper.applyL1ToL2Alias(address(l1CrossDomainMessenger)); + address caller = _l1MessengerAlias(); + uint256 nonce = _versionedNonce(1); vm.expectCall(target, hex"1111"); @@ -165,106 +178,67 @@ contract L2CrossDomainMessenger_Uncategorized_Test is L2CrossDomainMessenger_Tes vm.expectEmit(true, true, true, true); - bytes32 hash = - Hashing.hashCrossDomainMessage(Encoding.encodeVersionedNonce(0, 1), sender, target, 0, 0, hex"1111"); + bytes32 hash = Hashing.hashCrossDomainMessage(nonce, sender, target, 0, 0, hex"1111"); emit RelayedMessage(hash); - l2CrossDomainMessenger.relayMessage( - Encoding.encodeVersionedNonce(0, 1), // nonce - sender, - target, - 0, // value - 0, - hex"1111" - ); + l2CrossDomainMessenger.relayMessage(nonce, sender, target, 0, 0, hex"1111"); - // the message hash is in the successfulMessages mapping - assert(l2CrossDomainMessenger.successfulMessages(hash)); - // it is not in the received messages mapping - assertEq(l2CrossDomainMessenger.failedMessages(hash), false); + _assertMessageStatus(hash, true, false); } /// @notice Tests that `relayMessage` reverts if the value sent does not match the amount function test_relayMessage_fromOtherMessengerValueMismatch_reverts() external { - // set the target to be alice address target = alice; address sender = address(l1CrossDomainMessenger); - address caller = AddressAliasHelper.applyL1ToL2Alias(address(l1CrossDomainMessenger)); + address caller = _l1MessengerAlias(); bytes memory message = hex"1111"; - // cannot send a message where the amount inputted does not match the msg.value vm.deal(caller, 10 ether); vm.prank(caller); vm.expectRevert(stdError.assertionError); - l2CrossDomainMessenger.relayMessage{ value: 10 ether }( - Encoding.encodeVersionedNonce({ _nonce: 0, _version: 1 }), sender, target, 9 ether, 0, message - ); + l2CrossDomainMessenger.relayMessage{ value: 10 ether }(_versionedNonce(1), sender, target, 9 ether, 0, message); } /// @notice Tests that `relayMessage` reverts if a failed message is attempted to be replayed /// and the caller is the other messenger function test_relayMessage_fromOtherMessengerFailedMessageReplay_reverts() external { - // set the target to be alice address target = alice; address sender = address(l1CrossDomainMessenger); - address caller = AddressAliasHelper.applyL1ToL2Alias(address(l1CrossDomainMessenger)); + address caller = _l1MessengerAlias(); + uint256 nonce = _versionedNonce(1); bytes memory message = hex"1111"; - // make a failed message vm.etch(target, hex"fe"); vm.prank(caller); - l2CrossDomainMessenger.relayMessage( - Encoding.encodeVersionedNonce({ _nonce: 0, _version: 1 }), sender, target, 0, 0, message - ); + l2CrossDomainMessenger.relayMessage(nonce, sender, target, 0, 0, message); - // cannot replay messages when the caller is the other messenger vm.prank(caller); vm.expectRevert(stdError.assertionError); - l2CrossDomainMessenger.relayMessage( - Encoding.encodeVersionedNonce({ _nonce: 0, _version: 1 }), sender, target, 0, 0, message - ); + l2CrossDomainMessenger.relayMessage(nonce, sender, target, 0, 0, message); } /// @notice Tests that `relayMessage` reverts if attempting to relay a message sent to self function test_relayMessage_toSelf_reverts() external { address sender = address(l1CrossDomainMessenger); - address caller = AddressAliasHelper.applyL1ToL2Alias(address(l1CrossDomainMessenger)); + address caller = _l1MessengerAlias(); bytes memory message = hex"1111"; - vm.store(address(optimismPortal2), bytes32(0), bytes32(abi.encode(sender))); - vm.prank(caller); vm.expectRevert("CrossDomainMessenger: cannot send message to blocked system address"); - l2CrossDomainMessenger.relayMessage( - Encoding.encodeVersionedNonce({ _nonce: 0, _version: 1 }), - sender, - address(l2CrossDomainMessenger), - 0, - 0, - message - ); + l2CrossDomainMessenger.relayMessage(_versionedNonce(1), sender, address(l2CrossDomainMessenger), 0, 0, message); } /// @notice Tests that `relayMessage` reverts if attempting to relay a message sent to the /// `l2ToL1MessagePasser` address function test_relayMessage_toL2ToL1MessagePasser_reverts() external { address sender = address(l1CrossDomainMessenger); - address caller = AddressAliasHelper.applyL1ToL2Alias(address(l1CrossDomainMessenger)); + address caller = _l1MessengerAlias(); bytes memory message = hex"1111"; - vm.store(address(optimismPortal2), bytes32(0), bytes32(abi.encode(sender))); - vm.prank(caller); vm.expectRevert("CrossDomainMessenger: cannot send message to blocked system address"); - l2CrossDomainMessenger.relayMessage( - Encoding.encodeVersionedNonce({ _nonce: 0, _version: 1 }), - sender, - address(l2ToL1MessagePasser), - 0, - 0, - message - ); + l2CrossDomainMessenger.relayMessage(_versionedNonce(1), sender, address(l2ToL1MessagePasser), 0, 0, message); } /// @notice Tests that `relayMessage` reverts if the message called by non-`optimismPortal2` @@ -274,13 +248,9 @@ contract L2CrossDomainMessenger_Uncategorized_Test is L2CrossDomainMessenger_Tes address sender = address(l1CrossDomainMessenger); bytes memory message = hex"1111"; - vm.store(address(optimismPortal2), bytes32(0), bytes32(abi.encode(sender))); - vm.prank(bob); vm.expectRevert("CrossDomainMessenger: message cannot be replayed"); - l2CrossDomainMessenger.relayMessage( - Encoding.encodeVersionedNonce({ _nonce: 0, _version: 1 }), sender, target, 0, 0, message - ); + l2CrossDomainMessenger.relayMessage(_versionedNonce(1), sender, target, 0, 0, message); } /// @notice Tests that `relayMessage` on L2 will always succeed for any potential message, @@ -327,7 +297,7 @@ contract L2CrossDomainMessenger_Uncategorized_Test is L2CrossDomainMessenger_Tes // Encode the message. bytes memory encoded = Encoding.encodeCrossDomainMessage( - Encoding.encodeVersionedNonce(0, 1), // nonce + _versionedNonce(1), alice, // Sender doesn't matter target, 0, // Value doesn't matter @@ -335,40 +305,36 @@ contract L2CrossDomainMessenger_Uncategorized_Test is L2CrossDomainMessenger_Tes _message ); - // Count the number of non-zero bytes in the message. - uint256 zeroBytesInCalldata = 0; + // Count calldata bytes so the EIP-7623 floor can be checked against actual encoded data. uint256 nonzeroBytesInCalldata = 0; for (uint256 i = 0; i < encoded.length; i++) { if (encoded[i] != bytes1(0)) { nonzeroBytesInCalldata++; - } else { - zeroBytesInCalldata++; } } - // Base gas must always be sufficient to cover the floor cost from EIP-7623. - assertGt(baseGas, 21000 + ((zeroBytesInCalldata + nonzeroBytesInCalldata * 4) * 10)); + uint256 zeroBytesInCalldata = encoded.length - nonzeroBytesInCalldata; + uint256 calldataTokens = zeroBytesInCalldata + nonzeroBytesInCalldata * 4; + uint256 floorCost = l2CrossDomainMessenger.TX_BASE_GAS() + calldataTokens + * (l2CrossDomainMessenger.FLOOR_CALLDATA_OVERHEAD() / 4); - // In the L2 => L1 direction we actually get all of the base gas supplied, nothing is - // deducted. This is an advantage over the L1 => L2 direction because it means the base - // gas goes a lot further. - uint256 gasSupplied = baseGas; + // Base gas must always be sufficient to cover the floor cost from EIP-7623. + assertGt(baseGas, floorCost); - // Store the value of l2Sender in the OptimismPortal2 contract. - StorageSlot memory slot = ForgeArtifacts.getSlot("OptimismPortal2", "l2Sender"); - vm.store(address(optimismPortal2), bytes32(slot.slot), bytes32(abi.encode(address(l2CrossDomainMessenger)))); + _setPortalL2Sender(address(l2CrossDomainMessenger)); - // We'll trigger the L1CrossDomainMessenger as if we're the OptimismPortal2 vm.prank(address(optimismPortal2)); - // Trigger the L1CrossDomainMessenger. - // Should NOT fail. - (bool success,) = address(l1CrossDomainMessenger).call{ gas: gasSupplied }(encoded); + // In the L2 => L1 direction we actually get all of the base gas supplied, nothing is + // deducted. This is an advantage over the L1 => L2 direction because it means the base + // gas goes a lot further. + (bool success,) = address(l1CrossDomainMessenger).call{ gas: baseGas }(encoded); assertTrue(success, "L1CrossDomainMessenger call should not fail"); // Message should either be in the failed or successful messages mapping. - bool inFailedMessages = l1CrossDomainMessenger.failedMessages(keccak256(encoded)); - bool inSuccessfulMessages = l1CrossDomainMessenger.successfulMessages(keccak256(encoded)); + bytes32 encodedHash = keccak256(encoded); + bool inFailedMessages = l1CrossDomainMessenger.failedMessages(encodedHash); + bool inSuccessfulMessages = l1CrossDomainMessenger.successfulMessages(encodedHash); assertTrue( inFailedMessages || inSuccessfulMessages, "message should be in either failed or successful messages" ); @@ -380,9 +346,9 @@ contract L2CrossDomainMessenger_Uncategorized_Test is L2CrossDomainMessenger_Tes vm.expectRevert("CrossDomainMessenger: xDomainMessageSender is not set"); l2CrossDomainMessenger.xDomainMessageSender(); - address caller = AddressAliasHelper.applyL1ToL2Alias(address(l1CrossDomainMessenger)); + address caller = _l1MessengerAlias(); vm.prank(caller); - l2CrossDomainMessenger.relayMessage(Encoding.encodeVersionedNonce(0, 1), address(0), address(0), 0, 0, hex""); + l2CrossDomainMessenger.relayMessage(_versionedNonce(1), address(0), address(0), 0, 0, hex""); vm.expectRevert("CrossDomainMessenger: xDomainMessageSender is not set"); l2CrossDomainMessenger.xDomainMessageSender(); @@ -393,28 +359,20 @@ contract L2CrossDomainMessenger_Uncategorized_Test is L2CrossDomainMessenger_Tes function test_relayMessage_retry_succeeds() external { address target = address(0xabcd); address sender = address(l1CrossDomainMessenger); - address caller = AddressAliasHelper.applyL1ToL2Alias(address(l1CrossDomainMessenger)); + address caller = _l1MessengerAlias(); + uint256 nonce = _versionedNonce(1); uint256 value = 100; - bytes32 hash = - Hashing.hashCrossDomainMessage(Encoding.encodeVersionedNonce(0, 1), sender, target, value, 0, hex"1111"); + bytes32 hash = Hashing.hashCrossDomainMessage(nonce, sender, target, value, 0, hex"1111"); vm.etch(target, address(new Reverter()).code); vm.deal(address(caller), value); vm.prank(caller); - l2CrossDomainMessenger.relayMessage{ value: value }( - Encoding.encodeVersionedNonce(0, 1), // nonce - sender, - target, - value, - 0, - hex"1111" - ); + l2CrossDomainMessenger.relayMessage{ value: value }(nonce, sender, target, value, 0, hex"1111"); assertEq(address(l2CrossDomainMessenger).balance, value); assertEq(address(target).balance, 0); - assertEq(l2CrossDomainMessenger.successfulMessages(hash), false); - assertEq(l2CrossDomainMessenger.failedMessages(hash), true); + _assertMessageStatus(hash, false, true); vm.expectEmit(true, true, true, true); @@ -422,18 +380,10 @@ contract L2CrossDomainMessenger_Uncategorized_Test is L2CrossDomainMessenger_Tes vm.etch(target, address(0).code); vm.prank(address(sender)); - l2CrossDomainMessenger.relayMessage( - Encoding.encodeVersionedNonce(0, 1), // nonce - sender, - target, - value, - 0, - hex"1111" - ); + l2CrossDomainMessenger.relayMessage(nonce, sender, target, value, 0, hex"1111"); assertEq(address(l2CrossDomainMessenger).balance, 0); assertEq(address(target).balance, value); - assertEq(l2CrossDomainMessenger.successfulMessages(hash), true); - assertEq(l2CrossDomainMessenger.failedMessages(hash), true); + _assertMessageStatus(hash, true, true); } } diff --git a/test/L2/L2ERC721Bridge.t.sol b/test/L2/L2ERC721Bridge.t.sol index 4be2803a..07f15b0c 100644 --- a/test/L2/L2ERC721Bridge.t.sol +++ b/test/L2/L2ERC721Bridge.t.sol @@ -5,22 +5,11 @@ pragma solidity 0.8.15; import { CommonTest } from "test/setup/CommonTest.sol"; // Contracts -import { ERC721 } from "lib/openzeppelin-contracts/contracts/token/ERC721/ERC721.sol"; import { OptimismMintableERC721 } from "src/L2/OptimismMintableERC721.sol"; // Interfaces import { IL1ERC721Bridge } from "interfaces/L1/IL1ERC721Bridge.sol"; -import { IL2ERC721Bridge } from "interfaces/L2/IL2ERC721Bridge.sol"; - -/// @title TestERC721 -/// @notice A test ERC721 token used for `L2ERC721Bridge` tests. -contract L2ERC721Bridge_TestERC721_Harness is ERC721 { - constructor() ERC721("Test", "TST") { } - - function mint(address to, uint256 tokenId) public { - _mint(to, tokenId); - } -} +import { ICrossDomainMessenger } from "interfaces/universal/ICrossDomainMessenger.sol"; /// @title TestMintableERC721 /// @notice A test OptimismMintableERC721 token used for `L2ERC721Bridge` tests. @@ -53,9 +42,7 @@ contract L2ERC721Bridge_NonCompliantERC721_Harness { return address(0x01); } - function burn(address, uint256) external { - // Do nothing. - } + function burn(address, uint256) external { } function supportsInterface(bytes4) external pure returns (bool) { return false; @@ -65,9 +52,10 @@ contract L2ERC721Bridge_NonCompliantERC721_Harness { /// @title L2ERC721Bridge_TestInit /// @notice Reusable test initialization for `L2ERC721Bridge` tests. abstract contract L2ERC721Bridge_TestInit is CommonTest { - L2ERC721Bridge_TestMintableERC721_Harness internal localToken; - L2ERC721Bridge_TestERC721_Harness internal remoteToken; uint256 internal constant tokenId = 1; + uint32 internal constant DEFAULT_MIN_GAS_LIMIT = 1234; + bytes internal constant EXTRA_DATA = hex"5678"; + address internal constant remoteToken = address(0x01); event ERC721BridgeInitiated( address indexed localToken, @@ -86,21 +74,67 @@ abstract contract L2ERC721Bridge_TestInit is CommonTest { uint256 tokenId, bytes extraData ); +} + +abstract contract L2ERC721Bridge_Bridge_TestInit is L2ERC721Bridge_TestInit { + L2ERC721Bridge_TestMintableERC721_Harness internal localToken; /// @notice Sets up the test suite. - function setUp() public override { + function setUp() public virtual override { super.setUp(); - remoteToken = new L2ERC721Bridge_TestERC721_Harness(); - localToken = new L2ERC721Bridge_TestMintableERC721_Harness(address(l2ERC721Bridge), address(remoteToken)); + localToken = new L2ERC721Bridge_TestMintableERC721_Harness(address(l2ERC721Bridge), remoteToken); - // Mint alice a token. localToken.mint(alice, tokenId); - // Approve the bridge to transfer the token. vm.prank(alice); localToken.approve(address(l2ERC721Bridge), tokenId); } + + function _expectBridgeMessage(address _to) internal { + bytes memory message = abi.encodeCall( + IL1ERC721Bridge.finalizeBridgeERC721, (remoteToken, address(localToken), alice, _to, tokenId, EXTRA_DATA) + ); + + vm.expectCall( + address(l2ERC721Bridge.messenger()), + abi.encodeCall( + ICrossDomainMessenger.sendMessage, + (address(l2ERC721Bridge.otherBridge()), message, DEFAULT_MIN_GAS_LIMIT) + ) + ); + } + + function _expectBridgeInitiated(address _to) internal { + vm.expectEmit(address(l2ERC721Bridge)); + emit ERC721BridgeInitiated(address(localToken), remoteToken, alice, _to, tokenId, EXTRA_DATA); + } + + function _mockXDomainMessageSender(address _sender) internal { + vm.mockCall( + address(l2ERC721Bridge.messenger()), + abi.encodeCall(ICrossDomainMessenger.xDomainMessageSender, ()), + abi.encode(_sender) + ); + } + + function _mockOtherBridge() internal { + _mockXDomainMessageSender(address(l2ERC721Bridge.otherBridge())); + } + + function _bridgeToken() internal { + vm.prank(alice, alice); + l2ERC721Bridge.bridgeERC721(address(localToken), remoteToken, tokenId, DEFAULT_MIN_GAS_LIMIT, EXTRA_DATA); + } + + function _assertTokenBurned() internal { + vm.expectRevert("ERC721: invalid token ID"); + localToken.ownerOf(tokenId); + } + + function _assertTokenOwnedBy(address _owner) internal view { + assertEq(localToken.ownerOf(tokenId), _owner); + } } /// @title L2ERC721Bridge_Test_Constructor @@ -117,263 +151,177 @@ contract L2ERC721Bridge_Constructor_Test is L2ERC721Bridge_TestInit { /// @title L2ERC721Bridge_FinalizeBridgeERC721_Test /// @notice Tests the `finalizeBridgeERC721` function of the `L2ERC721Bridge` contract. -contract L2ERC721Bridge_FinalizeBridgeERC721_Test is L2ERC721Bridge_TestInit { +contract L2ERC721Bridge_FinalizeBridgeERC721_Test is L2ERC721Bridge_Bridge_TestInit { /// @notice Tests that `finalizeBridgeERC721` correctly finalizes a bridged token. function test_finalizeBridgeERC721_succeeds() external { - // Bridge the token. - vm.prank(alice, alice); - l2ERC721Bridge.bridgeERC721(address(localToken), address(remoteToken), tokenId, 1234, hex"5678"); + _bridgeToken(); - // Expect an event to be emitted. - vm.expectEmit(true, true, true, true); - emit ERC721BridgeFinalized(address(localToken), address(remoteToken), alice, alice, tokenId, hex"5678"); + vm.expectEmit(address(l2ERC721Bridge)); + emit ERC721BridgeFinalized(address(localToken), remoteToken, alice, alice, tokenId, EXTRA_DATA); - // Finalize a withdrawal. - vm.mockCall( - address(l2CrossDomainMessenger), - abi.encodeCall(l2CrossDomainMessenger.xDomainMessageSender, ()), - abi.encode(l1ERC721Bridge) - ); - vm.prank(address(l2CrossDomainMessenger)); - l2ERC721Bridge.finalizeBridgeERC721(address(localToken), address(remoteToken), alice, alice, tokenId, hex"5678"); + _mockOtherBridge(); + vm.prank(address(l2ERC721Bridge.messenger())); + l2ERC721Bridge.finalizeBridgeERC721(address(localToken), remoteToken, alice, alice, tokenId, EXTRA_DATA); - // Token is not locked in the bridge. - assertEq(localToken.ownerOf(tokenId), alice); + _assertTokenOwnedBy(alice); } /// @notice Tests that `finalizeBridgeERC721` reverts if the token is not compliant with the /// `IOptimismMintableERC721` interface. function test_finalizeBridgeERC721_interfaceNotCompliant_reverts() external { - // Create a non-compliant token L2ERC721Bridge_NonCompliantERC721_Harness nonCompliantToken = new L2ERC721Bridge_NonCompliantERC721_Harness(alice); - // Bridge the non-compliant token. vm.prank(alice, alice); - l2ERC721Bridge.bridgeERC721(address(nonCompliantToken), address(0x01), tokenId, 1234, hex"5678"); + l2ERC721Bridge.bridgeERC721(address(nonCompliantToken), remoteToken, tokenId, DEFAULT_MIN_GAS_LIMIT, EXTRA_DATA); - // Attempt to finalize the withdrawal. Should revert because the token does not claim to be - // compliant with the `IOptimismMintableERC721` interface. - vm.mockCall( - address(l2CrossDomainMessenger), - abi.encodeCall(l2CrossDomainMessenger.xDomainMessageSender, ()), - abi.encode(l1ERC721Bridge) - ); - vm.prank(address(l2CrossDomainMessenger)); + _mockOtherBridge(); + vm.prank(address(l2ERC721Bridge.messenger())); vm.expectRevert("L2ERC721Bridge: local token interface is not compliant"); - l2ERC721Bridge.finalizeBridgeERC721( - address(address(nonCompliantToken)), address(address(0x01)), alice, alice, tokenId, hex"5678" - ); + l2ERC721Bridge.finalizeBridgeERC721(address(nonCompliantToken), remoteToken, alice, alice, tokenId, EXTRA_DATA); } /// @notice Tests that `finalizeBridgeERC721` reverts when not called by the remote bridge. function test_finalizeBridgeERC721_notViaLocalMessenger_reverts() external { - // Finalize a withdrawal. vm.prank(alice); vm.expectRevert("ERC721Bridge: function can only be called from the other bridge"); - l2ERC721Bridge.finalizeBridgeERC721(address(localToken), address(remoteToken), alice, alice, tokenId, hex"5678"); + l2ERC721Bridge.finalizeBridgeERC721(address(localToken), remoteToken, alice, alice, tokenId, EXTRA_DATA); } /// @notice Tests that `finalizeBridgeERC721` reverts when not called by the remote bridge. function test_finalizeBridgeERC721_notFromRemoteMessenger_reverts() external { - // Finalize a withdrawal. - vm.mockCall( - address(l2CrossDomainMessenger), - abi.encodeCall(l2CrossDomainMessenger.xDomainMessageSender, ()), - abi.encode(alice) - ); - vm.prank(address(l2CrossDomainMessenger)); + _mockXDomainMessageSender(alice); + vm.prank(address(l2ERC721Bridge.messenger())); vm.expectRevert("ERC721Bridge: function can only be called from the other bridge"); - l2ERC721Bridge.finalizeBridgeERC721(address(localToken), address(remoteToken), alice, alice, tokenId, hex"5678"); + l2ERC721Bridge.finalizeBridgeERC721(address(localToken), remoteToken, alice, alice, tokenId, EXTRA_DATA); } /// @notice Tests that `finalizeBridgeERC721` reverts when the local token is the address of /// the bridge itself. function test_finalizeBridgeERC721_selfToken_reverts() external { - // Finalize a withdrawal. - vm.mockCall( - address(l2CrossDomainMessenger), - abi.encodeCall(l2CrossDomainMessenger.xDomainMessageSender, ()), - abi.encode(address(l1ERC721Bridge)) - ); - vm.prank(address(l2CrossDomainMessenger)); + _mockOtherBridge(); + vm.prank(address(l2ERC721Bridge.messenger())); vm.expectRevert("L2ERC721Bridge: local token cannot be self"); - l2ERC721Bridge.finalizeBridgeERC721( - address(l2ERC721Bridge), address(remoteToken), alice, alice, tokenId, hex"5678" - ); + l2ERC721Bridge.finalizeBridgeERC721(address(l2ERC721Bridge), remoteToken, alice, alice, tokenId, EXTRA_DATA); } /// @notice Tests that `finalizeBridgeERC721` reverts when already finalized. function test_finalizeBridgeERC721_alreadyExists_reverts() external { - // Finalize a withdrawal. - vm.mockCall( - address(l2CrossDomainMessenger), - abi.encodeCall(l2CrossDomainMessenger.xDomainMessageSender, ()), - abi.encode(address(l1ERC721Bridge)) - ); - vm.prank(address(l2CrossDomainMessenger)); + _mockOtherBridge(); + vm.prank(address(l2ERC721Bridge.messenger())); vm.expectRevert("ERC721: token already minted"); - l2ERC721Bridge.finalizeBridgeERC721(address(localToken), address(remoteToken), alice, alice, tokenId, hex"5678"); + l2ERC721Bridge.finalizeBridgeERC721(address(localToken), remoteToken, alice, alice, tokenId, EXTRA_DATA); } } -/// @title L2ERC721Bridge_Uncategorized_Test -/// @notice General tests that are not testing any function directly of the `L2ERC721Bridge` -/// contract or are testing multiple functions at once. -contract L2ERC721Bridge_Uncategorized_Test is L2ERC721Bridge_TestInit { +/// @title L2ERC721Bridge_Paused_Test +/// @notice Tests the `paused` function of the `L2ERC721Bridge` contract. +contract L2ERC721Bridge_Paused_Test is L2ERC721Bridge_TestInit { /// @notice Ensures that the L2ERC721Bridge is always not paused. The pausability happens on L1 /// and not L2. function test_paused_succeeds() external view { assertFalse(l2ERC721Bridge.paused()); } +} +/// @title L2ERC721Bridge_BridgeERC721_Test +/// @notice Tests the `bridgeERC721` and `bridgeERC721To` functions of the `L2ERC721Bridge` contract. +contract L2ERC721Bridge_BridgeERC721_Test is L2ERC721Bridge_Bridge_TestInit { /// @notice Tests that `bridgeERC721` correctly bridges a token and burns it on the origin /// chain. function test_bridgeERC721_succeeds() public { - // Expect a call to the messenger. - vm.expectCall( - address(l2CrossDomainMessenger), - abi.encodeCall( - l2CrossDomainMessenger.sendMessage, - ( - address(l1ERC721Bridge), - abi.encodeCall( - IL2ERC721Bridge.finalizeBridgeERC721, - (address(remoteToken), address(localToken), alice, alice, tokenId, hex"5678") - ), - 1234 - ) - ) - ); - - // Expect an event to be emitted. - vm.expectEmit(true, true, true, true); - emit ERC721BridgeInitiated(address(localToken), address(remoteToken), alice, alice, tokenId, hex"5678"); + _expectBridgeMessage(alice); + _expectBridgeInitiated(alice); - // Bridge the token. - vm.prank(alice, alice); - l2ERC721Bridge.bridgeERC721(address(localToken), address(remoteToken), tokenId, 1234, hex"5678"); + _bridgeToken(); - // Token is burned. - vm.expectRevert("ERC721: invalid token ID"); - localToken.ownerOf(tokenId); + _assertTokenBurned(); } /// @notice Tests that `bridgeERC721` reverts if the owner is not an EOA. function test_bridgeERC721_fromContract_reverts() external { - // Bridge the token. vm.etch(alice, hex"01"); vm.prank(alice); vm.expectRevert("ERC721Bridge: account is not externally owned"); - l2ERC721Bridge.bridgeERC721(address(localToken), address(remoteToken), tokenId, 1234, hex"5678"); + l2ERC721Bridge.bridgeERC721(address(localToken), remoteToken, tokenId, DEFAULT_MIN_GAS_LIMIT, EXTRA_DATA); - // Token is not locked in the bridge. - assertEq(localToken.ownerOf(tokenId), alice); + _assertTokenOwnedBy(alice); } /// @notice Tests that `bridgeERC721` reverts if the local token is the zero address. function test_bridgeERC721_localTokenZeroAddress_reverts() external { - // Bridge the token. vm.prank(alice, alice); vm.expectRevert(); // nosemgrep: sol-safety-expectrevert-no-args - l2ERC721Bridge.bridgeERC721(address(0), address(remoteToken), tokenId, 1234, hex"5678"); + l2ERC721Bridge.bridgeERC721(address(0), remoteToken, tokenId, DEFAULT_MIN_GAS_LIMIT, EXTRA_DATA); - // Token is not locked in the bridge. - assertEq(localToken.ownerOf(tokenId), alice); + _assertTokenOwnedBy(alice); } /// @notice Tests that `bridgeERC721` reverts if the remote token is the zero address. function test_bridgeERC721_remoteTokenZeroAddress_reverts() external { - // Bridge the token. vm.prank(alice, alice); vm.expectRevert("L2ERC721Bridge: remote token cannot be address(0)"); - l2ERC721Bridge.bridgeERC721(address(localToken), address(0), tokenId, 1234, hex"5678"); + l2ERC721Bridge.bridgeERC721(address(localToken), address(0), tokenId, DEFAULT_MIN_GAS_LIMIT, EXTRA_DATA); - // Token is not locked in the bridge. - assertEq(localToken.ownerOf(tokenId), alice); + _assertTokenOwnedBy(alice); } /// @notice Tests that `bridgeERC721` reverts if the caller is not the token owner. function test_bridgeERC721_wrongOwner_reverts() external { - // Bridge the token. vm.prank(bob, bob); vm.expectRevert("L2ERC721Bridge: Withdrawal is not being initiated by NFT owner"); - l2ERC721Bridge.bridgeERC721(address(localToken), address(remoteToken), tokenId, 1234, hex"5678"); + l2ERC721Bridge.bridgeERC721(address(localToken), remoteToken, tokenId, DEFAULT_MIN_GAS_LIMIT, EXTRA_DATA); - // Token is not locked in the bridge. - assertEq(localToken.ownerOf(tokenId), alice); + _assertTokenOwnedBy(alice); } /// @notice Tests that `bridgeERC721To` correctly bridges a token and burns it on the origin /// chain. function test_bridgeERC721To_succeeds() external { - // Expect a call to the messenger. - vm.expectCall( - address(l2CrossDomainMessenger), - abi.encodeCall( - l2CrossDomainMessenger.sendMessage, - ( - address(l1ERC721Bridge), - abi.encodeCall( - IL1ERC721Bridge.finalizeBridgeERC721, - (address(remoteToken), address(localToken), alice, bob, tokenId, hex"5678") - ), - 1234 - ) - ) - ); - - // Expect an event to be emitted. - vm.expectEmit(true, true, true, true); - emit ERC721BridgeInitiated(address(localToken), address(remoteToken), alice, bob, tokenId, hex"5678"); + _expectBridgeMessage(bob); + _expectBridgeInitiated(bob); - // Bridge the token. vm.prank(alice); - l2ERC721Bridge.bridgeERC721To(address(localToken), address(remoteToken), bob, tokenId, 1234, hex"5678"); + l2ERC721Bridge.bridgeERC721To(address(localToken), remoteToken, bob, tokenId, DEFAULT_MIN_GAS_LIMIT, EXTRA_DATA); - // Token is burned. - vm.expectRevert("ERC721: invalid token ID"); - localToken.ownerOf(tokenId); + _assertTokenBurned(); } /// @notice Tests that `bridgeERC721To` reverts if the local token is the zero address. function test_bridgeERC721To_localTokenZeroAddress_reverts() external { - // Bridge the token. vm.prank(alice); vm.expectRevert(); // nosemgrep: sol-safety-expectrevert-no-args - l2ERC721Bridge.bridgeERC721To(address(0), address(l1ERC721Bridge), bob, tokenId, 1234, hex"5678"); + l2ERC721Bridge.bridgeERC721To( + address(0), address(l1ERC721Bridge), bob, tokenId, DEFAULT_MIN_GAS_LIMIT, EXTRA_DATA + ); - // Token is not locked in the bridge. - assertEq(localToken.ownerOf(tokenId), alice); + _assertTokenOwnedBy(alice); } /// @notice Tests that `bridgeERC721To` reverts if the remote token is the zero address. function test_bridgeERC721To_remoteTokenZeroAddress_reverts() external { - // Bridge the token. vm.prank(alice); vm.expectRevert("L2ERC721Bridge: remote token cannot be address(0)"); - l2ERC721Bridge.bridgeERC721To(address(localToken), address(0), bob, tokenId, 1234, hex"5678"); + l2ERC721Bridge.bridgeERC721To(address(localToken), address(0), bob, tokenId, DEFAULT_MIN_GAS_LIMIT, EXTRA_DATA); - // Token is not locked in the bridge. - assertEq(localToken.ownerOf(tokenId), alice); + _assertTokenOwnedBy(alice); } /// @notice Tests that `bridgeERC721To` reverts if the caller is not the token owner. function test_bridgeERC721To_wrongOwner_reverts() external { - // Bridge the token. vm.prank(bob); vm.expectRevert("L2ERC721Bridge: Withdrawal is not being initiated by NFT owner"); - l2ERC721Bridge.bridgeERC721To(address(localToken), address(remoteToken), bob, tokenId, 1234, hex"5678"); + l2ERC721Bridge.bridgeERC721To(address(localToken), remoteToken, bob, tokenId, DEFAULT_MIN_GAS_LIMIT, EXTRA_DATA); - // Token is not locked in the bridge. - assertEq(localToken.ownerOf(tokenId), alice); + _assertTokenOwnedBy(alice); } /// @notice Tests that `bridgeERC721To` reverts if the to address is the zero address. function test_bridgeERC721To_toZeroAddress_reverts() external { - // Bridge the token. vm.prank(bob); vm.expectRevert("ERC721Bridge: nft recipient cannot be address(0)"); - l2ERC721Bridge.bridgeERC721To(address(localToken), address(remoteToken), address(0), tokenId, 1234, hex"5678"); + l2ERC721Bridge.bridgeERC721To( + address(localToken), remoteToken, address(0), tokenId, DEFAULT_MIN_GAS_LIMIT, EXTRA_DATA + ); } } diff --git a/test/L2/L2StandardBridge.t.sol b/test/L2/L2StandardBridge.t.sol index 03332e40..07244695 100644 --- a/test/L2/L2StandardBridge.t.sol +++ b/test/L2/L2StandardBridge.t.sol @@ -2,7 +2,6 @@ pragma solidity 0.8.15; // Testing -import { stdStorage, StdStorage } from "lib/forge-std/src/Test.sol"; import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; import { CommonTest } from "test/setup/CommonTest.sol"; @@ -26,8 +25,7 @@ import { IL2StandardBridge } from "interfaces/L2/IL2StandardBridge.sol"; /// @notice Reusable test initialization for `L2StandardBridge` tests. abstract contract L2StandardBridge_TestInit is CommonTest { /// @notice Sets up expected calls and emits for a successful ERC20 withdrawal. - function _preBridgeERC20(bool _isLegacy, address _l2Token) internal { - // Alice has 100 L2Token + function _preBridgeERC20(address _l2Token) internal { deal(_l2Token, alice, 100, true); assertEq(ERC20(_l2Token).balanceOf(alice), 100); uint256 nonce = l2CrossDomainMessenger.messageNonce(); @@ -49,17 +47,6 @@ abstract contract L2StandardBridge_TestInit is CommonTest { }) ); - if (_isLegacy) { - vm.expectCall( - address(l2StandardBridge), abi.encodeCall(l2StandardBridge.withdraw, (_l2Token, 100, 1000, hex"")) - ); - } else { - vm.expectCall( - address(l2StandardBridge), - abi.encodeCall(l2StandardBridge.bridgeERC20, (_l2Token, address(L1Token), 100, 1000, hex"")) - ); - } - vm.expectCall( address(l2CrossDomainMessenger), abi.encodeCall(ICrossDomainMessenger.sendMessage, (address(l1StandardBridge), message, 1000)) @@ -72,7 +59,6 @@ abstract contract L2StandardBridge_TestInit is CommonTest { ) ); - // The l2StandardBridge should burn the tokens vm.expectCall(_l2Token, abi.encodeCall(OptimismMintableERC20.burn, (alice, 100))); vm.expectEmit(true, true, true, true); @@ -107,9 +93,9 @@ abstract contract L2StandardBridge_TestInit is CommonTest { /// recipient. /// @dev `withdrawTo` and `bridgeERC20To` should behave the same when transferring ERC20 tokens /// so they should share the same setup and expectEmit calls - function _preBridgeERC20To(bool _isLegacy, address _l2Token) internal { + function _preBridgeERC20To(address _l2Token) internal { deal(_l2Token, alice, 100, true); - assertEq(L2Token.balanceOf(alice), 100); + assertEq(ERC20(_l2Token).balanceOf(alice), 100); uint256 nonce = l2CrossDomainMessenger.messageNonce(); bytes memory message = abi.encodeCall(IStandardBridge.finalizeBridgeERC20, (address(L1Token), _l2Token, alice, bob, 100, hex"")); @@ -154,18 +140,6 @@ abstract contract L2StandardBridge_TestInit is CommonTest { vm.expectEmit(address(l2CrossDomainMessenger)); emit SentMessageExtension1(address(l2StandardBridge), 0); - if (_isLegacy) { - vm.expectCall( - address(l2StandardBridge), - abi.encodeCall(l2StandardBridge.withdrawTo, (_l2Token, bob, 100, 1000, hex"")) - ); - } else { - vm.expectCall( - address(l2StandardBridge), - abi.encodeCall(l2StandardBridge.bridgeERC20To, (_l2Token, address(L1Token), bob, 100, 1000, hex"")) - ); - } - vm.expectCall( address(l2CrossDomainMessenger), abi.encodeCall(ICrossDomainMessenger.sendMessage, (address(l1StandardBridge), message, 1000)) @@ -178,13 +152,28 @@ abstract contract L2StandardBridge_TestInit is CommonTest { ) ); - // The l2StandardBridge should burn the tokens - vm.expectCall(address(L2Token), abi.encodeCall(OptimismMintableERC20.burn, (alice, 100))); + vm.expectCall(_l2Token, abi.encodeCall(OptimismMintableERC20.burn, (alice, 100))); vm.prank(alice, alice); } - using stdStorage for StdStorage; + function _mockXDomainMessageSender(address _sender) internal { + vm.mockCall( + address(l2StandardBridge.messenger()), + abi.encodeCall(ICrossDomainMessenger.xDomainMessageSender, ()), + abi.encode(_sender) + ); + } + + function _mockOtherBridge() internal { + _mockXDomainMessageSender(address(l2StandardBridge.OTHER_BRIDGE())); + } + + function _setupFinalizeBridgeETH() internal returns (address messenger) { + messenger = address(l2StandardBridge.messenger()); + _mockOtherBridge(); + vm.deal(messenger, 100); + } } /// @title L2StandardBridge_Version_Test @@ -348,7 +337,7 @@ contract L2StandardBridge_Withdraw_Test is L2StandardBridge_TestInit { /// @notice Tests that `withdraw` burns the tokens, emits `WithdrawalInitiated`, and initiates /// a withdrawal with `Withdrawer.initiateWithdrawal`. function test_withdraw_withdrawingERC20_succeeds() external { - _preBridgeERC20({ _isLegacy: true, _l2Token: address(L2Token) }); + _preBridgeERC20(address(L2Token)); l2StandardBridge.withdraw(address(L2Token), 100, 1000, hex""); assertEq(L2Token.balanceOf(alice), 0); @@ -378,7 +367,7 @@ contract L2StandardBridge_WithdrawTo_Test is L2StandardBridge_TestInit { /// @notice Tests that `withdrawTo` burns the tokens, emits `WithdrawalInitiated`, and /// initiates a withdrawal with `Withdrawer.initiateWithdrawal`. function test_withdrawTo_withdrawingERC20_succeeds() external { - _preBridgeERC20To({ _isLegacy: true, _l2Token: address(L2Token) }); + _preBridgeERC20To(address(L2Token)); l2StandardBridge.withdrawTo(address(L2Token), bob, 100, 1000, hex""); assertEq(L2Token.balanceOf(alice), 0); @@ -398,7 +387,7 @@ contract L2StandardBridge_Uncategorized_Test is L2StandardBridge_TestInit { /// @notice Tests that `bridgeERC20` burns the tokens, emits `WithdrawalInitiated`, and /// initiates a withdrawal with `Withdrawer.initiateWithdrawal`. function test_bridgeERC20_succeeds() external { - _preBridgeERC20({ _isLegacy: false, _l2Token: address(L2Token) }); + _preBridgeERC20(address(L2Token)); l2StandardBridge.bridgeERC20(address(L2Token), address(L1Token), 100, 1000, hex""); assertEq(L2Token.balanceOf(alice), 0); @@ -413,35 +402,25 @@ contract L2StandardBridge_Uncategorized_Test is L2StandardBridge_TestInit { /// @notice Tests that `bridgeERC20To` burns the tokens, emits `WithdrawalInitiated`, and /// initiates a withdrawal with `Withdrawer.initiateWithdrawal`. function test_bridgeERC20To_succeeds() external { - _preBridgeERC20To({ _isLegacy: false, _l2Token: address(L2Token) }); + _preBridgeERC20To(address(L2Token)); l2StandardBridge.bridgeERC20To(address(L2Token), address(L1Token), bob, 100, 1000, hex""); assertEq(L2Token.balanceOf(alice), 0); } /// @notice Tests that `finalizeBridgeETH` reverts if the recipient is the other bridge. function test_finalizeBridgeETH_sendToSelf_reverts() external { - vm.mockCall( - address(l2StandardBridge.messenger()), - abi.encodeCall(ICrossDomainMessenger.xDomainMessageSender, ()), - abi.encode(address(l2StandardBridge.OTHER_BRIDGE())) - ); - vm.deal(address(l2CrossDomainMessenger), 100); - vm.prank(address(l2CrossDomainMessenger)); + address messenger = _setupFinalizeBridgeETH(); + vm.prank(messenger); vm.expectRevert("StandardBridge: cannot send to self"); l2StandardBridge.finalizeBridgeETH{ value: 100 }(alice, address(l2StandardBridge), 100, hex""); } /// @notice Tests that `finalizeBridgeETH` reverts if the recipient is the messenger. function test_finalizeBridgeETH_sendToMessenger_reverts() external { - vm.mockCall( - address(l2StandardBridge.messenger()), - abi.encodeCall(ICrossDomainMessenger.xDomainMessageSender, ()), - abi.encode(address(l2StandardBridge.OTHER_BRIDGE())) - ); - vm.deal(address(l2CrossDomainMessenger), 100); - vm.prank(address(l2CrossDomainMessenger)); + address messenger = _setupFinalizeBridgeETH(); + vm.prank(messenger); vm.expectRevert("StandardBridge: cannot send to messenger"); - l2StandardBridge.finalizeBridgeETH{ value: 100 }(alice, address(l2CrossDomainMessenger), 100, hex""); + l2StandardBridge.finalizeBridgeETH{ value: 100 }(alice, messenger, 100, hex""); } /// @notice Tests that bridging ETH succeeds. @@ -451,10 +430,6 @@ contract L2StandardBridge_Uncategorized_Test is L2StandardBridge_TestInit { bytes memory message = abi.encodeCall(IStandardBridge.finalizeBridgeETH, (alice, alice, _value, _extraData)); - vm.expectCall( - address(l2StandardBridge), _value, abi.encodeCall(l2StandardBridge.bridgeETH, (_minGasLimit, _extraData)) - ); - vm.expectCall( address(l2CrossDomainMessenger), _value, @@ -483,16 +458,8 @@ contract L2StandardBridge_Uncategorized_Test is L2StandardBridge_TestInit { skipIfSysFeatureEnabled(Features.CUSTOM_GAS_TOKEN); uint256 nonce = l2CrossDomainMessenger.messageNonce(); - vm.expectCall( - address(l2StandardBridge), - _value, - abi.encodeCall(l1StandardBridge.bridgeETHTo, (bob, _minGasLimit, _extraData)) - ); - bytes memory message = abi.encodeCall(IStandardBridge.finalizeBridgeETH, (alice, bob, _value, _extraData)); - // the L2 bridge should call - // L2CrossDomainMessenger.sendMessage vm.expectCall( address(l2CrossDomainMessenger), abi.encodeCall(ICrossDomainMessenger.sendMessage, (address(l1StandardBridge), message, _minGasLimit)) @@ -509,7 +476,6 @@ contract L2StandardBridge_Uncategorized_Test is L2StandardBridge_TestInit { vm.expectEmit(address(l2CrossDomainMessenger)); emit SentMessageExtension1(address(l2StandardBridge), _value); - // deposit eth to bob vm.deal(alice, _value); vm.prank(alice, alice); @@ -518,13 +484,7 @@ contract L2StandardBridge_Uncategorized_Test is L2StandardBridge_TestInit { /// @notice Tests that `finalizeBridgeETH` succeeds. function test_finalizeBridgeETH_succeeds() external { - address messenger = address(l2StandardBridge.messenger()); - vm.mockCall( - messenger, - abi.encodeCall(ICrossDomainMessenger.xDomainMessageSender, ()), - abi.encode(address(l2StandardBridge.OTHER_BRIDGE())) - ); - vm.deal(messenger, 100); + address messenger = _setupFinalizeBridgeETH(); vm.prank(messenger); vm.expectEmit(true, true, true, true); @@ -541,11 +501,7 @@ contract L2StandardBridge_Uncategorized_Test is L2StandardBridge_TestInit { address messenger = address(l2StandardBridge.messenger()); address localToken = address(L2Token); address remoteToken = address(L1Token); - vm.mockCall( - messenger, - abi.encodeCall(ICrossDomainMessenger.xDomainMessageSender, ()), - abi.encode(address(l2StandardBridge.OTHER_BRIDGE())) - ); + _mockOtherBridge(); deal(localToken, messenger, 100, true); vm.prank(messenger); @@ -562,11 +518,7 @@ contract L2StandardBridge_Uncategorized_Test is L2StandardBridge_TestInit { address messenger = address(l2StandardBridge.messenger()); address localToken = address(L2Token); address remoteToken = address(BadL1Token); - vm.mockCall( - messenger, - abi.encodeCall(ICrossDomainMessenger.xDomainMessageSender, ()), - abi.encode(address(l2StandardBridge.OTHER_BRIDGE())) - ); + _mockOtherBridge(); deal(localToken, messenger, 100, true); vm.prank(messenger); diff --git a/test/L2/L2ToL1MessagePasser.t.sol b/test/L2/L2ToL1MessagePasser.t.sol index 5768b36f..ab55fe71 100644 --- a/test/L2/L2ToL1MessagePasser.t.sol +++ b/test/L2/L2ToL1MessagePasser.t.sol @@ -8,11 +8,56 @@ import { CommonTest } from "test/setup/CommonTest.sol"; import { Types } from "src/libraries/Types.sol"; import { Hashing } from "src/libraries/Hashing.sol"; import { Features } from "src/libraries/Features.sol"; +import { Encoding } from "src/libraries/Encoding.sol"; import { SemverComp } from "src/libraries/SemverComp.sol"; +/// @title L2ToL1MessagePasser_TestInit +/// @notice Reusable test initialization for `L2ToL1MessagePasser` tests. +abstract contract L2ToL1MessagePasser_TestInit is CommonTest { + uint256 internal constant RECEIVE_DEFAULT_GAS_LIMIT = 100_000; + + function _hashWithdrawal( + uint256 _nonce, + address _sender, + address _target, + uint256 _value, + uint256 _gasLimit, + bytes memory _data + ) + internal + pure + returns (bytes32) + { + return Hashing.hashWithdrawal( + Types.WithdrawalTransaction({ + nonce: _nonce, sender: _sender, target: _target, value: _value, gasLimit: _gasLimit, data: _data + }) + ); + } + + function _expectMessagePassed( + uint256 _nonce, + address _sender, + address _target, + uint256 _value, + uint256 _gasLimit, + bytes memory _data + ) + internal + returns (bytes32) + { + bytes32 withdrawalHash = _hashWithdrawal(_nonce, _sender, _target, _value, _gasLimit, _data); + + vm.expectEmit(address(l2ToL1MessagePasser)); + emit MessagePassed(_nonce, _sender, _target, _value, _gasLimit, _data, withdrawalHash); + + return withdrawalHash; + } +} + /// @title L2ToL1MessagePasser_Version_Test /// @notice Tests the `version` function of the `L2ToL1MessagePasser` contract. -contract L2ToL1MessagePasser_Version_Test is CommonTest { +contract L2ToL1MessagePasser_Version_Test is L2ToL1MessagePasser_TestInit { /// @notice Tests that the version follows valid semver format. function test_version_validFormat_succeeds() external view { SemverComp.parse(l2ToL1MessagePasser.version()); @@ -21,47 +66,32 @@ contract L2ToL1MessagePasser_Version_Test is CommonTest { /// @title L2ToL1MessagePasser_Receive_Test /// @notice Tests the `receive` function of the `L2ToL1MessagePasser` contract. -contract L2ToL1MessagePasser_Receive_Test is CommonTest { +contract L2ToL1MessagePasser_Receive_Test is L2ToL1MessagePasser_TestInit { /// @notice Tests that receive() initiates withdrawal with default gas limit. function testFuzz_receive_initiatesWithdrawal_succeeds(uint256 _value) external { skipIfSysFeatureEnabled(Features.CUSTOM_GAS_TOKEN); uint256 nonce = l2ToL1MessagePasser.messageNonce(); - - bytes32 withdrawalHash = Hashing.hashWithdrawal( - Types.WithdrawalTransaction({ - nonce: nonce, - sender: address(this), - target: address(this), - value: _value, - gasLimit: 100_000, // RECEIVE_DEFAULT_GAS_LIMIT - data: bytes("") - }) - ); - - vm.expectEmit(address(l2ToL1MessagePasser)); - emit MessagePassed(nonce, address(this), address(this), _value, 100_000, bytes(""), withdrawalHash); + bytes memory data = bytes(""); + bytes32 withdrawalHash = + _expectMessagePassed(nonce, address(this), address(this), _value, RECEIVE_DEFAULT_GAS_LIMIT, data); vm.deal(address(this), _value); - (bool success,) = address(l2ToL1MessagePasser).call{ value: _value }(""); + (bool success,) = address(l2ToL1MessagePasser).call{ value: _value }(data); assertTrue(success); - assertEq(l2ToL1MessagePasser.sentMessages(withdrawalHash), true); + assertTrue(l2ToL1MessagePasser.sentMessages(withdrawalHash)); assertEq(l2ToL1MessagePasser.messageNonce(), nonce + 1); } } /// @title L2ToL1MessagePasser_Burn_Test /// @notice Tests the `burn` function of the `L2ToL1MessagePasser` contract. -contract L2ToL1MessagePasser_Burn_Test is CommonTest { +contract L2ToL1MessagePasser_Burn_Test is L2ToL1MessagePasser_TestInit { /// @notice Tests that `burn` succeeds and destroys the ETH held in the contract. - function testFuzz_burn_succeeds(uint256 _value, address _target, uint256 _gasLimit, bytes memory _data) external { + function testFuzz_burn_succeeds(uint256 _value) external { skipIfSysFeatureEnabled(Features.CUSTOM_GAS_TOKEN); - vm.deal(address(this), _value); - - l2ToL1MessagePasser.initiateWithdrawal{ value: _value }({ - _target: _target, _gasLimit: _gasLimit, _data: _data - }); + vm.deal(address(l2ToL1MessagePasser), _value); assertEq(address(l2ToL1MessagePasser).balance, _value); @@ -69,14 +99,13 @@ contract L2ToL1MessagePasser_Burn_Test is CommonTest { emit WithdrawerBalanceBurnt(_value); l2ToL1MessagePasser.burn(); - // The Withdrawer should have no balance assertEq(address(l2ToL1MessagePasser).balance, 0); } } /// @title L2ToL1MessagePasser_InitiateWithdrawal_Test /// @notice Tests the `initiateWithdrawal` function of the `L2ToL1MessagePasser` contract. -contract L2ToL1MessagePasser_InitiateWithdrawal_Test is CommonTest { +contract L2ToL1MessagePasser_InitiateWithdrawal_Test is L2ToL1MessagePasser_TestInit { /// @notice Tests that `initiateWithdrawal` succeeds and correctly sets the state of the /// message passer for the withdrawal hash. function testFuzz_initiateWithdrawal_succeeds( @@ -93,25 +122,14 @@ contract L2ToL1MessagePasser_InitiateWithdrawal_Test is CommonTest { } uint256 nonce = l2ToL1MessagePasser.messageNonce(); - bytes32 withdrawalHash = Hashing.hashWithdrawal( - Types.WithdrawalTransaction({ - nonce: nonce, sender: _sender, target: _target, value: _value, gasLimit: _gasLimit, data: _data - }) - ); - - vm.expectEmit(address(l2ToL1MessagePasser)); - emit MessagePassed(nonce, _sender, _target, _value, _gasLimit, _data, withdrawalHash); + bytes32 withdrawalHash = _expectMessagePassed(nonce, _sender, _target, _value, _gasLimit, _data); vm.deal(_sender, _value); vm.prank(_sender); l2ToL1MessagePasser.initiateWithdrawal{ value: _value }(_target, _gasLimit, _data); - assertEq(l2ToL1MessagePasser.sentMessages(withdrawalHash), true); + assertTrue(l2ToL1MessagePasser.sentMessages(withdrawalHash)); assertEq(l2ToL1MessagePasser.messageNonce(), nonce + 1); - - bytes32 slot = keccak256(bytes.concat(withdrawalHash, bytes32(0))); - - assertEq(vm.load(address(l2ToL1MessagePasser), slot), bytes32(uint256(1))); } /// @notice Tests that `initiateWithdrawal` succeeds when called by a contract. @@ -124,26 +142,13 @@ contract L2ToL1MessagePasser_InitiateWithdrawal_Test is CommonTest { external { skipIfSysFeatureEnabled(Features.CUSTOM_GAS_TOKEN); - bytes32 withdrawalHash = Hashing.hashWithdrawal( - Types.WithdrawalTransaction({ - nonce: l2ToL1MessagePasser.messageNonce(), - sender: address(this), - target: _target, - value: _value, - gasLimit: _gasLimit, - data: _data - }) - ); - - vm.expectEmit(address(l2ToL1MessagePasser)); - emit MessagePassed( - l2ToL1MessagePasser.messageNonce(), address(this), _target, _value, _gasLimit, _data, withdrawalHash - ); + uint256 nonce = l2ToL1MessagePasser.messageNonce(); + bytes32 withdrawalHash = _expectMessagePassed(nonce, address(this), _target, _value, _gasLimit, _data); vm.deal(address(this), _value); l2ToL1MessagePasser.initiateWithdrawal{ value: _value }(_target, _gasLimit, _data); - assertEq(l2ToL1MessagePasser.sentMessages(withdrawalHash), true); + assertTrue(l2ToL1MessagePasser.sentMessages(withdrawalHash)); } /// @notice Tests that `initiateWithdrawal` succeeds when called by an EOA. @@ -157,40 +162,25 @@ contract L2ToL1MessagePasser_InitiateWithdrawal_Test is CommonTest { { skipIfSysFeatureEnabled(Features.CUSTOM_GAS_TOKEN); uint256 nonce = l2ToL1MessagePasser.messageNonce(); - - // Verify caller is an EOA (alice has no code) - assertEq(alice.code.length, 0); - - // EOA emulation - vm.prank(alice, alice); vm.deal(alice, _value); - bytes32 withdrawalHash = - Hashing.hashWithdrawal(Types.WithdrawalTransaction(nonce, alice, _target, _value, _gasLimit, _data)); - - vm.expectEmit(address(l2ToL1MessagePasser)); - emit MessagePassed(nonce, alice, _target, _value, _gasLimit, _data, withdrawalHash); + bytes32 withdrawalHash = _expectMessagePassed(nonce, alice, _target, _value, _gasLimit, _data); + vm.prank(alice, alice); l2ToL1MessagePasser.initiateWithdrawal{ value: _value }({ _target: _target, _gasLimit: _gasLimit, _data: _data }); - // the sent messages mapping is filled - assertEq(l2ToL1MessagePasser.sentMessages(withdrawalHash), true); - // the nonce increments - assertEq(nonce + 1, l2ToL1MessagePasser.messageNonce()); + assertTrue(l2ToL1MessagePasser.sentMessages(withdrawalHash)); + assertEq(l2ToL1MessagePasser.messageNonce(), nonce + 1); } } /// @title L2ToL1MessagePasser_MessageNonce_Test /// @notice Tests the `messageNonce` function of the `L2ToL1MessagePasser` contract. -contract L2ToL1MessagePasser_MessageNonce_Test is CommonTest { +contract L2ToL1MessagePasser_MessageNonce_Test is L2ToL1MessagePasser_TestInit { /// @notice Tests that messageNonce encodes version in upper bytes. function test_messageNonce_encodesVersion_succeeds() external view { - uint256 nonce = l2ToL1MessagePasser.messageNonce(); - - // MESSAGE_VERSION is 1, should be in upper 2 bytes - // Version is stored in bits 240-255 (upper 2 bytes of uint256) - uint256 version = nonce >> 240; - assertEq(version, 1); + (, uint16 version) = Encoding.decodeVersionedNonce(l2ToL1MessagePasser.messageNonce()); + assertEq(version, l2ToL1MessagePasser.MESSAGE_VERSION()); } } diff --git a/test/L2/OperatorFeeVault.t.sol b/test/L2/OperatorFeeVault.t.sol index 9f09f567..c672a951 100644 --- a/test/L2/OperatorFeeVault.t.sol +++ b/test/L2/OperatorFeeVault.t.sol @@ -1,34 +1,24 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -// Interfaces import { IFeeVault } from "interfaces/L2/IFeeVault.sol"; - -// Libraries import { Predeploys } from "src/libraries/Predeploys.sol"; import { FeeVault_Uncategorized_Test } from "test/L2/FeeVault.t.sol"; import { Types } from "src/libraries/Types.sol"; import { SemverComp } from "src/libraries/SemverComp.sol"; import { CommonTest } from "test/setup/CommonTest.sol"; -/// @title OperatorFeeVault_Uncategorized_Test -/// @notice Test contract for the OperatorFeeVault contract's functionality contract OperatorFeeVault_Uncategorized_Test is FeeVault_Uncategorized_Test { - /// @dev Sets up the test suite. - function setUp() public virtual override { + function setUp() public override { super.setUp(); recipient = deploy.cfg().operatorFeeVaultRecipient(); - feeVaultName = "OperatorFeeVault"; minWithdrawalAmount = deploy.cfg().operatorFeeVaultMinimumWithdrawalAmount(); feeVault = IFeeVault(payable(Predeploys.OPERATOR_FEE_VAULT)); withdrawalNetwork = Types.WithdrawalNetwork(uint8(deploy.cfg().operatorFeeVaultWithdrawalNetwork())); } } -/// @title OperatorFeeVault_Version_Test -/// @notice Tests the `version` function of the `OperatorFeeVault` contract. contract OperatorFeeVault_Version_Test is CommonTest { - /// @notice Tests that version returns a valid semver string. function test_version_validFormat_succeeds() external view { SemverComp.parse(operatorFeeVault.version()); } diff --git a/test/L2/OptimismMintableERC721.t.sol b/test/L2/OptimismMintableERC721.t.sol index cc5ffe53..0783be4a 100644 --- a/test/L2/OptimismMintableERC721.t.sol +++ b/test/L2/OptimismMintableERC721.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { ERC721, IERC721 } from "lib/openzeppelin-contracts/contracts/token/ERC721/ERC721.sol"; +import { IERC721 } from "lib/openzeppelin-contracts/contracts/token/ERC721/ERC721.sol"; import { IERC721Enumerable } from "lib/openzeppelin-contracts/contracts/token/ERC721/extensions/ERC721Enumerable.sol"; import { IERC165 } from "lib/openzeppelin-contracts/contracts/utils/introspection/IERC165.sol"; import { Strings } from "lib/openzeppelin-contracts/contracts/utils/Strings.sol"; @@ -11,7 +11,10 @@ import { OptimismMintableERC721, IOptimismMintableERC721 } from "src/L2/Optimism /// @title OptimismMintableERC721_TestInit /// @notice Reusable test initialization for `OptimismMintableERC721` tests. abstract contract OptimismMintableERC721_TestInit is CommonTest { - ERC721 internal L1NFT; + uint256 internal constant remoteChainId = 1; + uint256 internal constant tokenId = 1; + + address internal L1NFT; OptimismMintableERC721 internal L2NFT; event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); @@ -23,14 +26,17 @@ abstract contract OptimismMintableERC721_TestInit is CommonTest { function setUp() public override { super.setUp(); - // Set up the token pair. - L1NFT = new ERC721("L1NFT", "L1T"); - L2NFT = new OptimismMintableERC721(address(l2ERC721Bridge), 1, address(L1NFT), "L2NFT", "L2T"); + L1NFT = makeAddr("L1NFT"); + L2NFT = new OptimismMintableERC721(address(l2ERC721Bridge), remoteChainId, L1NFT, "L2NFT", "L2T"); - // Label the addresses for nice traces. - vm.label(address(L1NFT), "L1ERC721Token"); + vm.label(L1NFT, "L1ERC721Token"); vm.label(address(L2NFT), "L2ERC721Token"); } + + function _bridgeMint(address _to) internal { + vm.prank(address(l2ERC721Bridge)); + L2NFT.safeMint(_to, tokenId); + } } /// @title OptimismMintableERC721_Constructor_Test @@ -40,30 +46,30 @@ contract OptimismMintableERC721_Constructor_Test is OptimismMintableERC721_TestI function test_constructor_succeeds() external view { assertEq(L2NFT.name(), "L2NFT"); assertEq(L2NFT.symbol(), "L2T"); - assertEq(L2NFT.remoteToken(), address(L1NFT)); + assertEq(L2NFT.remoteToken(), L1NFT); assertEq(L2NFT.bridge(), address(l2ERC721Bridge)); - assertEq(L2NFT.remoteChainId(), 1); - assertEq(L2NFT.REMOTE_TOKEN(), address(L1NFT)); + assertEq(L2NFT.remoteChainId(), remoteChainId); + assertEq(L2NFT.REMOTE_TOKEN(), L1NFT); assertEq(L2NFT.BRIDGE(), address(l2ERC721Bridge)); - assertEq(L2NFT.REMOTE_CHAIN_ID(), 1); + assertEq(L2NFT.REMOTE_CHAIN_ID(), remoteChainId); } /// @notice Tests that the constructor reverts when the bridge address is zero. function test_constructor_bridgeAsAddress0_reverts() external { vm.expectRevert("OptimismMintableERC721: bridge cannot be address(0)"); - L2NFT = new OptimismMintableERC721(address(0), 1, address(L1NFT), "L2NFT", "L2T"); + L2NFT = new OptimismMintableERC721(address(0), remoteChainId, L1NFT, "L2NFT", "L2T"); } /// @notice Tests that the constructor reverts when the remote chain ID is zero. function test_constructor_remoteChainId0_reverts() external { vm.expectRevert("OptimismMintableERC721: remote chain id cannot be zero"); - L2NFT = new OptimismMintableERC721(address(l2ERC721Bridge), 0, address(L1NFT), "L2NFT", "L2T"); + L2NFT = new OptimismMintableERC721(address(l2ERC721Bridge), 0, L1NFT, "L2NFT", "L2T"); } /// @notice Tests that the constructor reverts when the remote token address is zero. function test_constructor_remoteTokenAsAddress0_reverts() external { vm.expectRevert("OptimismMintableERC721: remote token cannot be address(0)"); - L2NFT = new OptimismMintableERC721(address(l2ERC721Bridge), 1, address(0), "L2NFT", "L2T"); + L2NFT = new OptimismMintableERC721(address(l2ERC721Bridge), remoteChainId, address(0), "L2NFT", "L2T"); } } @@ -73,28 +79,22 @@ contract OptimismMintableERC721_SafeMint_Test is OptimismMintableERC721_TestInit /// @notice Tests that the `safeMint` function successfully mints a token when called by the /// bridge. function test_safeMint_succeeds() external { - // Expect a transfer event. - vm.expectEmit(true, true, true, true); - emit Transfer(address(0), alice, 1); + vm.expectEmit(address(L2NFT)); + emit Transfer(address(0), alice, tokenId); - // Expect a mint event. - vm.expectEmit(true, true, true, true); - emit Mint(alice, 1); + vm.expectEmit(address(L2NFT)); + emit Mint(alice, tokenId); - // Mint the token. - vm.prank(address(l2ERC721Bridge)); - L2NFT.safeMint(alice, 1); + _bridgeMint(alice); - // Token should be owned by alice. - assertEq(L2NFT.ownerOf(1), alice); + assertEq(L2NFT.ownerOf(tokenId), alice); } /// @notice Tests that the `safeMint` function reverts when called by an address other than the bridge. function test_safeMint_notBridge_reverts() external { - // Try to mint the token. vm.expectRevert("OptimismMintableERC721: only bridge can call this function"); vm.prank(address(alice)); - L2NFT.safeMint(alice, 1); + L2NFT.safeMint(alice, tokenId); } } @@ -104,38 +104,29 @@ contract OptimismMintableERC721_Burn_Test is OptimismMintableERC721_TestInit { /// @notice Tests that the `burn` function successfully burns a token when called by the /// bridge. function test_burn_succeeds() external { - // Mint the token first. - vm.prank(address(l2ERC721Bridge)); - L2NFT.safeMint(alice, 1); + _bridgeMint(alice); - // Expect a transfer event. - vm.expectEmit(true, true, true, true); - emit Transfer(alice, address(0), 1); + vm.expectEmit(address(L2NFT)); + emit Transfer(alice, address(0), tokenId); - // Expect a burn event. - vm.expectEmit(true, true, true, true); - emit Burn(alice, 1); + vm.expectEmit(address(L2NFT)); + emit Burn(alice, tokenId); - // Burn the token. vm.prank(address(l2ERC721Bridge)); - L2NFT.burn(alice, 1); + L2NFT.burn(alice, tokenId); - // Token should be owned by address(0). vm.expectRevert("ERC721: invalid token ID"); - L2NFT.ownerOf(1); + L2NFT.ownerOf(tokenId); } /// @notice Tests that the `burn` function reverts when called by an address other than the /// bridge. function test_burn_notBridge_reverts() external { - // Mint the token first. - vm.prank(address(l2ERC721Bridge)); - L2NFT.safeMint(alice, 1); + _bridgeMint(alice); - // Try to burn the token. vm.expectRevert("OptimismMintableERC721: only bridge can call this function"); vm.prank(address(alice)); - L2NFT.burn(alice, 1); + L2NFT.burn(alice, tokenId); } } @@ -145,13 +136,9 @@ contract OptimismMintableERC721_SupportsInterface_Test is OptimismMintableERC721 /// @notice Tests that the `supportsInterface` function returns true for /// IOptimismMintableERC721, IERC721Enumerable, IERC721 and IERC165 interfaces. function test_supportsInterface_succeeds() external view { - // Checks if the contract supports the IOptimismMintableERC721 interface. assertTrue(L2NFT.supportsInterface(type(IOptimismMintableERC721).interfaceId)); - // Checks if the contract supports the IERC721Enumerable interface. assertTrue(L2NFT.supportsInterface(type(IERC721Enumerable).interfaceId)); - // Checks if the contract supports the IERC721 interface. assertTrue(L2NFT.supportsInterface(type(IERC721).interfaceId)); - // Checks if the contract supports the IERC165 interface. assertTrue(L2NFT.supportsInterface(type(IERC165).interfaceId)); } } @@ -162,22 +149,17 @@ contract OptimismMintableERC721_SupportsInterface_Test is OptimismMintableERC721 contract OptimismMintableERC721_Uncategorized_Test is OptimismMintableERC721_TestInit { /// @notice Tests that the `tokenURI` function returns the correct URI for a minted token. function test_tokenURI_succeeds() external { - // Mint the token first. - vm.prank(address(l2ERC721Bridge)); - L2NFT.safeMint(alice, 1); + _bridgeMint(alice); - // Token URI should be correct. assertEq( - L2NFT.tokenURI(1), - string( - abi.encodePacked( - "ethereum:", - Strings.toHexString(uint160(address(L1NFT)), 20), - "@", - Strings.toString(1), - "/tokenURI?uint256=", - Strings.toString(1) - ) + L2NFT.tokenURI(tokenId), + string.concat( + "ethereum:", + Strings.toHexString(uint160(L1NFT), 20), + "@", + Strings.toString(remoteChainId), + "/tokenURI?uint256=", + Strings.toString(tokenId) ) ); } diff --git a/test/L2/OptimismMintableERC721Factory.t.sol b/test/L2/OptimismMintableERC721Factory.t.sol index 8d45a34c..b11cb483 100644 --- a/test/L2/OptimismMintableERC721Factory.t.sol +++ b/test/L2/OptimismMintableERC721Factory.t.sol @@ -7,9 +7,14 @@ import { OptimismMintableERC721 } from "src/L2/OptimismMintableERC721.sol"; /// @title OptimismMintableERC721Factory_TestInit /// @notice Reusable test initialization for `OptimismMintableERC721Factory` tests. abstract contract OptimismMintableERC721Factory_TestInit is CommonTest { + address internal constant REMOTE_TOKEN = address(1234); + string internal constant TOKEN_NAME = "L2Token"; + string internal constant TOKEN_SYMBOL = "L2T"; + event OptimismMintableERC721Created(address indexed localToken, address indexed remoteToken, address deployer); - function calculateTokenAddress( + /// @notice Precalculates the address of the token contract. + function _calculateTokenAddress( address _remote, string memory _name, string memory _symbol @@ -47,29 +52,22 @@ contract OptimismMintableERC721Factory_Constructor_Test is OptimismMintableERC72 contract OptimismMintableERC721Factory_CreateOptimismMintableERC721_Test is OptimismMintableERC721Factory_TestInit { /// @notice Tests that the `createOptimismMintableERC721` function succeeds. function test_createOptimismMintableERC721_succeeds() external { - address remote = address(1234); - address local = calculateTokenAddress(address(1234), "L2Token", "L2T"); + address local = _calculateTokenAddress(REMOTE_TOKEN, TOKEN_NAME, TOKEN_SYMBOL); - // Expect a token creation event. vm.expectEmit(address(l2OptimismMintableERC721Factory)); - emit OptimismMintableERC721Created(local, remote, alice); + emit OptimismMintableERC721Created(local, REMOTE_TOKEN, alice); - // Create the token. vm.prank(alice); OptimismMintableERC721 created = OptimismMintableERC721( - l2OptimismMintableERC721Factory.createOptimismMintableERC721(remote, "L2Token", "L2T") + l2OptimismMintableERC721Factory.createOptimismMintableERC721(REMOTE_TOKEN, TOKEN_NAME, TOKEN_SYMBOL) ); - // Token address should be correct. assertEq(address(created), local); - - // Should be marked as created by the factory. assertTrue(l2OptimismMintableERC721Factory.isOptimismMintableERC721(address(created))); - // Token should've been constructed correctly. - assertEq(created.name(), "L2Token"); - assertEq(created.symbol(), "L2T"); - assertEq(created.REMOTE_TOKEN(), remote); + assertEq(created.name(), TOKEN_NAME); + assertEq(created.symbol(), TOKEN_SYMBOL); + assertEq(created.REMOTE_TOKEN(), REMOTE_TOKEN); assertEq(created.BRIDGE(), address(l2ERC721Bridge)); assertEq(created.REMOTE_CHAIN_ID(), deploy.cfg().l1ChainId()); } @@ -77,22 +75,19 @@ contract OptimismMintableERC721Factory_CreateOptimismMintableERC721_Test is Opti /// @notice Tests that the `createOptimismMintableERC721` function reverts if the same token is /// created twice. function test_createOptimismMintableERC721_sameTwice_reverts() external { - address remote = address(1234); - vm.prank(alice); - l2OptimismMintableERC721Factory.createOptimismMintableERC721(remote, "L2Token", "L2T"); + l2OptimismMintableERC721Factory.createOptimismMintableERC721(REMOTE_TOKEN, TOKEN_NAME, TOKEN_SYMBOL); vm.expectRevert(); // nosemgrep: sol-safety-expectrevert-no-args vm.prank(alice); - l2OptimismMintableERC721Factory.createOptimismMintableERC721(remote, "L2Token", "L2T"); + l2OptimismMintableERC721Factory.createOptimismMintableERC721(REMOTE_TOKEN, TOKEN_NAME, TOKEN_SYMBOL); } /// @notice Tests that the `createOptimismMintableERC721` function reverts if the remote token /// address is zero. function test_createOptimismMintableERC721_zeroRemoteToken_reverts() external { - // Try to create a token with a zero remote token address. vm.expectRevert("OptimismMintableERC721Factory: L1 token address cannot be address(0)"); - l2OptimismMintableERC721Factory.createOptimismMintableERC721(address(0), "L2Token", "L2T"); + l2OptimismMintableERC721Factory.createOptimismMintableERC721(address(0), TOKEN_NAME, TOKEN_SYMBOL); } } diff --git a/test/L2/SequencerFeeVault.t.sol b/test/L2/SequencerFeeVault.t.sol index 5ef59377..307b59ed 100644 --- a/test/L2/SequencerFeeVault.t.sol +++ b/test/L2/SequencerFeeVault.t.sol @@ -1,29 +1,22 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -// Interfaces import { IFeeVault } from "interfaces/L2/IFeeVault.sol"; -import { ISequencerFeeVault } from "interfaces/L2/ISequencerFeeVault.sol"; -// Libraries import { Predeploys } from "src/libraries/Predeploys.sol"; import { FeeVault_Uncategorized_Test } from "test/L2/FeeVault.t.sol"; import { Types } from "src/libraries/Types.sol"; -/// @title SequencerFeeVault_Uncategorized_Test -/// @notice Test contract for the SequencerFeeVault contract's functionality contract SequencerFeeVault_Uncategorized_Test is FeeVault_Uncategorized_Test { - /// @dev Sets up the test suite. function setUp() public virtual override { super.setUp(); recipient = deploy.cfg().sequencerFeeVaultRecipient(); - feeVaultName = "SequencerFeeVault"; minWithdrawalAmount = deploy.cfg().sequencerFeeVaultMinimumWithdrawalAmount(); feeVault = IFeeVault(payable(Predeploys.SEQUENCER_FEE_WALLET)); - withdrawalNetwork = Types.WithdrawalNetwork(uint8(deploy.cfg().sequencerFeeVaultWithdrawalNetwork())); + withdrawalNetwork = Types.WithdrawalNetwork(deploy.cfg().sequencerFeeVaultWithdrawalNetwork()); } - function test_constructor_l1FeeWallet_succeeds() external view { - assertEq(ISequencerFeeVault(payable(address(feeVault))).l1FeeWallet(), recipient); + function test_l1FeeWallet_matchesRecipient_succeeds() external view { + assertEq(sequencerFeeVault.l1FeeWallet(), recipient); } } diff --git a/test/L2/WETH.t.sol b/test/L2/WETH.t.sol index 12581b33..fb86c95e 100644 --- a/test/L2/WETH.t.sol +++ b/test/L2/WETH.t.sol @@ -1,54 +1,40 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -// Testing utilities import { CommonTest } from "test/setup/CommonTest.sol"; - -// Libraries import { SemverComp } from "src/libraries/SemverComp.sol"; - -// Interfaces import { ISemver } from "interfaces/universal/ISemver.sol"; -/// @title WETH_Version_Test -/// @notice Tests the `version` function of the `WETH` contract. contract WETH_Version_Test is CommonTest { - /// @notice Tests that the version returns a valid semver string. - function test_version_succeeds() external view { + function test_version_validFormat_succeeds() external view { SemverComp.parse(ISemver(address(weth)).version()); } } -/// @title WETH_Name_Test -/// @notice Tests the `name` function of the `WETH` contract. contract WETH_Name_Test is CommonTest { - /// @notice Tests that the `name` function returns the correct value. function testFuzz_name_succeeds(string memory _gasPayingTokenName) external { + vm.assume(bytes(_gasPayingTokenName).length <= 128); vm.mockCall(address(l1Block), abi.encodeCall(l1Block.gasPayingTokenName, ()), abi.encode(_gasPayingTokenName)); - assertEq(string.concat("Wrapped ", _gasPayingTokenName), weth.name()); + assertEq(weth.name(), string.concat("Wrapped ", _gasPayingTokenName)); } - /// @notice Tests that the `name` function returns 'Wrapped Ether' by default. - function test_name_ether_succeeds() external view { - assertEq(string.concat("Wrapped ", l1Block.gasPayingTokenName()), weth.name()); + function test_name_defaultGasPayingToken_succeeds() external view { + assertEq(weth.name(), string.concat("Wrapped ", l1Block.gasPayingTokenName())); } } -/// @title WETH_Symbol_Test -/// @notice Tests the `symbol` function of the `WETH` contract. contract WETH_Symbol_Test is CommonTest { - /// @notice Tests that the `symbol` function returns the correct value. function testFuzz_symbol_succeeds(string memory _gasPayingTokenSymbol) external { + vm.assume(bytes(_gasPayingTokenSymbol).length <= 128); vm.mockCall( address(l1Block), abi.encodeCall(l1Block.gasPayingTokenSymbol, ()), abi.encode(_gasPayingTokenSymbol) ); - assertEq(string.concat("W", _gasPayingTokenSymbol), weth.symbol()); + assertEq(weth.symbol(), string.concat("W", _gasPayingTokenSymbol)); } - /// @notice Tests that the `symbol` function returns 'WETH' by default. - function test_symbol_ether_succeeds() external view { - assertEq(string.concat("W", l1Block.gasPayingTokenSymbol()), weth.symbol()); + function test_symbol_defaultGasPayingToken_succeeds() external view { + assertEq(weth.symbol(), string.concat("W", l1Block.gasPayingTokenSymbol())); } } diff --git a/test/deploy/SystemDeploy.t.sol b/test/deploy/SystemDeploy.t.sol index f45b456d..6a94439d 100644 --- a/test/deploy/SystemDeploy.t.sol +++ b/test/deploy/SystemDeploy.t.sol @@ -8,16 +8,12 @@ import { SystemDeploy } from "scripts/deploy/SystemDeploy.s.sol"; import { Types } from "scripts/libraries/Types.sol"; import { SystemDeployAssertions } from "test/deploy/SystemDeployAssertions.sol"; -import { IDisputeGame } from "interfaces/L1/proofs/IDisputeGame.sol"; import { ISP1Verifier } from "interfaces/L1/proofs/zk/ISP1Verifier.sol"; -import { IProxy } from "interfaces/universal/IProxy.sol"; -import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; -import { AggregateVerifier } from "src/L1/proofs/AggregateVerifier.sol"; import { TEEProverRegistry } from "src/L1/proofs/tee/TEEProverRegistry.sol"; import { TEEVerifier } from "src/L1/proofs/tee/TEEVerifier.sol"; import { ZKVerifier } from "src/L1/proofs/zk/ZKVerifier.sol"; import { GameType, Hash, Proposal } from "src/libraries/bridge/Types.sol"; -import { Claim } from "src/libraries/bridge/LibUDT.sol"; +import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; contract MockNitroEnclaveVerifier { address public proofSubmitter; @@ -47,7 +43,6 @@ contract SystemDeploy_Test is Test, SystemDeployAssertions { MockSP1Verifier internal sp1Verifier; uint256 internal l2ChainId = 901; - Claim internal absolutePrestate = Claim.wrap(0x038512e02c4c3f7bdaec27d00edf55b7155e0905301e1a88083e4e0a6764d54c); function setUp() public { systemDeploy = new SystemDeploy(); @@ -76,32 +71,28 @@ contract SystemDeploy_Test is Test, SystemDeployAssertions { assertEq(output.superchainProxyAdmin.owner(), _superchainProxyAdminOwner, "proxy admin owner"); assertEq(output.superchainConfigProxy.guardian(), _guardian, "proxy guardian"); assertEq(output.superchainConfigImpl.guardian(), _guardian, "impl guardian"); + assertEq(output.superchainConfigProxy.incidentResponder(), _incidentResponder, "proxy incident responder"); + assertEq(output.superchainConfigImpl.incidentResponder(), _incidentResponder, "impl incident responder"); - vm.startPrank(address(0)); assertEq( - IProxy(payable(address(output.superchainConfigProxy))).implementation(), + EIP1967Helper.getImplementation(address(output.superchainConfigProxy)), address(output.superchainConfigImpl), "implementation" ); assertEq( - IProxy(payable(address(output.superchainConfigProxy))).admin(), - address(output.superchainProxyAdmin), - "admin" + EIP1967Helper.getAdmin(address(output.superchainConfigProxy)), address(output.superchainProxyAdmin), "admin" ); - vm.stopPrank(); } function test_deploySuperchain_nullInput_reverts() public { SystemDeploy.SuperchainInput memory input = SystemDeploy.SuperchainInput({ - guardian: guardian, incidentResponder: address(0), superchainProxyAdminOwner: owner + guardian: guardian, incidentResponder: incidentResponder, superchainProxyAdminOwner: address(0) }); - - input.superchainProxyAdminOwner = address(0); vm.expectRevert(abi.encodeWithSelector(SystemDeploy.InvalidRoleAddress.selector, "superchainProxyAdminOwner")); systemDeploy.deploySuperchain(input); input = SystemDeploy.SuperchainInput({ - guardian: address(0), incidentResponder: address(0), superchainProxyAdminOwner: owner + guardian: address(0), incidentResponder: incidentResponder, superchainProxyAdminOwner: owner }); vm.expectRevert(abi.encodeWithSelector(SystemDeploy.InvalidRoleAddress.selector, "guardian")); systemDeploy.deploySuperchain(input); @@ -109,7 +100,7 @@ contract SystemDeploy_Test is Test, SystemDeployAssertions { function test_deploySuperchain_reuseAddresses_succeeds() public { SystemDeploy.SuperchainInput memory input = SystemDeploy.SuperchainInput({ - guardian: guardian, incidentResponder: address(0), superchainProxyAdminOwner: owner + guardian: guardian, incidentResponder: incidentResponder, superchainProxyAdminOwner: owner }); SystemDeploy.SuperchainOutput memory output0 = systemDeploy.deploySuperchain(input); @@ -141,7 +132,8 @@ contract SystemDeploy_Test is Test, SystemDeployAssertions { } function test_upgrade_withoutManagerDelegatecall_succeeds() public { - SystemDeploy.DeployOutput memory output = systemDeploy.deploy(_defaultDeployInput()); + SystemDeploy.DeployInput memory input = _defaultDeployInput(); + SystemDeploy.DeployOutput memory output = systemDeploy.deploy(input); SystemDeploy.UpgradeOutput memory upgradeOutput = systemDeploy.upgrade( SystemDeploy.UpgradeInput({ @@ -154,14 +146,19 @@ contract SystemDeploy_Test is Test, SystemDeployAssertions { assertFalse(upgradeOutput.superchainConfigUpgraded, "superchain already current"); assertTrue(upgradeOutput.chainUpgraded, "chain upgraded"); - _assertUpgradedProxyImplementations(output); - assertValidStandardSystem(_expected(output, _defaultDeployInput())); + assertEq( + output.superchain.superchainProxyAdmin + .getProxyImplementation(address(output.superchain.superchainConfigProxy)), + output.impls.superchainConfigImpl, + "superchain config impl" + ); + assertValidStandardSystem(_expected(output, input)); } function test_deploy_reusingImplementations_doesNotSaveZeroImplementationOnlyArtifacts() public { - SystemDeploy.DeployOutput memory output = systemDeploy.deploy(_defaultDeployInput()); - SystemDeploy.DeployInput memory input = _defaultDeployInput(); + SystemDeploy.DeployOutput memory output = systemDeploy.deploy(input); + input.saveArtifacts = true; input.superchainConfigProxy = output.superchain.superchainConfigProxy; input.implementations = output.impls; @@ -224,51 +221,28 @@ contract SystemDeploy_Test is Test, SystemDeployAssertions { internal view { - GameType gameType = GameType.wrap(uint32(_input.implementationsInput.multiproofGameType)); - address aggregateVerifierAddr = address(_output.opChain.aggregateVerifier); address teeProverRegistryProxyAddr = address(_output.opChain.teeProverRegistryProxy); address teeVerifierAddr = address(_output.opChain.teeVerifier); address zkVerifierAddr = address(_output.opChain.zkVerifier); Types.Implementations memory impls = _output.impls; - assertNotEq(aggregateVerifierAddr, address(0), "aggregate verifier"); assertNotEq(teeProverRegistryProxyAddr, address(0), "tee prover registry proxy"); assertNotEq(impls.teeProverRegistryImpl, address(0), "tee prover registry impl"); - assertNotEq(teeVerifierAddr, address(0), "tee verifier"); - assertNotEq(zkVerifierAddr, address(0), "zk verifier"); - assertEq(impls.aggregateVerifierImpl, aggregateVerifierAddr, "aggregate verifier impl"); + assertEq(impls.aggregateVerifierImpl, address(_output.opChain.aggregateVerifier), "aggregate verifier impl"); assertEq(impls.teeVerifierImpl, teeVerifierAddr, "tee verifier impl"); assertEq(impls.zkVerifierImpl, zkVerifierAddr, "zk verifier impl"); - assertEq(address(_output.opChain.nitroEnclaveVerifier), _input.implementationsInput.nitroEnclaveVerifier); - assertEq(address(_output.opChain.sp1Verifier), address(_input.implementationsInput.sp1Verifier)); + assertEq( + address(_output.opChain.nitroEnclaveVerifier), + _input.implementationsInput.nitroEnclaveVerifier, + "nitro enclave verifier" + ); + assertEq(address(_output.opChain.sp1Verifier), address(_input.implementationsInput.sp1Verifier), "sp1 verifier"); assertEq( _output.opChain.opChainProxyAdmin.getProxyImplementation(teeProverRegistryProxyAddr), impls.teeProverRegistryImpl, "tee registry proxy impl" ); - assertEq( - address(_output.opChain.disputeGameFactoryProxy.gameImpls(gameType)), - aggregateVerifierAddr, - "multiproof game impl" - ); - - AggregateVerifier aggregateVerifier = AggregateVerifier(aggregateVerifierAddr); - assertEq( - address(aggregateVerifier.anchorStateRegistry()), - address(_output.opChain.anchorStateRegistryProxy), - "aggregate verifier asr" - ); - assertEq(address(aggregateVerifier.DISPUTE_GAME_FACTORY()), address(_output.opChain.disputeGameFactoryProxy)); - assertEq(address(aggregateVerifier.DELAYED_WETH()), address(_output.opChain.delayedWETHProxy)); - assertEq(address(aggregateVerifier.TEE_VERIFIER()), teeVerifierAddr); - assertEq(address(aggregateVerifier.ZK_VERIFIER()), zkVerifierAddr); - assertEq(aggregateVerifier.TEE_IMAGE_HASH(), _input.implementationsInput.teeImageHash); - assertEq(aggregateVerifier.ZK_RANGE_HASH(), _input.implementationsInput.zkRangeHash); - assertEq(aggregateVerifier.ZK_AGGREGATE_HASH(), _input.implementationsInput.zkAggregationHash); - assertEq(aggregateVerifier.CONFIG_HASH(), _input.implementationsInput.multiproofConfigHash); - assertEq(aggregateVerifier.L2_CHAIN_ID(), _input.opChainInput.l2ChainId); - TEEProverRegistry teeProverRegistry = TEEProverRegistry(teeProverRegistryProxyAddr); assertEq(teeProverRegistry.owner(), _input.opChainInput.roles.opChainProxyAdminOwner, "tee registry owner"); assertEq(teeProverRegistry.manager(), _input.opChainInput.roles.opChainProxyAdminOwner, "tee registry manager"); @@ -290,58 +264,10 @@ contract SystemDeploy_Test is Test, SystemDeployAssertions { teeProverRegistryProxyAddr, "tee verifier registry" ); - assertEq(address(ZKVerifier(zkVerifierAddr).SP1_VERIFIER()), address(_input.implementationsInput.sp1Verifier)); - } - - function _assertUpgradedProxyImplementations(SystemDeploy.DeployOutput memory _output) internal view { - IProxyAdmin superchainProxyAdmin = _output.superchain.superchainProxyAdmin; - IProxyAdmin opChainProxyAdmin = _output.opChain.opChainProxyAdmin; - Types.Implementations memory impls = _output.impls; - - assertEq( - superchainProxyAdmin.getProxyImplementation(address(_output.superchain.superchainConfigProxy)), - impls.superchainConfigImpl, - "superchain config impl" - ); - assertEq( - opChainProxyAdmin.getProxyImplementation(address(_output.opChain.systemConfigProxy)), - impls.systemConfigImpl, - "system config impl" - ); - assertEq( - opChainProxyAdmin.getProxyImplementation(address(_output.opChain.optimismPortalProxy)), - impls.optimismPortalImpl, - "portal impl" - ); - assertEq( - opChainProxyAdmin.getProxyImplementation(address(_output.opChain.anchorStateRegistryProxy)), - impls.anchorStateRegistryImpl, - "anchor state registry impl" - ); - assertEq( - opChainProxyAdmin.getProxyImplementation(address(_output.opChain.optimismMintableERC20FactoryProxy)), - impls.optimismMintableERC20FactoryImpl, - "erc20 factory impl" - ); - assertEq( - opChainProxyAdmin.getProxyImplementation(address(_output.opChain.disputeGameFactoryProxy)), - impls.disputeGameFactoryImpl, - "dispute game factory impl" - ); - assertEq( - opChainProxyAdmin.getProxyImplementation(address(_output.opChain.l1CrossDomainMessengerProxy)), - impls.l1CrossDomainMessengerImpl, - "messenger impl" - ); - assertEq( - opChainProxyAdmin.getProxyImplementation(address(_output.opChain.l1StandardBridgeProxy)), - impls.l1StandardBridgeImpl, - "standard bridge impl" - ); assertEq( - opChainProxyAdmin.getProxyImplementation(address(_output.opChain.l1ERC721BridgeProxy)), - impls.l1ERC721BridgeImpl, - "erc721 bridge impl" + address(ZKVerifier(zkVerifierAddr).SP1_VERIFIER()), + address(_input.implementationsInput.sp1Verifier), + "zk verifier sp1" ); } diff --git a/test/deploy/SystemDeployAssertions.sol b/test/deploy/SystemDeployAssertions.sol index 6b805ee8..fb3aa1c1 100644 --- a/test/deploy/SystemDeployAssertions.sol +++ b/test/deploy/SystemDeployAssertions.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { Test, console } from "lib/forge-std/src/Test.sol"; +import { Test } from "lib/forge-std/src/Test.sol"; import { Types } from "scripts/libraries/Types.sol"; @@ -172,8 +172,7 @@ abstract contract SystemDeployAssertions is Test { ); assertEq(address(portal.disputeGameFactory()), address(dgf), "PORTAL-30"); assertEq(address(portal.systemConfig()), address(sysCfg), "PORTAL-40"); - IDisputeGame av = dgf.gameImpls(_expected.multiproofGameType); - assertEq(address(portal.anchorStateRegistry()), address(av.anchorStateRegistry()), "PORTAL-50"); + assertEq(address(portal.anchorStateRegistry()), address(_expected.anchorStateRegistry), "PORTAL-50"); assertEq(portal.l2Sender(), Constants.DEFAULT_L2_SENDER, "PORTAL-80"); assertEq(address(_proxyAdminFor(address(portal))), address(_proxyAdmin), "PORTAL-90"); } @@ -208,9 +207,7 @@ abstract contract SystemDeployAssertions is Test { IDisputeGame game = factory.gameImpls(_gameType); assertNotEq(address(game), address(0), "AV-10"); - address expectedImpl = _expected.implementations.aggregateVerifierImpl; - assertEq(address(game), expectedImpl, "AV-15"); - assertEq(_version(address(game)), _version(expectedImpl), "AV-20"); + assertEq(address(game), _expected.implementations.aggregateVerifierImpl, "AV-15"); _assertGameArgsAndContracts({ _expected: _expected, @@ -272,19 +269,14 @@ abstract contract SystemDeployAssertions is Test { private view { - string memory prefix = "AV-DWETH"; + assertEq(_version(address(_weth)), _version(_expected.implementations.delayedWETHImpl), "AV-DWETH-10"); assertEq( - _version(address(_weth)), _version(_expected.implementations.delayedWETHImpl), string.concat(prefix, "-10") + _proxyAdmin.getProxyImplementation(address(_weth)), _expected.implementations.delayedWETHImpl, "AV-DWETH-20" ); - assertEq( - _proxyAdmin.getProxyImplementation(address(_weth)), - _expected.implementations.delayedWETHImpl, - string.concat(prefix, "-20") - ); - assertEq(_weth.proxyAdminOwner(), _expected.proxyAdminOwner, string.concat(prefix, "-30")); - assertEq(_weth.delay(), _expected.withdrawalDelaySeconds, string.concat(prefix, "-40")); - assertEq(address(_weth.systemConfig()), address(_expected.systemConfig), string.concat(prefix, "-50")); - assertEq(address(_proxyAdminFor(address(_weth))), address(_proxyAdmin), string.concat(prefix, "-60")); + assertEq(_weth.proxyAdminOwner(), _expected.proxyAdminOwner, "AV-DWETH-30"); + assertEq(_weth.delay(), _expected.withdrawalDelaySeconds, "AV-DWETH-40"); + assertEq(address(_weth.systemConfig()), address(_expected.systemConfig), "AV-DWETH-50"); + assertEq(address(_proxyAdminFor(address(_weth))), address(_proxyAdmin), "AV-DWETH-60"); } function _assertAnchorStateRegistry( @@ -296,21 +288,16 @@ abstract contract SystemDeployAssertions is Test { private view { - string memory prefix = "AV-ANCHORP"; - assertEq( - _version(address(_asr)), - _version(_expected.implementations.anchorStateRegistryImpl), - string.concat(prefix, "-10") - ); + assertEq(_version(address(_asr)), _version(_expected.implementations.anchorStateRegistryImpl), "AV-ANCHORP-10"); assertEq( _proxyAdmin.getProxyImplementation(address(_asr)), _expected.implementations.anchorStateRegistryImpl, - string.concat(prefix, "-20") + "AV-ANCHORP-20" ); - assertEq(address(_asr.disputeGameFactory()), address(_factory), string.concat(prefix, "-30")); - assertEq(address(_asr.systemConfig()), address(_expected.systemConfig), string.concat(prefix, "-40")); - assertEq(address(_proxyAdminFor(address(_asr))), address(_proxyAdmin), string.concat(prefix, "-50")); - assertGt(_asr.retirementTimestamp(), 0, string.concat(prefix, "-60")); + assertEq(address(_asr.disputeGameFactory()), address(_factory), "AV-ANCHORP-30"); + assertEq(address(_asr.systemConfig()), address(_expected.systemConfig), "AV-ANCHORP-40"); + assertEq(address(_proxyAdminFor(address(_asr))), address(_proxyAdmin), "AV-ANCHORP-50"); + assertGt(_asr.retirementTimestamp(), 0, "AV-ANCHORP-60"); } function _assertETHLockbox(ExpectedSystemDeployState memory _expected, IProxyAdmin _proxyAdmin) private view { diff --git a/test/invariants/CrossDomainMessenger.t.sol b/test/invariants/CrossDomainMessenger.t.sol index bf68e695..ec1ca218 100644 --- a/test/invariants/CrossDomainMessenger.t.sol +++ b/test/invariants/CrossDomainMessenger.t.sol @@ -1,127 +1,105 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { StdUtils } from "lib/forge-std/src/StdUtils.sol"; import { Vm } from "lib/forge-std/src/Vm.sol"; -import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; import { IL1CrossDomainMessenger } from "interfaces/L1/IL1CrossDomainMessenger.sol"; import { CommonTest } from "test/setup/CommonTest.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; -import { Constants } from "src/libraries/Constants.sol"; import { Encoding } from "src/libraries/Encoding.sol"; import { Hashing } from "src/libraries/Hashing.sol"; -import { ForgeArtifacts } from "scripts/libraries/ForgeArtifacts.sol"; +import { ForgeArtifacts, StorageSlot } from "scripts/libraries/ForgeArtifacts.sol"; -contract RelayActor is StdUtils { - // Storage slot of the l2Sender - uint256 senderSlotIndex; +contract RelayActor { + address internal constant IDENTITY_PRECOMPILE = address(0x04); + uint256 internal constant MAX_MESSAGE_SIZE = 1000; - uint256 public numHashes; - bytes32[] public hashes; - bool public reverted = false; + // Invariant handlers ignore target-call reverts, so failures must persist after relay returns. + bool public badRelayResult; - IOptimismPortal2 op; - IL1CrossDomainMessenger xdm; - Vm vm; - bool doFail; + address internal immutable op; + IL1CrossDomainMessenger internal immutable xdm; + Vm internal immutable vm; + bool internal immutable shouldFail; - constructor(IOptimismPortal2 _op, IL1CrossDomainMessenger _xdm, Vm _vm, bool _doFail) { + constructor(address _op, IL1CrossDomainMessenger _xdm, Vm _vm, bool _shouldFail) { op = _op; xdm = _xdm; vm = _vm; - doFail = _doFail; - senderSlotIndex = ForgeArtifacts.getSlot("OptimismPortal2", "l2Sender").slot; + shouldFail = _shouldFail; } - /// @notice Relays a message to the `L1CrossDomainMessenger` with a random `version`, - /// and `_message`. function relay(uint8 _version, uint8 _value, bytes memory _message) external { - address target = address(0x04); // ID precompile - address sender = Predeploys.L2_CROSS_DOMAIN_MESSENGER; + vm.assume(_message.length <= MAX_MESSAGE_SIZE); - // Set the minimum gas limit to the cost of the identity precompile's execution for - // the given message. - // ID Precompile cost can be determined by calculating: 15 + 3 * data_word_length - uint32 minGasLimit = uint32(15 + 3 * ((_message.length + 31) / 32)); - - // set the value of op.l2Sender() to be the L2 Cross Domain Messenger. - vm.store(address(op), bytes32(senderSlotIndex), bytes32(abi.encode(sender))); - - // Restrict version to the range of [0, 1] _version = _version % 2; - - // Restrict the value to the range of [0, 1] - // This is just so we get variance of calls with and without value. The ID precompile - // will not reject value being sent to it. _value = _value % 2; + // ID precompile gas cost: 15 + 3 * data_word_length. + uint32 minGasLimit = uint32(15 + 3 * ((_message.length + 31) / 32)); + // For the failure case, we use an impossibly large minGasLimit so that the hasMinGas // check always fails regardless of available gas. We provide baseGas-level gas (enough // for relayMessage's overhead) to avoid OOG reverts. Limiting gas directly is fragile // because the proxy-to-proxy call overhead (SystemConfig → SuperchainConfig, // OptimismPortal) leaves a razor-thin window between "enough to not OOG" and // "not enough for hasMinGas to pass". - uint32 relayMinGasLimit = doFail ? type(uint32).max : minGasLimit; - uint256 gas = xdm.baseGas(_message, minGasLimit); - - // Compute the cross domain message hash and store it in `hashes`. - // The `relayMessage` function will always encode the message as a version 1 - // message after checking that the V0 hash has not already been relayed. - bytes32 _hash = Hashing.hashCrossDomainMessageV1( - Encoding.encodeVersionedNonce(0, _version), sender, target, _value, relayMinGasLimit, _message - ); - hashes.push(_hash); - numHashes += 1; + uint32 relayMinGasLimit = shouldFail ? type(uint32).max : minGasLimit; + + // `relayMessage` always re-encodes as a v1 hash after checking the v0 hash hasn't been + // relayed, so the v1 hash is what we track. + uint256 nonce = Encoding.encodeVersionedNonce({ _nonce: 0, _version: _version }); + + bytes32 relayMessageHash = Hashing.hashCrossDomainMessageV1({ + _nonce: nonce, + _sender: Predeploys.L2_CROSS_DOMAIN_MESSENGER, + _target: IDENTITY_PRECOMPILE, + _value: _value, + _gasLimit: relayMinGasLimit, + _data: _message + }); + vm.assume(!xdm.successfulMessages(relayMessageHash) && !xdm.failedMessages(relayMessageHash)); - // Make sure we've got a fresh message. - vm.assume(xdm.successfulMessages(_hash) == false && xdm.failedMessages(_hash) == false); + uint256 gas = xdm.baseGas(_message, minGasLimit); - // Act as the optimism portal and call `relayMessage` on the `L1CrossDomainMessenger` with - // the outer min gas limit. - vm.startPrank(address(op)); - if (!doFail) { - vm.expectCallMinGas(address(0x04), _value, minGasLimit, _message); + if (!shouldFail) { + vm.expectCallMinGas(IDENTITY_PRECOMPILE, _value, minGasLimit, _message); } + vm.prank(op); try xdm.relayMessage{ gas: gas, value: _value }( - Encoding.encodeVersionedNonce(0, _version), sender, target, _value, relayMinGasLimit, _message + nonce, Predeploys.L2_CROSS_DOMAIN_MESSENGER, IDENTITY_PRECOMPILE, _value, relayMinGasLimit, _message ) { } catch { - // If any of these calls revert, set `reverted` to true to fail the invariant test. - // NOTE: This is to get around forge's invariant fuzzer ignoring reverted calls - // to this function. - reverted = true; + // Forge's invariant fuzzer ignores reverted target calls, so we surface the failure + // by flipping a flag the invariant asserts on. + badRelayResult = true; + } + + bool relaySucceeded = xdm.successfulMessages(relayMessageHash); + bool relayFailed = xdm.failedMessages(relayMessageHash); + if (relaySucceeded == shouldFail || relayFailed != shouldFail) { + badRelayResult = true; } - vm.stopPrank(); } } contract XDM_MinGasLimits is CommonTest { RelayActor actor; - function init(bool doFail) public virtual { - // Set up the `L1CrossDomainMessenger` and `OptimismPortal` contracts. + function _init(bool _shouldFail) internal { super.setUp(); - // Deploy a relay actor - actor = new RelayActor(optimismPortal2, l1CrossDomainMessenger, vm, doFail); + StorageSlot memory l2SenderSlot = ForgeArtifacts.getSlot("OptimismPortal2", "l2Sender"); + vm.store( + address(optimismPortal2), + bytes32(l2SenderSlot.slot), + bytes32(abi.encode(Predeploys.L2_CROSS_DOMAIN_MESSENGER)) + ); + actor = new RelayActor(address(optimismPortal2), l1CrossDomainMessenger, vm, _shouldFail); - // Give the portal some ether to send to `relayMessage` vm.deal(address(optimismPortal2), type(uint128).max); - // Target the `RelayActor` contract targetContract(address(actor)); - // Don't allow the estimation address to be the sender - excludeSender(Constants.ESTIMATION_ADDRESS); - - // Don't allow the predeploys to be the senders - uint160 prefix = uint160(0x420) << 148; - for (uint256 i = 0; i < 2048; i++) { - address addr = address(prefix | uint160(i)); - excludeContract(addr); - } - - // Target the actor's `relay` function bytes4[] memory selectors = new bytes4[](1); selectors[0] = actor.relay.selector; targetSelector(FuzzSelector({ addr: address(actor), selectors: selectors })); @@ -130,65 +108,24 @@ contract XDM_MinGasLimits is CommonTest { contract XDM_MinGasLimits_Succeeds is XDM_MinGasLimits { function setUp() public override { - // Don't fail - super.init(false); + super._init(false); } - /// @custom:invariant A call to `relayMessage` should succeed if at least the minimum gas limit - /// can be supplied to the target context, there is enough gas to complete - /// execution of `relayMessage` after the target context's execution is - /// finished, and the target context did not revert. - /// - /// There are two minimum gas limits here: - /// - /// - The outer min gas limit is for the call from the `OptimismPortal` to the - /// `L1CrossDomainMessenger`, and it can be retrieved by calling the xdm's - /// `baseGas` function with the `message` and inner limit. - /// - /// - The inner min gas limit is for the call from the - /// `L1CrossDomainMessenger` to the target contract. - function invariant_minGasLimits() external view { - uint256 length = actor.numHashes(); - for (uint256 i = 0; i < length; ++i) { - bytes32 hash = actor.hashes(i); - // The message hash is set in the successfulMessages mapping - assertTrue(l1CrossDomainMessenger.successfulMessages(hash)); - // The message hash is not set in the failedMessages mapping - assertFalse(l1CrossDomainMessenger.failedMessages(hash)); - } - assertFalse(actor.reverted()); + /// @custom:invariant `relayMessage` should succeed when the outer call has base gas and the + /// target can receive the inner minimum gas limit. + function invariant_relayMessage_forwardsMinGas_succeeds() external view { + assertFalse(actor.badRelayResult()); } } contract XDM_MinGasLimits_Reverts is XDM_MinGasLimits { function setUp() public override { - // Do fail - super.init(true); + super._init(true); } - /// @custom:invariant A call to `relayMessage` should assign the message hash to the - /// `failedMessages` mapping if not enough gas is supplied to forward - /// `minGasLimit` to the target context or if there is not enough gas to - /// complete execution of `relayMessage` after the target context's execution - /// is finished. - /// - /// There are two minimum gas limits here: - /// - /// - The outer min gas limit is for the call from the `OptimismPortal` to the - /// `L1CrossDomainMessenger`, and it can be retrieved by calling the xdm's - /// `baseGas` function with the `message` and inner limit. - /// - /// - The inner min gas limit is for the call from the - /// `L1CrossDomainMessenger` to the target contract. - function invariant_minGasLimits() external view { - uint256 length = actor.numHashes(); - for (uint256 i = 0; i < length; ++i) { - bytes32 hash = actor.hashes(i); - // The message hash is not set in the successfulMessages mapping - assertFalse(l1CrossDomainMessenger.successfulMessages(hash)); - // The message hash is set in the failedMessages mapping - assertTrue(l1CrossDomainMessenger.failedMessages(hash)); - } - assertFalse(actor.reverted()); + /// @custom:invariant `relayMessage` should mark the message failed when the inner minimum gas + /// limit is too large to forward to the target. + function invariant_relayMessage_insufficientMinGas_fails() external view { + assertFalse(actor.badRelayResult()); } } diff --git a/test/invariants/SafeCall.t.sol b/test/invariants/SafeCall.t.sol index ea255d1b..7a522be7 100644 --- a/test/invariants/SafeCall.t.sol +++ b/test/invariants/SafeCall.t.sol @@ -1,109 +1,89 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { StdUtils } from "lib/forge-std/src/StdUtils.sol"; import { Test } from "lib/forge-std/src/Test.sol"; -import { Vm } from "lib/forge-std/src/Vm.sol"; import { SafeCall } from "src/libraries/SafeCall.sol"; -contract SafeCall_Succeeds_Invariants is Test { +abstract contract SafeCall_Invariants is Test { SafeCaller_Actor actor; - function setUp() public { - // Create a new safe caller actor. - actor = new SafeCaller_Actor(vm, false); - - // Set the caller to this contract - targetSender(address(this)); - - // Target the safe caller actor. + function _init(bool _expectCallToFail) internal { + actor = new SafeCaller_Actor(address(this), _expectCallToFail); targetContract(address(actor)); - // Give the actor some ETH to work with - vm.deal(address(actor), type(uint128).max); - } + bytes4[] memory selectors = new bytes4[](1); + selectors[0] = actor.performSafeCallMinGas.selector; + targetSelector(FuzzSelector({ addr: address(actor), selectors: selectors })); - /// @custom:invariant If `callWithMinGas` performs a call, then it must always - /// provide at least the specified minimum gas limit to the subcontext. - /// - /// If the check for remaining gas in `SafeCall.callWithMinGas` passes, the - /// subcontext of the call below it must be provided at least `minGas` gas. - function invariant_callWithMinGas_alwaysForwardsMinGas_succeeds() public view { - assertEq(actor.numCalls(), 0, "no failed calls allowed"); + vm.deal(address(actor), type(uint128).max); } - function performSafeCallMinGas(address to, uint64 minGas) external payable { + function callWithMinGas(address to, uint64 minGas) external payable { SafeCall.callWithMinGas(to, minGas, msg.value, hex""); } } -contract SafeCall_Fails_Invariants is Test { - SafeCaller_Actor actor; - +contract SafeCall_Succeeds_Invariants is SafeCall_Invariants { function setUp() public { - // Create a new safe caller actor. - actor = new SafeCaller_Actor(vm, true); - - // Set the caller to this contract - targetSender(address(this)); - - // Target the safe caller actor. - targetContract(address(actor)); + _init(false); + } - // Give the actor some ETH to work with - vm.deal(address(actor), type(uint128).max); + /// @custom:invariant Successful calls always forward at least the requested minimum gas. + function invariant_callWithMinGas_alwaysForwardsMinGas_succeeds() external view { + assertFalse(actor.badCallResult()); } +} - /// @custom:invariant `callWithMinGas` reverts if there is not enough gas to pass - /// to the subcontext. - /// - /// If there is not enough gas in the callframe to ensure that - /// `callWithMinGas` can provide the specified minimum gas limit - /// to the subcontext of the call, then `callWithMinGas` must revert. - function invariant_callWithMinGas_neverForwardsMinGas_reverts() public view { - assertEq(actor.numCalls(), 0, "no successful calls allowed"); +contract SafeCall_Fails_Invariants is SafeCall_Invariants { + function setUp() public { + _init(true); } - function performSafeCallMinGas(address to, uint64 minGas) external payable { - SafeCall.callWithMinGas(to, minGas, msg.value, hex""); + /// @custom:invariant Calls revert when the frame cannot provide the requested minimum gas. + function invariant_callWithMinGas_neverForwardsMinGas_reverts() external view { + assertFalse(actor.badCallResult()); } } -contract SafeCaller_Actor is StdUtils { - bool internal immutable FAILS; +contract SafeCaller_Actor is Test { + uint64 internal constant MIN_MIN_GAS = 2_500; - Vm internal vm; - uint256 public numCalls; + // Keep the EIP-150 min-gas calculation inside uint64 bounds. + uint64 internal constant MAX_MIN_GAS = type(uint64).max / 64; - constructor(Vm _vm, bool _fails) { - vm = _vm; - FAILS = _fails; + // `callWithMinGas` reserves 40k call overhead; the extra 1k covers the harness frame. + uint64 internal constant CALL_WITH_MIN_GAS_BUFFER = 40_000 + 1_000; + + address internal immutable safeCallHarness; + bool internal immutable expectCallToFail; + + // Invariant handlers ignore target-call reverts, so failures must persist after the call. + bool public badCallResult; + + constructor(address _safeCallHarness, bool _expectCallToFail) { + safeCallHarness = _safeCallHarness; + expectCallToFail = _expectCallToFail; } function performSafeCallMinGas(uint64 gas, uint64 minGas, address to, uint8 value) external { - // Only send to EOAs - we exclude the console as it has no code but reverts when called - // with a selector that doesn't exist due to the foundry hook. - vm.assume(to.code.length == 0 && to != 0x000000000000000000636F6e736F6c652e6c6f67); - - // Bound the minimum gas amount to [2500, type(uint48).max] - minGas = uint64(bound(minGas, 2500, type(uint48).max)); - if (FAILS) { - // Bound the gas passed to [minGas, ((minGas * 64) / 63)] - gas = uint64(bound(gas, minGas, (minGas * 64) / 63)); + assumeUnusedAddress(to); + + minGas = uint64(bound(minGas, MIN_MIN_GAS, MAX_MIN_GAS)); + uint64 minCallGas = (minGas * 64) / 63; + if (expectCallToFail) { + gas = uint64(bound(gas, minGas, minCallGas)); } else { - // Bound the gas passed to - // [((minGas * 64) / 63) + 40_000 + 1000, type(uint64).max] - // The extra 1000 gas is to account for the gas used by the `SafeCall.call` call - // itself. - gas = uint64(bound(gas, ((minGas * 64) / 63) + 40_000 + 1000, type(uint64).max)); + gas = uint64(bound(gas, minCallGas + CALL_WITH_MIN_GAS_BUFFER, type(uint64).max)); + vm.expectCallMinGas(to, value, minGas, hex""); } - vm.expectCallMinGas(to, value, minGas, hex""); bool success = SafeCall.call( - msg.sender, gas, value, abi.encodeCall(SafeCall_Succeeds_Invariants.performSafeCallMinGas, (to, minGas)) + safeCallHarness, gas, value, abi.encodeCall(SafeCall_Invariants.callWithMinGas, (to, minGas)) ); - if (success && FAILS) numCalls++; - if (!FAILS && !success) numCalls++; + bool expectedSuccess = !expectCallToFail; + if (success != expectedSuccess) { + badCallResult = true; + } } } diff --git a/test/legacy/L1ChugSplashProxy.t.sol b/test/legacy/L1ChugSplashProxy.t.sol index bc5c6231..e7662cb6 100644 --- a/test/legacy/L1ChugSplashProxy.t.sol +++ b/test/legacy/L1ChugSplashProxy.t.sol @@ -16,11 +16,7 @@ import { LibString } from "lib/solady/src/utils/LibString.sol"; import { IL1ChugSplashProxy } from "interfaces/legacy/IL1ChugSplashProxy.sol"; contract L1ChugSplashProxy_Owner_Harness { - bool public isUpgrading; - - function setIsUpgrading(bool _isUpgrading) public { - isUpgrading = _isUpgrading; - } + bool public isUpgrading = true; } contract L1ChugSplashProxy_Implementation_Harness { @@ -48,20 +44,37 @@ contract L1ChugSplashProxy_Implementation_Harness { /// @title L1ChugSplashProxy_TestInit /// @notice Reusable test initialization for `L1ChugSplashProxy` tests. abstract contract L1ChugSplashProxy_TestInit is Test { + bytes internal constant RETURN_42_BYTECODE = hex"604260005260206000f3"; + IL1ChugSplashProxy proxy; - address impl; address owner = makeAddr("owner"); address alice = makeAddr("alice"); - function setUp() public { - proxy = IL1ChugSplashProxy( + function setUp() public virtual { + proxy = _deployProxy(); + vm.prank(owner); + assertEq(proxy.getOwner(), owner); + } + + function _deployProxy() internal returns (IL1ChugSplashProxy) { + return IL1ChugSplashProxy( DeployUtils.create1({ _name: "L1ChugSplashProxy", _args: DeployUtils.encodeConstructor(abi.encodeCall(IL1ChugSplashProxy.__constructor__, (owner))) }) ); - vm.prank(owner); - assertEq(proxy.getOwner(), owner); + } + + function _proxyAsImplementation() internal view returns (L1ChugSplashProxy_Implementation_Harness) { + return L1ChugSplashProxy_Implementation_Harness(address(proxy)); + } +} + +abstract contract L1ChugSplashProxy_WithImplementation_TestInit is L1ChugSplashProxy_TestInit { + address impl; + + function setUp() public virtual override { + super.setUp(); vm.prank(owner); proxy.setCode(type(L1ChugSplashProxy_Implementation_Harness).runtimeCode); @@ -73,11 +86,11 @@ abstract contract L1ChugSplashProxy_TestInit is Test { /// @title L1ChugSplashProxy_SetCode_Test /// @notice Tests the `setCode` function of the `L1ChugSplashProxy` contract. -contract L1ChugSplashProxy_SetCode_Test is L1ChugSplashProxy_TestInit { +contract L1ChugSplashProxy_SetCode_Test is L1ChugSplashProxy_WithImplementation_TestInit { /// @notice Tests that the owner can deploy a new implementation with a given runtime code. function test_setCode_whenOwner_succeeds() public { vm.prank(owner); - proxy.setCode(hex"604260005260206000f3"); + proxy.setCode(RETURN_42_BYTECODE); vm.prank(owner); assertNotEq(proxy.getImplementation(), impl); @@ -85,7 +98,7 @@ contract L1ChugSplashProxy_SetCode_Test is L1ChugSplashProxy_TestInit { /// @notice Tests that when not the owner, `setCode` delegatecalls the implementation. function test_setCode_whenNotOwner_works() public view { - uint256 ret = L1ChugSplashProxy_Implementation_Harness(address(proxy)).setCode(hex"604260005260206000f3"); + uint256 ret = _proxyAsImplementation().setCode(RETURN_42_BYTECODE); assertEq(ret, 1); } @@ -95,7 +108,6 @@ contract L1ChugSplashProxy_SetCode_Test is L1ChugSplashProxy_TestInit { vm.prank(owner); proxy.setCode(type(L1ChugSplashProxy_Implementation_Harness).runtimeCode); - // does not deploy new implementation vm.prank(owner); assertEq(proxy.getImplementation(), impl); } @@ -135,7 +147,7 @@ contract L1ChugSplashProxy_SetCode_Test is L1ChugSplashProxy_TestInit { /// @title L1ChugSplashProxy_SetStorage_Test /// @notice Tests the `setStorage` function of the `L1ChugSplashProxy` contract. -contract L1ChugSplashProxy_SetStorage_Test is L1ChugSplashProxy_TestInit { +contract L1ChugSplashProxy_SetStorage_Test is L1ChugSplashProxy_WithImplementation_TestInit { /// @notice Tests that the owner can set storage of the proxy function test_setStorage_whenOwner_works() public { vm.prank(owner); @@ -145,8 +157,7 @@ contract L1ChugSplashProxy_SetStorage_Test is L1ChugSplashProxy_TestInit { /// @notice Tests that when not the owner, `setStorage` delegatecalls the implementation function test_setStorage_whenNotOwner_works() public view { - uint256 ret = - L1ChugSplashProxy_Implementation_Harness(address(proxy)).setStorage(bytes32(0), bytes32(uint256(42))); + uint256 ret = _proxyAsImplementation().setStorage(bytes32(0), bytes32(uint256(42))); assertEq(ret, 2); assertEq(vm.load(address(proxy), bytes32(0)), bytes32(uint256(0))); } @@ -154,7 +165,7 @@ contract L1ChugSplashProxy_SetStorage_Test is L1ChugSplashProxy_TestInit { /// @title L1ChugSplashProxy_SetOwner_Test /// @notice Tests the `setOwner` function of the `L1ChugSplashProxy` contract. -contract L1ChugSplashProxy_SetOwner_Test is L1ChugSplashProxy_TestInit { +contract L1ChugSplashProxy_SetOwner_Test is L1ChugSplashProxy_WithImplementation_TestInit { /// @notice Tests that the owner can set the owner of the proxy function test_setOwner_whenOwner_works() public { vm.prank(owner); @@ -166,7 +177,7 @@ contract L1ChugSplashProxy_SetOwner_Test is L1ChugSplashProxy_TestInit { /// @notice Tests that when not the owner, `setOwner` delegatecalls the implementation function test_setOwner_whenNotOwner_works() public { - uint256 ret = L1ChugSplashProxy_Implementation_Harness(address(proxy)).setOwner(alice); + uint256 ret = _proxyAsImplementation().setOwner(alice); assertEq(ret, 3); vm.prank(owner); @@ -176,7 +187,7 @@ contract L1ChugSplashProxy_SetOwner_Test is L1ChugSplashProxy_TestInit { /// @title L1ChugSplashProxy_GetOwner_Test /// @notice Tests the `getOwner` function of the `L1ChugSplashProxy` contract. -contract L1ChugSplashProxy_GetOwner_Test is L1ChugSplashProxy_TestInit { +contract L1ChugSplashProxy_GetOwner_Test is L1ChugSplashProxy_WithImplementation_TestInit { /// @notice Tests that the owner can get the owner of the proxy function test_getOwner_whenOwner_works() public { vm.prank(owner); @@ -185,14 +196,14 @@ contract L1ChugSplashProxy_GetOwner_Test is L1ChugSplashProxy_TestInit { /// @notice Tests that when not the owner, `getOwner` delegatecalls the implementation function test_getOwner_whenNotOwner_works() public view { - uint256 ret = L1ChugSplashProxy_Implementation_Harness(address(proxy)).getOwner(); + uint256 ret = _proxyAsImplementation().getOwner(); assertEq(ret, 4); } } /// @title L1ChugSplashProxy_GetImplementation_Test /// @notice Tests the `getImplementation` function of the `L1ChugSplashProxy` contract. -contract L1ChugSplashProxy_GetImplementation_Test is L1ChugSplashProxy_TestInit { +contract L1ChugSplashProxy_GetImplementation_Test is L1ChugSplashProxy_WithImplementation_TestInit { /// @notice Tests that the owner can get the implementation of the proxy function test_getImplementation_whenOwner_works() public { vm.prank(owner); @@ -201,29 +212,25 @@ contract L1ChugSplashProxy_GetImplementation_Test is L1ChugSplashProxy_TestInit /// @notice Tests that when not the owner, `getImplementation` delegatecalls the implementation function test_getImplementation_whenNotOwner_works() public view { - uint256 ret = L1ChugSplashProxy_Implementation_Harness(address(proxy)).getImplementation(); + uint256 ret = _proxyAsImplementation().getImplementation(); assertEq(ret, 5); } } -/// @title L1ChugSplashProxy_Uncategorized_Test -/// @notice General tests that are not testing any function directly of the `L1ChugSplashProxy` -/// contract or are testing multiple functions at once. -contract L1ChugSplashProxy_Uncategorized_Test is L1ChugSplashProxy_TestInit { +/// @title L1ChugSplashProxy_NoImplementation_Test +/// @notice Tests calls against a proxy before its implementation has been set. +contract L1ChugSplashProxy_NoImplementation_Test is L1ChugSplashProxy_TestInit { /// @notice Tests that when the caller is not the owner and the implementation is not set, all /// calls reverts. function test_calls_whenNotOwnerNoImplementation_reverts() public { - proxy = IL1ChugSplashProxy( - DeployUtils.create1({ - _name: "L1ChugSplashProxy", - _args: DeployUtils.encodeConstructor(abi.encodeCall(IL1ChugSplashProxy.__constructor__, (owner))) - }) - ); - vm.expectRevert(bytes("L1ChugSplashProxy: implementation is not set yet")); - L1ChugSplashProxy_Implementation_Harness(address(proxy)).setCode(hex"604260005260206000f3"); + _proxyAsImplementation().setCode(RETURN_42_BYTECODE); } +} +/// @title L1ChugSplashProxy_Upgrading_Test +/// @notice Tests calls against a proxy while its owner reports an active upgrade. +contract L1ChugSplashProxy_Upgrading_Test is L1ChugSplashProxy_WithImplementation_TestInit { /// @notice Tests that when the caller is not the owner but the owner has marked `isUpgrading` /// as true, the call reverts. function test_calls_whenUpgrading_reverts() public { @@ -231,9 +238,7 @@ contract L1ChugSplashProxy_Uncategorized_Test is L1ChugSplashProxy_TestInit { vm.prank(owner); proxy.setOwner(address(ownerContract)); - ownerContract.setIsUpgrading(true); - vm.expectRevert(bytes("L1ChugSplashProxy: system is currently being upgraded")); - L1ChugSplashProxy_Implementation_Harness(address(proxy)).setCode(hex"604260005260206000f3"); + _proxyAsImplementation().setCode(RETURN_42_BYTECODE); } } diff --git a/test/legacy/ResolvedDelegateProxy.t.sol b/test/legacy/ResolvedDelegateProxy.t.sol index 27997f09..247395f2 100644 --- a/test/legacy/ResolvedDelegateProxy.t.sol +++ b/test/legacy/ResolvedDelegateProxy.t.sol @@ -1,13 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -// Testing utilities import { Test } from "lib/forge-std/src/Test.sol"; - -// Target contract dependencies import { IAddressManager } from "interfaces/legacy/IAddressManager.sol"; - -// Target contract import { IResolvedDelegateProxy } from "interfaces/legacy/IResolvedDelegateProxy.sol"; import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; @@ -21,76 +16,68 @@ contract ResolvedDelegateProxy_SimpleImplementation_Harness { } } -/// @title ResolvedDelegateProxy_TestInit -/// @notice Reusable test initialization for `ResolvedDelegateProxy` tests. -abstract contract ResolvedDelegateProxy_TestInit is Test { - IAddressManager internal addressManager; - ResolvedDelegateProxy_SimpleImplementation_Harness internal impl; - ResolvedDelegateProxy_SimpleImplementation_Harness internal proxy; +abstract contract ResolvedDelegateProxy_TestBase is Test { + string internal constant IMPLEMENTATION_NAME = "SimpleImplementation"; - /// @notice Sets up the test suite. - function setUp() public { - // Set up the address manager. - addressManager = IAddressManager( + function _deployAddressManager() internal returns (IAddressManager) { + return IAddressManager( DeployUtils.create1({ _name: "AddressManager", _args: DeployUtils.encodeConstructor(abi.encodeCall(IAddressManager.__constructor__, ())) }) ); - impl = new ResolvedDelegateProxy_SimpleImplementation_Harness(); - addressManager.setAddress("SimpleImplementation", address(impl)); + } - // Set up the proxy. - proxy = ResolvedDelegateProxy_SimpleImplementation_Harness( - address( - DeployUtils.create1({ - _name: "ResolvedDelegateProxy", - _args: DeployUtils.encodeConstructor( - abi.encodeCall(IResolvedDelegateProxy.__constructor__, (addressManager, "SimpleImplementation")) - ) - }) - ) + function _deployProxy(IAddressManager _addressManager) internal returns (IResolvedDelegateProxy) { + return IResolvedDelegateProxy( + DeployUtils.create1({ + _name: "ResolvedDelegateProxy", + _args: DeployUtils.encodeConstructor( + abi.encodeCall(IResolvedDelegateProxy.__constructor__, (_addressManager, IMPLEMENTATION_NAME)) + ) + }) ); } + + function _proxyAsImplementation(IResolvedDelegateProxy _proxy) + internal + pure + returns (ResolvedDelegateProxy_SimpleImplementation_Harness) + { + return ResolvedDelegateProxy_SimpleImplementation_Harness(address(_proxy)); + } } -/// @title ResolvedDelegateProxy_Fallback_Test -/// @notice Tests the `fallback` function of the `ResolvedDelegateProxy` contract. -contract ResolvedDelegateProxy_Fallback_Test is ResolvedDelegateProxy_TestInit { - /// @notice Tests that the proxy properly bubbles up returndata when the delegatecall succeeds. +contract ResolvedDelegateProxy_Fallback_Test is ResolvedDelegateProxy_TestBase { + IAddressManager internal addressManager; + ResolvedDelegateProxy_SimpleImplementation_Harness internal impl; + IResolvedDelegateProxy internal proxy; + + function setUp() public { + addressManager = _deployAddressManager(); + impl = new ResolvedDelegateProxy_SimpleImplementation_Harness(); + addressManager.setAddress(IMPLEMENTATION_NAME, address(impl)); + + proxy = _deployProxy(addressManager); + } + function testFuzz_fallback_delegateCallFoo_succeeds(uint256 x) public { vm.expectCall(address(impl), abi.encodeCall(impl.foo, (x))); - assertEq(proxy.foo(x), x); + assertEq(_proxyAsImplementation(proxy).foo(x), x); } - /// @notice Tests that the proxy properly bubbles up returndata when the delegatecall reverts. function test_fallback_delegateCallBar_reverts() public { vm.expectRevert("SimpleImplementation: revert"); vm.expectCall(address(impl), abi.encodeCall(impl.bar, ())); - proxy.bar(); + _proxyAsImplementation(proxy).bar(); } +} - /// @notice Tests that the proxy fallback reverts as expected if the implementation within the - /// address manager is not set. +contract ResolvedDelegateProxy_FallbackNoImplementation_Test is ResolvedDelegateProxy_TestBase { function test_fallback_addressManagerNotSet_reverts() public { - IAddressManager am = IAddressManager( - DeployUtils.create1({ - _name: "AddressManager", - _args: DeployUtils.encodeConstructor(abi.encodeCall(IAddressManager.__constructor__, ())) - }) - ); - ResolvedDelegateProxy_SimpleImplementation_Harness p = ResolvedDelegateProxy_SimpleImplementation_Harness( - address( - DeployUtils.create1({ - _name: "ResolvedDelegateProxy", - _args: DeployUtils.encodeConstructor( - abi.encodeCall(IResolvedDelegateProxy.__constructor__, (am, "SimpleImplementation")) - ) - }) - ) - ); + IResolvedDelegateProxy proxy = _deployProxy(_deployAddressManager()); vm.expectRevert("ResolvedDelegateProxy: target address must be initialized"); - p.foo(0); + _proxyAsImplementation(proxy).foo(0); } } diff --git a/test/libraries/Bytes.t.sol b/test/libraries/Bytes.t.sol index a3308772..4f214fd5 100644 --- a/test/libraries/Bytes.t.sol +++ b/test/libraries/Bytes.t.sol @@ -1,10 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -// Testing utilities import { Test } from "lib/forge-std/src/Test.sol"; - -// Target contract import { Bytes } from "src/libraries/Bytes.sol"; contract Bytes_Harness { @@ -16,34 +13,38 @@ contract Bytes_Harness { /// @title Bytes_TestInit /// @notice Reusable test initialization for `Bytes` tests. abstract contract Bytes_TestInit is Test { - Bytes_Harness harness; - - /// @notice Manually checks equality of two dynamic `bytes` arrays in memory. - /// @param _a The first `bytes` array to compare. - /// @param _b The second `bytes` array to compare. - /// @return True if the two `bytes` arrays are equal in memory. function manualEq(bytes memory _a, bytes memory _b) internal pure returns (bool) { - bool _eq; + return _a.length == _b.length && keccak256(_a) == keccak256(_b); + } + + function freeMemoryPtr() internal pure returns (uint64 ptr_) { assembly { - _eq := and( - // Check if the contents of the two bytes arrays are equal in memory. - eq(keccak256(add(0x20, _a), mload(_a)), keccak256(add(0x20, _b), mload(_b))), - // Check if the length of the two bytes arrays are equal in memory. - // This is redundant given the above check, but included for completeness. - eq(mload(_a), mload(_b)) - ) + ptr_ := mload(0x40) } - return _eq; } - function setUp() public { - harness = new Bytes_Harness(); + function nextSliceMemoryPtr(uint64 _ptr, uint256 _length) internal pure returns (uint64) { + if (_length == 0) { + return _ptr + 0x20; + } + + return uint64((_ptr + 0x20 + _length + 0x1f) & ~uint256(0x1f)); + } + + function nextBytesMemoryPtr(uint64 _ptr, uint256 _length) internal pure returns (uint64) { + return uint64(_ptr + 0x20 + ((_length + 0x1f) & ~uint256(0x1f))); } } /// @title Bytes_Slice_Test /// @notice Tests the `slice` function of the `Bytes` library. contract Bytes_Slice_Test is Bytes_TestInit { + Bytes_Harness harness; + + function setUp() public { + harness = new Bytes_Harness(); + } + /// @notice Tests that the `slice` function works as expected when starting from index 0. function test_slice_fromZeroIdx_works() public pure { bytes memory input = hex"11223344556677889900"; @@ -107,54 +108,17 @@ contract Bytes_Slice_Test is Bytes_TestInit { function testFuzz_slice_memorySafety_succeeds(bytes memory _input, uint256 _start, uint256 _length) public { vm.assume(_input.length > 0); - // The start should never be more than the length of the input bytes array - 1 _start = bound(_start, 0, _input.length - 1); - - // The length should never be more than the length of the input bytes array - the starting - // slice index. _length = bound(_length, 0, _input.length - _start); - // Grab the free memory pointer before the slice operation - uint64 initPtr; - assembly { - initPtr := mload(0x40) - } - uint64 expectedPtr = uint64((initPtr + 0x20 + _length + 0x1f) & ~uint256(0x1f)); + uint64 initPtr = freeMemoryPtr(); + uint64 expectedPtr = nextSliceMemoryPtr(initPtr, _length); - // Ensure that all memory outside of the expected range is safe. vm.expectSafeMemory(initPtr, expectedPtr); - - // Slice the input bytes array from `_start` to `_start + _length` bytes memory slice = Bytes.slice(_input, _start, _length); - vm.stopExpectSafeMemory(); - // Grab the free memory pointer after the slice operation - uint64 finalPtr; - assembly { - finalPtr := mload(0x40) - } - - // The free memory pointer should have been updated properly - if (_length == 0) { - // If the slice length is zero, only 32 bytes of memory should have been allocated. - assertEq(finalPtr, initPtr + 0x20); - } else { - // If the slice length is greater than zero, the memory allocated should be the length - // of the slice rounded up to the next 32 byte word + 32 bytes for the length of the - // byte array. - // - // Note that we use a slightly less efficient, but equivalent method of rounding up - // `_length` to the next multiple of 32 than is used in the `slice` function. This is - // to diff test the method used in `slice`. - uint64 _expectedPtr = uint64(((initPtr + 0x20 + _length + 0x1F) >> 5) << 5); - assertEq(finalPtr, _expectedPtr); - - // Sanity check for equivalence of the rounding methods. - assertEq(_expectedPtr, expectedPtr); - } - - // The slice length should be equal to `_length` + assertEq(freeMemoryPtr(), expectedPtr); assertEq(slice.length, _length); } @@ -181,26 +145,25 @@ contract Bytes_Slice_Test is Bytes_TestInit { /// @notice Tests that, when given a length `n` that is greater than `type(uint256).max - 31`, /// the `slice` function reverts. - function testFuzz_slice_lengthOverflows_reverts(bytes memory _input, uint256 _start, uint256 _length) public { + function testFuzz_slice_lengthOverflows_reverts(uint256 _length) public { // Ensure that the `_length` will overflow if a number >= 31 is added to it. _length = uint256(bound(_length, type(uint256).max - 30, type(uint256).max)); vm.expectRevert("slice_overflow"); - harness.exposed_slice(_input, _start, _length); + harness.exposed_slice(hex"", 0, _length); } /// @notice Tests that, when given a start index `n` that is greater than /// `type(uint256).max - n`, the `slice` function reverts. /// The calls to `bound` are to reduce the number of times that `assume` is triggered. function testFuzz_slice_rangeOverflows_reverts(bytes memory _input, uint256 _start, uint256 _length) public { + vm.assume(_input.length > 1); + // Ensure that `_length` is a realistic length of a slice. This is to make sure we revert // on the correct require statement. - _length = bound(_length, 0, _input.length == 0 ? 0 : _input.length - 1); - vm.assume(_length < _input.length); + _length = bound(_length, 1, _input.length - 1); - // Ensure that `_start` will overflow if `_length` is added to it. - _start = bound(_start, type(uint256).max - _length, type(uint256).max); - vm.assume(_start > type(uint256).max - _length); + _start = bound(_start, type(uint256).max - _length + 1, type(uint256).max); vm.expectRevert("slice_overflow"); harness.exposed_slice(_input, _start, _length); @@ -240,60 +203,22 @@ contract Bytes_ToNibbles_Test is Bytes_TestInit { /// @notice Tests that, given an input of 0 bytes, the `toNibbles` function returns a zero /// length array. function test_toNibbles_zeroLengthInput_works() public pure { - bytes memory input = hex""; - bytes memory expected = hex""; - bytes memory actual = Bytes.toNibbles(input); - - assertEq(input.length, 0); - assertEq(expected.length, 0); - assertEq(actual.length, 0); - assertEq(actual, expected); + assertEq(Bytes.toNibbles(hex""), hex""); } /// @notice Tests that the `toNibbles` function correctly updates the free memory pointer /// depending on the length of the resulting array. function testFuzz_toNibbles_memorySafety_succeeds(bytes memory _input) public { - // Grab the free memory pointer before the `toNibbles` operation - uint64 initPtr; - assembly { - initPtr := mload(0x40) - } - uint64 expectedPtr = uint64(initPtr + 0x20 + ((_input.length * 2 + 0x1F) & ~uint256(0x1F))); + uint256 nibblesLength = _input.length * 2; + uint64 initPtr = freeMemoryPtr(); + uint64 expectedPtr = nextBytesMemoryPtr(initPtr, nibblesLength); - // Ensure that all memory outside of the expected range is safe. vm.expectSafeMemory(initPtr, expectedPtr); - - // Pull out each individual nibble from the input bytes array bytes memory nibbles = Bytes.toNibbles(_input); - vm.stopExpectSafeMemory(); - // Grab the free memory pointer after the `toNibbles` operation - uint64 finalPtr; - assembly { - finalPtr := mload(0x40) - } - - // The free memory pointer should have been updated properly - if (_input.length == 0) { - // If the input length is zero, only 32 bytes of memory should have been allocated. - assertEq(finalPtr, initPtr + 0x20); - } else { - // If the input length is greater than zero, the memory allocated should be the length - // of the input * 2 + 32 bytes for the length field. - // - // Note that we use a slightly less efficient, but equivalent method of rounding up - // `_length` to the next multiple of 32 than is used in the `toNibbles` function. This - // is to diff test the method used in `toNibbles`. - uint64 _expectedPtr = uint64(initPtr + 0x20 + (((_input.length * 2 + 0x1F) >> 5) << 5)); - assertEq(finalPtr, _expectedPtr); - - // Sanity check for equivalence of the rounding methods. - assertEq(_expectedPtr, expectedPtr); - } - - // The nibbles length should be equal to `_length * 2` - assertEq(nibbles.length, _input.length << 1); + assertEq(freeMemoryPtr(), expectedPtr); + assertEq(nibbles.length, nibblesLength); } } diff --git a/test/libraries/Constants.t.sol b/test/libraries/Constants.t.sol index 7fc24797..3a2541ac 100644 --- a/test/libraries/Constants.t.sol +++ b/test/libraries/Constants.t.sol @@ -5,47 +5,37 @@ import { Test } from "lib/forge-std/src/Test.sol"; import { Constants } from "src/libraries/Constants.sol"; import { IResourceMetering } from "interfaces/L1/IResourceMetering.sol"; -/// @title Constants_Test -/// @notice Tests the constant values defined in the `Constants` library. contract Constants_Test is Test { - /// @notice Verify ESTIMATION_ADDRESS constant value. function test_estimationAddress_succeeds() external pure { assertEq(Constants.ESTIMATION_ADDRESS, address(1)); } - /// @notice Verify DEFAULT_L2_SENDER constant value. function test_defaultL2Sender_succeeds() external pure { assertEq(Constants.DEFAULT_L2_SENDER, 0x000000000000000000000000000000000000dEaD); } - /// @notice Verify EIP1967 proxy implementation storage slot. function test_proxyImplementationAddress_succeeds() external pure { assertEq( bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1), Constants.PROXY_IMPLEMENTATION_ADDRESS ); } - /// @notice Verify EIP1967 proxy admin storage slot. function test_proxyOwnerAddress_succeeds() external pure { assertEq(bytes32(uint256(keccak256("eip1967.proxy.admin")) - 1), Constants.PROXY_OWNER_ADDRESS); } - /// @notice Verify GUARD_STORAGE_SLOT constant value. function test_guardStorageSlot_succeeds() external pure { assertEq(keccak256("guard_manager.guard.address"), Constants.GUARD_STORAGE_SLOT); } - /// @notice Verify ETHER constant value. function test_ether_succeeds() external pure { assertEq(Constants.ETHER, 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE); } - /// @notice Verify DEPOSITOR_ACCOUNT constant value. function test_depositorAccount_succeeds() external pure { assertEq(Constants.DEPOSITOR_ACCOUNT, 0xDeaDDEaDDeAdDeAdDEAdDEaddeAddEAdDEAd0001); } - /// @notice Verify DEFAULT_RESOURCE_CONFIG returns expected values. function test_defaultResourceConfig_succeeds() external pure { IResourceMetering.ResourceConfig memory config = Constants.DEFAULT_RESOURCE_CONFIG(); assertEq(config.maxResourceLimit, 20_000_000); diff --git a/test/libraries/EOA.t.sol b/test/libraries/EOA.t.sol index 7f7f082b..97153565 100644 --- a/test/libraries/EOA.t.sol +++ b/test/libraries/EOA.t.sol @@ -17,99 +17,74 @@ contract EOA_Harness { } } -/// @title EOA_TestInit -/// @notice Reusable test initialization for `EOA` tests. -abstract contract EOA_TestInit is Test { +/// @title EOA_isSenderEOA_Test +/// @notice Tests the `isSenderEOA` function of the `EOA` library. +contract EOA_isSenderEOA_Test is Test { EOA_Harness harness; /// @notice Sets up the test. function setUp() public { harness = new EOA_Harness(); } -} -/// @title EOA_isSenderEOA_Test -/// @notice Tests the `isSenderEOA` function of the `EOA` library. -contract EOA_isSenderEOA_Test is EOA_TestInit { /// @notice Tests that a standard EOA is detected as an EOA. /// @param _privateKey The private key of the sender. function testFuzz_isSenderEOA_isStandardEOA_succeeds(uint256 _privateKey) external { - // Make sure that the private key is in the range of a valid secp256k1 private key. - _privateKey = boundPrivateKey(_privateKey); - - // Make sure that the sender is a standard EOA with no code. - address sender = vm.addr(_privateKey); + address sender = _senderFromPrivateKey(_privateKey); vm.assume(sender.code.length == 0); - // Should be considered an EOA vm.prank(sender, sender); - assertEq(harness.isSenderEOA(), true); + assertTrue(harness.isSenderEOA()); } /// @notice Tests that a 7702 EOA is detected as an EOA. /// @param _privateKey The private key of the sender. /// @param _7702Target The target of the 7702 EOA. function testFuzz_isSenderEOA_is7702EOA_succeeds(uint256 _privateKey, address _7702Target) external { - // Make sure that the private key is in the range of a valid secp256k1 private key. - _privateKey = boundPrivateKey(_privateKey); - // Delegating to address(0) revokes the delegation per EIP-7702, so exclude it. vm.assume(_7702Target != address(0)); - // Make sure that the sender is a 7702 EOA. - address sender = vm.addr(_privateKey); + address sender = _senderFromPrivateKey(_privateKey); vm.etch(sender, abi.encodePacked(hex"EF0100", _7702Target)); - // Should be considered a 7702 EOA. vm.prank(sender, sender); - assertEq(harness.isSenderEOA(), true); + assertTrue(harness.isSenderEOA()); // Should still be considered an EOA even if origin is different. vm.prank(sender, address(0x0420)); - assertEq(harness.isSenderEOA(), true); + assertTrue(harness.isSenderEOA()); } /// @notice Tests that a contract is not detected as an EOA. /// @param _privateKey The private key of the sender. /// @param _code The code of the sender. function testFuzz_isSenderEOA_isContract_succeeds(uint256 _privateKey, bytes memory _code) external { - // Make sure that the private key is in the range of a valid secp256k1 private key. - _privateKey = boundPrivateKey(_privateKey); - - // If code is empty or starts with EF, change it. + // Avoid empty code and the 0xEF prefix space, which includes 7702 delegation code. if (_code.length == 0 || _code[0] == 0xEF) { _code = bytes.concat(hex"FFFFFF", _code); } - // Make sure that the sender is a contract. - address sender = vm.addr(_privateKey); + address sender = _senderFromPrivateKey(_privateKey); vm.etch(sender, _code); - // Should not be considered an EOA. vm.prank(sender); - assertEq(harness.isSenderEOA(), false); + assertFalse(harness.isSenderEOA()); } /// @notice Tests that a contract with 23 bytes of code is not detected as an EOA. /// @param _privateKey The private key of the sender. function testFuzz_isSenderEOA_isContract23BytesNot7702_succeeds(uint256 _privateKey) external { - // Make sure that the private key is in the range of a valid secp256k1 private key. - _privateKey = boundPrivateKey(_privateKey); + address sender = _senderFromPrivateKey(_privateKey); + vm.etch(sender, abi.encodePacked(hex"FE", bytes22(0))); - // Generate a random 23 byte code. - bytes memory code = vm.randomBytes(23); - - // If the code happens to be EOF code, change it! - if (code[0] == 0xEF) { - code[0] = 0xFE; // Anything but EF! - } - - // Make sure that the sender is a contract. - address sender = vm.addr(_privateKey); - vm.etch(sender, code); - - // Should not be considered an EOA. vm.prank(sender); - assertEq(harness.isSenderEOA(), false); + assertFalse(harness.isSenderEOA()); + } + + /// @notice Returns the sender for a valid secp256k1 private key. + /// @param _privateKey The private key of the sender. + /// @return sender_ The sender address. + function _senderFromPrivateKey(uint256 _privateKey) internal pure returns (address sender_) { + sender_ = vm.addr(boundPrivateKey(_privateKey)); } } diff --git a/test/libraries/Encoding.t.sol b/test/libraries/Encoding.t.sol index b55164c1..972dbfb5 100644 --- a/test/libraries/Encoding.t.sol +++ b/test/libraries/Encoding.t.sol @@ -30,13 +30,45 @@ contract Encoding_Harness { } /// @title Encoding_TestInit -/// @notice Reusable test initialization for `Encoding` tests. +/// @notice Reusable helpers for `Encoding` tests. abstract contract Encoding_TestInit is CommonTest { - Encoding_Harness encoding; + bytes1 internal constant SUPER_ROOT_VERSION = 0x01; + uint256 internal constant MAX_OUTPUT_ROOTS = 50; - function setUp() public override { - super.setUp(); - encoding = new Encoding_Harness(); + function _outputRoot(uint256 _chainId, bytes32 _root) internal pure returns (Types.OutputRootWithChainId memory) { + return Types.OutputRootWithChainId({ chainId: _chainId, root: _root }); + } + + function _superRootProof( + uint64 _timestamp, + uint256 _length, + uint256 _seed + ) + internal + pure + returns (Types.SuperRootProof memory proof) + { + _length = bound(_length, 1, MAX_OUTPUT_ROOTS); + + Types.OutputRootWithChainId[] memory outputRoots = new Types.OutputRootWithChainId[](_length); + for (uint256 i = 0; i < _length; i++) { + outputRoots[i] = _outputRoot( + uint256(keccak256(abi.encode(_seed, uint8(0), i))), keccak256(abi.encode(_seed, uint8(1), i)) + ); + } + + proof = Types.SuperRootProof({ version: SUPER_ROOT_VERSION, timestamp: _timestamp, outputRoots: outputRoots }); + } + + function _expectedSuperRootProofEncoding(Types.SuperRootProof memory _proof) + internal + pure + returns (bytes memory expected) + { + expected = bytes.concat(_proof.version, bytes8(_proof.timestamp)); + for (uint256 i = 0; i < _proof.outputRoots.length; i++) { + expected = bytes.concat(expected, bytes32(_proof.outputRoots[i].chainId), _proof.outputRoots[i].root); + } } } @@ -56,14 +88,14 @@ contract Encoding_EncodeDepositTransaction_Test is Encoding_TestInit { ) external { - Types.UserDepositTransaction memory t = Types.UserDepositTransaction( + Types.UserDepositTransaction memory depositTx = Types.UserDepositTransaction( _from, _to, isCreate, _value, _mint, _gas, _data, bytes32(uint256(0)), _logIndex ); - bytes memory txn = Encoding.encodeDepositTransaction(t); - bytes memory _txn = ffi.encodeDepositTransaction(t); + bytes memory actual = Encoding.encodeDepositTransaction(depositTx); + bytes memory expected = ffi.encodeDepositTransaction(depositTx); - assertEq(txn, _txn); + assertEq(actual, expected); } } @@ -82,24 +114,23 @@ contract Encoding_EncodeCrossDomainMessage_Test is Encoding_TestInit { ) external { - uint8 version = _version % 2; + uint8 version = uint8(bound(uint256(_version), 0, 1)); uint256 nonce = Encoding.encodeVersionedNonce(_nonce, version); - bytes memory encoding = Encoding.encodeCrossDomainMessage(nonce, _sender, _target, _value, _gasLimit, _data); - - bytes memory _encoding = ffi.encodeCrossDomainMessage(nonce, _sender, _target, _value, _gasLimit, _data); + bytes memory actual = Encoding.encodeCrossDomainMessage(nonce, _sender, _target, _value, _gasLimit, _data); + bytes memory expected = ffi.encodeCrossDomainMessage(nonce, _sender, _target, _value, _gasLimit, _data); - assertEq(encoding, _encoding); + assertEq(actual, expected); } /// @notice Tests that encodeCrossDomainMessage reverts if version is greater than 1. - function testFuzz_encodeCrossDomainMessage_versionGreaterThanOne_reverts(uint256 nonce) external { - // nonce >> 240 must be greater than 1 - uint256 minInvalidNonce = (uint256(type(uint240).max) + 1) * 2; - nonce = bound(nonce, minInvalidNonce, type(uint256).max); + function testFuzz_encodeCrossDomainMessage_versionGreaterThanOne_reverts(uint240 _nonce, uint16 _version) external { + uint16 invalidVersion = uint16(bound(uint256(_version), 2, type(uint16).max)); + uint256 nonce = Encoding.encodeVersionedNonce(_nonce, invalidVersion); + Encoding_Harness harness = new Encoding_Harness(); vm.expectRevert(bytes("Encoding: unknown cross domain message version")); - encoding.encodeCrossDomainMessage(nonce, address(this), address(this), 1, 100, hex""); + harness.encodeCrossDomainMessage(nonce, address(this), address(this), 1, 100, hex""); } } @@ -111,99 +142,33 @@ contract Encoding_EncodeSuperRootProof_Test is Encoding_TestInit { /// @param _length The number of output roots in the super root proof /// @param _seed The seed used to generate the output roots function testFuzz_encodeSuperRootProof_succeeds(uint64 _timestamp, uint256 _length, uint256 _seed) external pure { - // Ensure at least 1 element and cap at a reasonable maximum to avoid gas issues - _length = uint256(bound(_length, 1, 50)); - - // Create output roots array - Types.OutputRootWithChainId[] memory outputRoots = new Types.OutputRootWithChainId[](_length); - - // Generate deterministic chain IDs and roots based on the seed - for (uint256 i = 0; i < _length; i++) { - // Use different derivations of the seed for each value - uint256 chainId = uint256(keccak256(abi.encode(_seed, "chainId", i))); - bytes32 root = keccak256(abi.encode(_seed, "root", i)); - - outputRoots[i] = Types.OutputRootWithChainId({ chainId: chainId, root: root }); - } + Types.SuperRootProof memory proof = _superRootProof(_timestamp, _length, _seed); - // Create the super root proof - Types.SuperRootProof memory proof = - Types.SuperRootProof({ version: 0x01, timestamp: _timestamp, outputRoots: outputRoots }); - - // Encode the proof - bytes memory encoded = Encoding.encodeSuperRootProof(proof); - - // Verify encoding structure - assertEq(encoded[0], bytes1(0x01), "Version byte should be 0x01"); - - // Verify timestamp (bytes 1-8) - bytes8 encodedTimestamp; - for (uint256 i = 0; i < 8; i++) { - encodedTimestamp |= bytes8(encoded[i + 1]) >> (i * 8); - } - assertEq(uint64(encodedTimestamp), _timestamp, "Timestamp should match"); - - // Verify each chain ID and root is encoded correctly - uint256 offset = 9; // 1 byte version + 8 bytes timestamp - for (uint256 i = 0; i < _length; i++) { - // Extract chain ID (32 bytes) - uint256 encodedChainId; - assembly { - // Load 32 bytes from encoded at position offset - encodedChainId := mload(add(add(encoded, 32), offset)) - } - assertEq(encodedChainId, outputRoots[i].chainId, "Chain ID should match"); - offset += 32; - - // Extract root (32 bytes) - bytes32 encodedRoot; - assembly { - // Load 32 bytes from encoded at position offset - encodedRoot := mload(add(add(encoded, 32), offset)) - } - assertEq(encodedRoot, outputRoots[i].root, "Root should match"); - offset += 32; - } - - // Verify total length - assertEq(encoded.length, 9 + (_length * 64), "Encoded length should match expected"); + assertEq(Encoding.encodeSuperRootProof(proof), _expectedSuperRootProofEncoding(proof)); } /// @notice Tests encoding with a single output root function test_encodeSuperRootProof_singleOutputRoot_succeeds() external pure { - // Create a single output root Types.OutputRootWithChainId[] memory outputRoots = new Types.OutputRootWithChainId[](1); - outputRoots[0] = Types.OutputRootWithChainId({ chainId: 10, root: bytes32(uint256(0xdeadbeef)) }); + outputRoots[0] = _outputRoot(10, bytes32(uint256(0xdeadbeef))); - // Create the super root proof Types.SuperRootProof memory proof = - Types.SuperRootProof({ version: 0x01, timestamp: 1234567890, outputRoots: outputRoots }); + Types.SuperRootProof({ version: SUPER_ROOT_VERSION, timestamp: 1234567890, outputRoots: outputRoots }); - // Encode the proof - bytes memory encoded = Encoding.encodeSuperRootProof(proof); - - // Expected: 1 byte version + 8 bytes timestamp + (32 bytes chainId + 32 bytes root) - assertEq(encoded.length, 1 + 8 + 64, "Encoded length should be 73 bytes"); - assertEq(encoded[0], bytes1(0x01), "First byte should be version 0x01"); + assertEq(Encoding.encodeSuperRootProof(proof), _expectedSuperRootProofEncoding(proof)); } /// @notice Tests encoding with multiple output roots function test_encodeSuperRootProof_multipleOutputRoots_succeeds() external pure { - // Create multiple output roots Types.OutputRootWithChainId[] memory outputRoots = new Types.OutputRootWithChainId[](3); - outputRoots[0] = Types.OutputRootWithChainId({ chainId: 10, root: bytes32(uint256(0xdeadbeef)) }); - outputRoots[1] = Types.OutputRootWithChainId({ chainId: 20, root: bytes32(uint256(0xbeefcafe)) }); - outputRoots[2] = Types.OutputRootWithChainId({ chainId: 30, root: bytes32(uint256(0xcafebabe)) }); + outputRoots[0] = _outputRoot(10, bytes32(uint256(0xdeadbeef))); + outputRoots[1] = _outputRoot(20, bytes32(uint256(0xbeefcafe))); + outputRoots[2] = _outputRoot(30, bytes32(uint256(0xcafebabe))); - // Create the super root proof Types.SuperRootProof memory proof = - Types.SuperRootProof({ version: 0x01, timestamp: 1234567890, outputRoots: outputRoots }); - - // Encode the proof - bytes memory encoded = Encoding.encodeSuperRootProof(proof); + Types.SuperRootProof({ version: SUPER_ROOT_VERSION, timestamp: 1234567890, outputRoots: outputRoots }); - // Expected: 1 byte version + 8 bytes timestamp + 3 * (32 bytes chainId + 32 bytes root) - assertEq(encoded.length, 1 + 8 + (3 * 64), "Encoded length should be 201 bytes"); + assertEq(Encoding.encodeSuperRootProof(proof), _expectedSuperRootProofEncoding(proof)); } /// @notice Tests that the Solidity impl of encodeSuperRootProof matches the FFI impl @@ -211,70 +176,40 @@ contract Encoding_EncodeSuperRootProof_Test is Encoding_TestInit { /// @param _length The number of output roots in the super root proof /// @param _seed The seed used to generate the output roots function testDiff_encodeSuperRootProof_succeeds(uint64 _timestamp, uint256 _length, uint256 _seed) external { - // Ensure at least 1 element and cap at a reasonable maximum to avoid gas issues - _length = uint256(bound(_length, 1, 50)); + Types.SuperRootProof memory proof = _superRootProof(_timestamp, _length, _seed); - // Create output roots array - Types.OutputRootWithChainId[] memory outputRoots = new Types.OutputRootWithChainId[](_length); - - // Generate deterministic chain IDs and roots based on the seed - for (uint256 i = 0; i < _length; i++) { - // Use different derivations of the seed for each value - uint256 chainId = uint256(keccak256(abi.encode(_seed, "chainId", i))); - bytes32 root = keccak256(abi.encode(_seed, "root", i)); - - outputRoots[i] = Types.OutputRootWithChainId({ chainId: chainId, root: root }); - } - - // Create the super root proof - Types.SuperRootProof memory proof = - Types.SuperRootProof({ version: 0x01, timestamp: _timestamp, outputRoots: outputRoots }); - - // Encode using the Solidity implementation - bytes memory encoding1 = Encoding.encodeSuperRootProof(proof); - - // Encode using the FFI implementation - bytes memory encoding2 = ffi.encodeSuperRootProof(proof); - - // Compare the results - assertEq(encoding1, encoding2, "Solidity and FFI implementations should match"); + assertEq(Encoding.encodeSuperRootProof(proof), ffi.encodeSuperRootProof(proof)); } /// @notice Tests that encoding fails when version is not 0x01 /// @param _version The version to use for the super root proof /// @param _timestamp The timestamp of the super root proof function testFuzz_encodeSuperRootProof_invalidVersion_reverts(bytes1 _version, uint64 _timestamp) external { - // Ensure version is not 0x01 - if (_version == 0x01) { + if (_version == SUPER_ROOT_VERSION) { _version = 0x02; } - // Create a minimal valid output roots array Types.OutputRootWithChainId[] memory outputRoots = new Types.OutputRootWithChainId[](1); - outputRoots[0] = Types.OutputRootWithChainId({ chainId: 1, root: bytes32(uint256(1)) }); + outputRoots[0] = _outputRoot(1, bytes32(uint256(1))); - // Create the super root proof with invalid version Types.SuperRootProof memory proof = Types.SuperRootProof({ version: _version, timestamp: _timestamp, outputRoots: outputRoots }); + Encoding_Harness harness = new Encoding_Harness(); - // Expect revert when encoding vm.expectRevert(Encoding.Encoding_InvalidSuperRootVersion.selector); - encoding.encodeSuperRootProof(proof); + harness.encodeSuperRootProof(proof); } /// @notice Tests that encoding fails when output roots array is empty /// @param _timestamp The timestamp of the super root proof function testFuzz_encodeSuperRootProof_emptyOutputRoots_reverts(uint64 _timestamp) external { - // Create an empty output roots array Types.OutputRootWithChainId[] memory outputRoots = new Types.OutputRootWithChainId[](0); - - // Create the super root proof with empty output roots Types.SuperRootProof memory proof = - Types.SuperRootProof({ version: 0x01, timestamp: _timestamp, outputRoots: outputRoots }); + Types.SuperRootProof({ version: SUPER_ROOT_VERSION, timestamp: _timestamp, outputRoots: outputRoots }); + Encoding_Harness harness = new Encoding_Harness(); - // Expect revert when encoding vm.expectRevert(Encoding.Encoding_EmptySuperRoot.selector); - encoding.encodeSuperRootProof(proof); + harness.encodeSuperRootProof(proof); } } diff --git a/test/libraries/Hashing.t.sol b/test/libraries/Hashing.t.sol index dfb61e7a..f02a8da9 100644 --- a/test/libraries/Hashing.t.sol +++ b/test/libraries/Hashing.t.sol @@ -32,6 +32,36 @@ contract Hashing_Harness { } } +abstract contract Hashing_TestInit is CommonTest { + bytes1 internal constant SUPER_ROOT_VERSION = 0x01; + uint256 internal constant MAX_OUTPUT_ROOTS = 50; + + function _outputRoot(uint256 _chainId, bytes32 _root) internal pure returns (Types.OutputRootWithChainId memory) { + return Types.OutputRootWithChainId({ chainId: _chainId, root: _root }); + } + + function _superRootProof( + uint64 _timestamp, + uint256 _length, + uint256 _seed + ) + internal + pure + returns (Types.SuperRootProof memory proof) + { + _length = bound(_length, 1, MAX_OUTPUT_ROOTS); + + Types.OutputRootWithChainId[] memory outputRoots = new Types.OutputRootWithChainId[](_length); + for (uint256 i = 0; i < _length; i++) { + outputRoots[i] = _outputRoot( + uint256(keccak256(abi.encode(_seed, uint8(0), i))), keccak256(abi.encode(_seed, uint8(1), i)) + ); + } + + proof = Types.SuperRootProof({ version: SUPER_ROOT_VERSION, timestamp: _timestamp, outputRoots: outputRoots }); + } +} + /// @title Hashing_hashDepositTransaction_Test /// @notice Tests the `hashDepositTransaction` function of the `Hashing` library. contract Hashing_hashDepositTransaction_Test is CommonTest { @@ -57,7 +87,7 @@ contract Hashing_hashDepositTransaction_Test is CommonTest { _mint, _gas, _data, - bytes32(uint256(0)), + bytes32(0), _logIndex ) ), @@ -90,15 +120,7 @@ contract Hashing_hashDepositSource_Test is CommonTest { /// @title Hashing_hashCrossDomainMessage_Test /// @notice Tests the `hashCrossDomainMessage` function of the `Hashing` library. contract Hashing_hashCrossDomainMessage_Test is CommonTest { - Hashing_Harness internal harness; - - /// @notice Sets up the test. - function setUp() public override { - super.setUp(); - harness = new Hashing_Harness(); - } /// @notice Tests that hashCrossDomainMessage returns the correct hash in a simple case. - function testDiff_hashCrossDomainMessage_succeeds( uint240 _nonce, uint16 _version, @@ -110,7 +132,6 @@ contract Hashing_hashCrossDomainMessage_Test is CommonTest { ) external { - // Ensure the version is valid. uint16 version = uint16(bound(uint256(_version), 0, 1)); uint256 nonce = Encoding.encodeVersionedNonce(_nonce, version); @@ -123,28 +144,13 @@ contract Hashing_hashCrossDomainMessage_Test is CommonTest { /// @notice Tests that hashCrossDomainMessage reverts with unknown version. /// @param _nonce Message nonce base value. /// @param _version Invalid version number (will be bounded to 2+). - /// @param _sender Address of the sender of the message. - /// @param _target Address of the target of the message. - /// @param _value ETH value to send to the target. - /// @param _gasLimit Gas limit to use for the message. - /// @param _data Data to send with the message. - function testFuzz_hashCrossDomainMessage_unknownVersion_reverts( - uint240 _nonce, - uint16 _version, - address _sender, - address _target, - uint256 _value, - uint256 _gasLimit, - bytes memory _data - ) - external - { - // Use version 2 or higher (invalid versions) + function testFuzz_hashCrossDomainMessage_unknownVersion_reverts(uint240 _nonce, uint16 _version) external { uint16 invalidVersion = uint16(bound(uint256(_version), 2, type(uint16).max)); uint256 nonce = Encoding.encodeVersionedNonce(_nonce, invalidVersion); + Hashing_Harness harness = new Hashing_Harness(); vm.expectRevert(bytes("Hashing: unknown cross domain message version")); - harness.hashCrossDomainMessage(nonce, _sender, _target, _value, _gasLimit, _data); + harness.hashCrossDomainMessage(nonce, address(this), address(this), 1, 100, hex""); } } @@ -225,49 +231,33 @@ contract Hashing_hashL2toL2CrossDomainMessage_Test is CommonTest { /// @title Hashing_hashSuperRootProof_Test /// @notice Tests the `hashSuperRootProof` function of the `Hashing` library. -contract Hashing_hashSuperRootProof_Test is CommonTest { - Hashing_Harness internal harness; - - /// @notice Sets up the test. - function setUp() public override { - super.setUp(); - harness = new Hashing_Harness(); - } - +contract Hashing_hashSuperRootProof_Test is Hashing_TestInit { /// @notice Tests that the Solidity impl of hashSuperRootProof matches the FFI impl - /// @param _proof The super root proof to test. - function testDiff_hashSuperRootProof_succeeds(Types.SuperRootProof memory _proof) external { - // Make sure the proof has the right version. - _proof.version = 0x01; - - // Make sure the proof has at least one output root. - if (_proof.outputRoots.length == 0) { - _proof.outputRoots = new Types.OutputRootWithChainId[](1); - _proof.outputRoots[0] = Types.OutputRootWithChainId({ - chainId: vm.randomUint(0, type(uint64).max), root: bytes32(vm.randomUint()) - }); - } + /// @param _timestamp The timestamp of the super root proof. + /// @param _length The number of output roots in the super root proof. + /// @param _seed The seed used to generate the output roots. + function testDiff_hashSuperRootProof_succeeds(uint64 _timestamp, uint256 _length, uint256 _seed) external { + Types.SuperRootProof memory proof = _superRootProof(_timestamp, _length, _seed); - // Encode using the Solidity implementation - bytes32 hash1 = harness.hashSuperRootProof(_proof); - - // Encode using the FFI implementation - bytes32 hash2 = ffi.hashSuperRootProof(_proof); - - // Compare the results - assertEq(hash1, hash2, "Solidity and FFI implementations should match"); + assertEq(Hashing.hashSuperRootProof(proof), ffi.hashSuperRootProof(proof), "hash mismatch"); } /// @notice Tests that hashSuperRootProof reverts when the version is incorrect. - /// @param _proof The super root proof to test. - function testFuzz_hashSuperRootProof_wrongVersion_reverts(Types.SuperRootProof memory _proof) external { - // 0x01 is the correct version, so we need any other version. - if (_proof.version == 0x01) { - _proof.version = 0x00; + /// @param _version The version to use for the super root proof. + /// @param _timestamp The timestamp of the super root proof. + function testFuzz_hashSuperRootProof_wrongVersion_reverts(bytes1 _version, uint64 _timestamp) external { + if (_version == SUPER_ROOT_VERSION) { + _version = 0x00; } - // Should always revert when the version is incorrect. + Types.OutputRootWithChainId[] memory outputRoots = new Types.OutputRootWithChainId[](1); + outputRoots[0] = _outputRoot(1, bytes32(uint256(1))); + + Types.SuperRootProof memory proof = + Types.SuperRootProof({ version: _version, timestamp: _timestamp, outputRoots: outputRoots }); + Hashing_Harness harness = new Hashing_Harness(); + vm.expectRevert(Encoding.Encoding_InvalidSuperRootVersion.selector); - harness.hashSuperRootProof(_proof); + harness.hashSuperRootProof(proof); } } diff --git a/test/libraries/Predeploys.t.sol b/test/libraries/Predeploys.t.sol index 1a9e7254..dfa5083f 100644 --- a/test/libraries/Predeploys.t.sol +++ b/test/libraries/Predeploys.t.sol @@ -22,11 +22,6 @@ abstract contract Predeploys_TestInit is CommonTest { return _addr == Predeploys.L1_BLOCK_ATTRIBUTES || _addr == Predeploys.L2_STANDARD_BRIDGE; } - /// @notice Returns true if the account is not meant to be in the L2 genesis anymore. - function _isOmitted(address _addr) internal pure returns (bool) { - return _addr == Predeploys.L1_MESSAGE_SENDER; - } - /// @notice Returns true if the predeploy is initializable and uses OpenZeppelin v4 storage pattern. /// These contracts have _initialized in the regular storage layout. function _isInitializableV4(address _addr) internal pure returns (bool) { @@ -48,26 +43,20 @@ abstract contract Predeploys_TestInit is CommonTest { /// @notice Internal test function for predeploys validation across different forks. function _test_predeploys() internal view { - uint256 count = 2048; uint160 prefix = uint160(0x420) << 148; bytes memory proxyCode = vm.getDeployedCode("src/universal/Proxy.sol:Proxy"); - for (uint256 i = 0; i < count; i++) { + for (uint256 i = 0; i < Predeploys.PREDEPLOY_COUNT; i++) { address addr = address(prefix | uint160(i)); - address implAddr = Predeploys.predeployToCodeNamespace(addr); - if (_isOmitted(addr)) { - assertEq(implAddr.code.length, 0, "must have no code"); + if (addr == Predeploys.L1_MESSAGE_SENDER) { + assertEq(Predeploys.predeployToCodeNamespace(addr).code.length, 0, "must have no code"); continue; } bool isPredeploy = Predeploys.isSupportedPredeploy(addr); - - bytes memory code = addr.code; - if (isPredeploy) assertTrue(code.length > 0); - - bool proxied = Predeploys.notProxied(addr) == false; + bool proxied = !Predeploys.notProxied(addr); if (!isPredeploy) { // All of the predeploys, even if inactive, have their admin set to the proxy admin @@ -75,13 +64,17 @@ abstract contract Predeploys_TestInit is CommonTest { continue; } + address implAddr = Predeploys.predeployToCodeNamespace(addr); + bytes memory code = addr.code; + assertTrue(code.length > 0); + string memory cname = Predeploys.getName(addr); assertNotEq(cname, "", "must have a name"); bytes memory supposedCode = vm.getDeployedCode(string.concat(cname, ".sol:", cname)); assertNotEq(supposedCode.length, 0, "must have supposed code"); - if (proxied == false) { + if (!proxied) { // can't check bytecode if it's modified with immutables in genesis. if (!_usesImmutables(addr)) { assertEq(code, supposedCode, "non-proxy contract should be deployed in-place"); diff --git a/test/libraries/Preinstalls.t.sol b/test/libraries/Preinstalls.t.sol index 2a2592db..e5940b34 100644 --- a/test/libraries/Preinstalls.t.sol +++ b/test/libraries/Preinstalls.t.sol @@ -9,39 +9,54 @@ import { IEIP712 } from "interfaces/universal/IEIP712.sol"; /// @title Preinstalls_TestInit /// @notice Reusable test initialization for `Preinstalls` tests. abstract contract Preinstalls_TestInit is CommonTest { + uint256 internal constant PERMIT2_CHAIN_ID_OFFSET = 6945; + uint256 internal constant PERMIT2_DOMAIN_SEPARATOR_OFFSET = 6983; + uint256 internal constant PERMIT2_DEPLOYMENT_CHAIN_ID = 901; + function assertPreinstall(address _addr, bytes memory _code) internal view { + assertPreinstall(_addr, _code, block.chainid); + } + + function assertPreinstall(address _addr, bytes memory _code, uint256 _chainId) internal view { + bytes memory deployedCode = _addr.code; + assertNotEq(_code.length, 0, "must have code"); - assertNotEq(_addr.code.length, 0, "deployed preinstall account must have code"); - assertEq(_addr.code, _code, "equal code must be deployed"); - assertEq(Preinstalls.getDeployedCode(_addr, block.chainid), _code, "deployed-code getter must match"); + assertNotEq(deployedCode.length, 0, "deployed preinstall account must have code"); + assertEq(deployedCode, _code, "equal code must be deployed"); + assertEq(Preinstalls.getDeployedCode(_addr, _chainId), _code, "deployed-code getter must match"); assertNotEq(Preinstalls.getName(_addr), "", "must have a name"); if (_addr != Preinstalls.DeterministicDeploymentProxy) { assertEq(vm.getNonce(_addr), 1, "preinstall account must have 1 nonce"); } } + + function assertPermit2Code( + uint256 _chainId, + bytes32 _expectedDomainSeparator + ) + internal + pure + returns (bytes memory code_) + { + code_ = Preinstalls.getPermit2Code(_chainId); + assertNotEq(code_.length, 0, "must have code"); + assertEq(uint256(bytes32(Bytes.slice(code_, PERMIT2_CHAIN_ID_OFFSET, 32))), _chainId, "expecting chain ID"); + assertEq( + bytes32(Bytes.slice(code_, PERMIT2_DOMAIN_SEPARATOR_OFFSET, 32)), + _expectedDomainSeparator, + "expecting domain separator" + ); + } } /// @title Preinstalls_GetPermit2Code_Test /// @notice Tests the `getPermit2Code` function of the `Preinstalls` library. contract Preinstalls_GetPermit2Code_Test is Preinstalls_TestInit { function test_getPermit2Code_templating_works() external pure { - bytes memory customCode = Preinstalls.getPermit2Code(1234); - assertNotEq(customCode.length, 0, "must have code"); - assertEq(uint256(bytes32(Bytes.slice(customCode, 6945, 32))), uint256(1234), "expecting custom chain ID"); - assertEq( - bytes32(Bytes.slice(customCode, 6983, 32)), - bytes32(0x6cda538cafce36292a6ef27740629597f85f6716f5694d26d5c59fc1d07cfd95), - "expecting custom domain separator" - ); + assertPermit2Code(1234, bytes32(0x6cda538cafce36292a6ef27740629597f85f6716f5694d26d5c59fc1d07cfd95)); - bytes memory defaultCode = Preinstalls.getPermit2Code(1); - assertNotEq(defaultCode.length, 0, "must have code"); - assertEq(uint256(bytes32(Bytes.slice(defaultCode, 6945, 32))), uint256(1), "expecting default chain ID"); - assertEq( - bytes32(Bytes.slice(defaultCode, 6983, 32)), - bytes32(0x866a5aba21966af95d6c7ab78eb2b2fc913915c28be3b9aa07cc04ff903e3f28), - "expecting default domain separator" - ); + bytes memory defaultCode = + assertPermit2Code(1, bytes32(0x866a5aba21966af95d6c7ab78eb2b2fc913915c28be3b9aa07cc04ff903e3f28)); assertEq(defaultCode, Preinstalls.Permit2TemplateCode, "template is using chain ID 1"); } } @@ -57,10 +72,9 @@ contract Preinstalls_Uncategorized_Test is Preinstalls_TestInit { keccak256(abi.encodePacked("EIP712Domain(string name,uint256 chainId,address verifyingContract)")); bytes32 nameHash = keccak256(abi.encodePacked("Permit2")); uint256 chainId = block.chainid; - bytes memory encoded = abi.encode(typeHash, nameHash, chainId, Preinstalls.Permit2); - bytes32 expectedDomainSeparator = keccak256(encoded); + bytes32 expectedDomainSeparator = keccak256(abi.encode(typeHash, nameHash, chainId, Preinstalls.Permit2)); assertEq(domainSeparator, expectedDomainSeparator, "Domain separator mismatch"); - assertEq(chainId, uint256(901)); // uses devnet config + assertEq(chainId, PERMIT2_DEPLOYMENT_CHAIN_ID); // uses devnet config assertEq(domainSeparator, bytes32(0x48deb34b39fb4b41f5c195008940d5ef510cdd7853eba5807b2fa08dfd586475)); // Warning the Permit2 domain separator as cached in the DeployPermit2.sol bytecode is // incorrect. @@ -98,11 +112,10 @@ contract Preinstalls_Uncategorized_Test is Preinstalls_TestInit { assertPreinstall(Preinstalls.MultiSend_v130, Preinstalls.MultiSend_v130Code); } - function test_preinstall_permit2_succeeds() external { - uint256 pre = block.chainid; - vm.chainId(901); // TODO legacy deployment does not use same chainID as tests run with - assertPreinstall(Preinstalls.Permit2, Preinstalls.getPermit2Code(block.chainid)); - vm.chainId(pre); + function test_preinstall_permit2_succeeds() external view { + assertPreinstall( + Preinstalls.Permit2, Preinstalls.getPermit2Code(PERMIT2_DEPLOYMENT_CHAIN_ID), PERMIT2_DEPLOYMENT_CHAIN_ID + ); } function test_preinstall_senderCreatorv060_succeeds() external view { diff --git a/test/libraries/SafeCall.t.sol b/test/libraries/SafeCall.t.sol index c76bfdd6..f94d7ac0 100644 --- a/test/libraries/SafeCall.t.sol +++ b/test/libraries/SafeCall.t.sol @@ -32,16 +32,26 @@ contract SimpleSafeCaller { /// @title SafeCall_TestInit /// @notice Reusable test initialization for `SafeCall` tests. abstract contract SafeCall_TestInit is Test { - /// @notice Helper function to deduplicate code. Makes all assumptions required for these - /// tests. + struct CallBalances { + uint256 from; + uint256 to; + } + + /// @notice Makes all assumptions required for these tests. function assumeNot(address _addr) internal { - vm.deal(_addr, 0); vm.assume(_addr != address(this)); assumeAddressIsNot(_addr, StdCheatsSafe.AddressType.ForgeAddress, StdCheatsSafe.AddressType.Precompile); + vm.deal(_addr, 0); } - /// @notice Internal helper function for `send` tests - function sendTest(address _from, address _to, uint64 _gas, uint256 _value) internal { + function prepareCall( + address _from, + address _to, + uint256 _value + ) + internal + returns (CallBalances memory balancesBefore) + { assumeNot(_from); assumeNot(_to); @@ -49,7 +59,29 @@ abstract contract SafeCall_TestInit is Test { vm.deal(_from, _value); assertEq(_from.balance, _value, "from balance not dealt"); - uint256[2] memory balancesBefore = [_from.balance, _to.balance]; + balancesBefore = CallBalances({ from: _from.balance, to: _to.balance }); + } + + function assertCallBalances( + address _from, + address _to, + uint256 _value, + CallBalances memory _balancesBefore + ) + internal + view + { + if (_from == _to) { + assertEq(_from.balance, _balancesBefore.from, "self-transfer did not preserve balance"); + } else { + assertEq(_from.balance, _balancesBefore.from - _value, "from balance not drained"); + assertEq(_to.balance, _balancesBefore.to + _value, "to balance received"); + } + } + + /// @notice Internal helper function for `send` tests + function sendTest(address _from, address _to, uint64 _gas, uint256 _value) internal { + CallBalances memory balancesBefore = prepareCall(_from, _to, _value); vm.expectCall(_to, _value, bytes("")); vm.prank(_from); @@ -61,12 +93,7 @@ abstract contract SafeCall_TestInit is Test { } assertTrue(success, "send not successful"); - if (_from == _to) { - assertEq(_from.balance, balancesBefore[0], "Self-send did not change balance"); - } else { - assertEq(_from.balance, balancesBefore[0] - _value, "from balance not drained"); - assertEq(_to.balance, balancesBefore[1] + _value, "to balance received"); - } + assertCallBalances(_from, _to, _value, balancesBefore); } } @@ -90,26 +117,14 @@ contract SafeCall_Send_Test is SafeCall_TestInit { contract SafeCall_Call_Test is SafeCall_TestInit { /// @notice Tests that `call` succeeds. function testFuzz_call_succeeds(address from, address to, uint256 gas, uint64 value, bytes memory data) external { - assumeNot(from); - assumeNot(to); - - assertEq(from.balance, 0, "from balance is 0"); - vm.deal(from, value); - assertEq(from.balance, value, "from balance not dealt"); - - uint256[2] memory balancesBefore = [from.balance, to.balance]; + CallBalances memory balancesBefore = prepareCall(from, to, value); vm.expectCall(to, value, data); vm.prank(from); bool success = SafeCall.call(to, gas, value, data); assertTrue(success, "call not successful"); - if (from == to) { - assertEq(from.balance, balancesBefore[0], "Self-send did not change balance"); - } else { - assertEq(from.balance, balancesBefore[0] - value, "from balance not drained"); - assertEq(to.balance, balancesBefore[1] + value, "to balance received"); - } + assertCallBalances(from, to, value, balancesBefore); } } @@ -126,110 +141,80 @@ contract SafeCall_CallWithMinGas_Test is SafeCall_TestInit { ) external { - assumeNot(from); - assumeNot(to); - - assertEq(from.balance, 0, "from balance is 0"); - vm.deal(from, value); - assertEq(from.balance, value, "from balance not dealt"); + CallBalances memory balancesBefore = prepareCall(from, to, value); // Bound minGas to [0, l1_block_gas_limit] minGas = uint64(bound(minGas, 0, 30_000_000)); - uint256[2] memory balancesBefore = [from.balance, to.balance]; - vm.expectCallMinGas(to, value, minGas, data); vm.prank(from); bool success = SafeCall.callWithMinGas(to, minGas, value, data); assertTrue(success, "call not successful"); - if (from == to) { - assertEq(from.balance, balancesBefore[0], "Self-send did not change balance"); - } else { - assertEq(from.balance, balancesBefore[0] - value, "from balance not drained"); - assertEq(to.balance, balancesBefore[1] + value, "to balance received"); - } + assertCallBalances(from, to, value, balancesBefore); } /// @notice Tests that `callWithMinGas` succeeds for the lower gas bounds. function test_callWithMinGas_noLeakageLow_succeeds() external { SimpleSafeCaller caller = new SimpleSafeCaller(); - for (uint64 i = 40_000; i < 100_000; i++) { - uint256 snapshot = vm.snapshot(); - - // The values below are best gotten by setting the value to a high number and running - // the test with a verbosity of `-vvv` then setting the value to the value (gas arg) of - // the failed assertion. A faster way to do this for forge coverage cases, is to - // comment out the optimizer and optimizer runs in the foundry.toml file and then run - // forge test. This is faster because forge test only compiles modified contracts - // unlike forge coverage. - uint256 expected; - - // Because forge coverage always runs with the optimizer disabled, if forge coverage is - // run before testing this with forge test or forge snapshot, forge clean should be run - // first so that it recompiles the contracts using the foundry.toml optimizer settings. - if (vm.isContext(VmSafe.ForgeContext.Coverage) || LibString.eq(Config.foundryProfile(), "lite")) { - // 66_290 is the exact amount of gas required to make the safe call - // successfully with the optimizer disabled (ran via forge coverage) - expected = 66_290; - } else if (vm.isContext(VmSafe.ForgeContext.Test) || vm.isContext(VmSafe.ForgeContext.Snapshot)) { - // 65_922 is the exact amount of gas required to make the safe call - // successfully with the foundry.toml optimizer settings. - expected = 65_922; - } else { - revert("SafeCall_Test: unknown context"); - } - - if (i < expected) { - assertFalse(caller.makeSafeCall(i, 25_000)); - } else { - vm.expectCallMinGas(address(caller), 0, 25_000, abi.encodeCall(caller.setA, (1))); - assertTrue(caller.makeSafeCall(i, 25_000)); - } - - assertTrue(vm.revertTo(snapshot)); - } + checkNoGasLeakage({ + _caller: caller, + _startGas: 40_000, + _endGas: 100_000, + _minGas: 25_000, + _expected: expectedSafeCallGas({ _coverageOrLiteGas: 66_290, _testGas: 65_922 }), + _setACall: abi.encodeCall(caller.setA, (1)) + }); } /// @notice Tests that `callWithMinGas` succeeds on the upper gas bounds. function test_callWithMinGas_noLeakageHigh_succeeds() external { SimpleSafeCaller caller = new SimpleSafeCaller(); - for (uint64 i = 15_200_000; i < 15_300_000; i++) { - uint256 snapshot = vm.snapshot(); - - // The values below are best gotten by setting the value to a high number and running - // the test with a verbosity of `-vvv` then setting the value to the value (gas arg) of - // the failed assertion. A faster way to do this for forge coverage cases, is to - // comment out the optimizer and optimizer runs in the foundry.toml file and then run - // forge test. This is faster because forge test only compiles modified contracts - // unlike forge coverage. - uint256 expected; - - // Because forge coverage always runs with the optimizer disabled, if forge coverage is - // run before testing this with forge test or forge snapshot, forge clean should be run - // first so that it recompiles the contracts using the foundry.toml optimizer settings. - if (vm.isContext(VmSafe.ForgeContext.Coverage) || LibString.eq(Config.foundryProfile(), "lite")) { - // 15_278_989 is the exact amount of gas required to make the safe call - // successfully with the optimizer disabled (ran via forge coverage) - expected = 15_278_989; - } else if (vm.isContext(VmSafe.ForgeContext.Test) || vm.isContext(VmSafe.ForgeContext.Snapshot)) { - // 15_278_621 is the exact amount of gas required to make the safe call - // successfully with the foundry.toml optimizer settings. - expected = 15_278_621; - } else { - revert("SafeCall_Test: unknown context"); - } + checkNoGasLeakage({ + _caller: caller, + _startGas: 15_200_000, + _endGas: 15_300_000, + _minGas: 15_000_000, + _expected: expectedSafeCallGas({ _coverageOrLiteGas: 15_278_989, _testGas: 15_278_621 }), + _setACall: abi.encodeCall(caller.setA, (1)) + }); + } + + /// @notice Returns the gas threshold calibrated for the current forge context. + /// @dev Thresholds are calibrated from the failing gas arg when running these tests with `-vvv`. + function expectedSafeCallGas(uint256 _coverageOrLiteGas, uint256 _testGas) internal view returns (uint256) { + if (vm.isContext(VmSafe.ForgeContext.Coverage) || LibString.eq(Config.foundryProfile(), "lite")) { + return _coverageOrLiteGas; + } else if (vm.isContext(VmSafe.ForgeContext.Test) || vm.isContext(VmSafe.ForgeContext.Snapshot)) { + return _testGas; + } else { + revert("SafeCall_Test: unknown context"); + } + } + + function checkNoGasLeakage( + SimpleSafeCaller _caller, + uint64 _startGas, + uint64 _endGas, + uint64 _minGas, + uint256 _expected, + bytes memory _setACall + ) + internal + { + for (uint64 i = _startGas; i < _endGas; i++) { + uint256 snapshot = vm.snapshotState(); - if (i < expected) { - assertFalse(caller.makeSafeCall(i, 15_000_000)); + if (i < _expected) { + assertFalse(_caller.makeSafeCall(i, _minGas)); } else { - vm.expectCallMinGas(address(caller), 0, 15_000_000, abi.encodeCall(caller.setA, (1))); - assertTrue(caller.makeSafeCall(i, 15_000_000)); + vm.expectCallMinGas(address(_caller), 0, _minGas, _setACall); + assertTrue(_caller.makeSafeCall(i, _minGas)); } - assertTrue(vm.revertTo(snapshot)); + assertTrue(vm.revertToState(snapshot)); } } } diff --git a/test/libraries/SemverComp.t.sol b/test/libraries/SemverComp.t.sol index 70ab7db6..79d91466 100644 --- a/test/libraries/SemverComp.t.sol +++ b/test/libraries/SemverComp.t.sol @@ -1,131 +1,88 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -// Forge import { Test } from "lib/forge-std/src/Test.sol"; -// Libraries import { JSONParserLib } from "lib/solady/src/utils/JSONParserLib.sol"; import { SemverComp } from "src/libraries/SemverComp.sol"; -/// @title SemverComp_Harness -/// @notice Exposes internal functions of `SemverComp` for testing. contract SemverComp_Harness { - /// @notice Parses a semver string into a Semver struct. This is a wrapper around - /// `SemverComp.parse` that returns the major, minor, and patch components as - /// separate values. - /// @param _semver The semver string to parse. - /// @return major_ The major version. - /// @return minor_ The minor version. - /// @return patch_ The patch version. function parse(string memory _semver) external pure returns (uint256 major_, uint256 minor_, uint256 patch_) { SemverComp.Semver memory v = SemverComp.parse(_semver); return (v.major, v.minor, v.patch); } } -/// @title SemverComp_TestInit -/// @notice Reusable test initialization for `SemverComp` tests. abstract contract SemverComp_TestInit is Test { SemverComp_Harness internal harness; - /// @notice Sets up the test environment. function setUp() public { harness = new SemverComp_Harness(); } - /// @notice Asserts that the parsed semver components match the expected values. - /// @param _semver The semver string to parse. - /// @param _major The expected major version. - /// @param _minor The expected minor version. - /// @param _patch The expected patch version. function assertParsedEq(string memory _semver, uint256 _major, uint256 _minor, uint256 _patch) internal view { (uint256 major, uint256 minor, uint256 patch) = harness.parse(_semver); assertEq(major, _major, "major mismatch"); assertEq(minor, _minor, "minor mismatch"); assertEq(patch, _patch, "patch mismatch"); } + + function assertParseReverts(string memory _semver, bytes4 _selector) internal { + vm.expectRevert(_selector); + harness.parse(_semver); + } } -/// @title SemverComp_parse_Test -/// @notice Tests the `parse` function behavior. contract SemverComp_parse_Test is SemverComp_TestInit { - /// @notice Parses the minimal version. function test_parse_basicZero_succeeds() external view { assertParsedEq("0.0.0", 0, 0, 0); } - /// @notice Parses a standard version. function test_parse_basic123_succeeds() external view { assertParsedEq("1.2.3", 1, 2, 3); } - /// @notice Ignores prerelease identifiers. function test_parse_withPrerelease_succeeds() external view { assertParsedEq("1.2.3-alpha", 1, 2, 3); assertParsedEq("1.2.3-alpha.1", 1, 2, 3); assertParsedEq("10.20.30-rc.1", 10, 20, 30); } - /// @notice Ignores build metadata. function test_parse_withBuildMetadataOnly_succeeds() external view { assertParsedEq("1.2.3+build.5", 1, 2, 3); assertParsedEq("1.2.3+20240101", 1, 2, 3); } - /// @notice Ignores prerelease and build metadata together. function test_parse_withPrereleaseAndBuild_succeeds() external view { assertParsedEq("1.2.3-rc.1+build.5", 1, 2, 3); assertParsedEq("2.0.0-beta+exp.sha.5114f85", 2, 0, 0); } - /// @notice Reverts when fewer than 3 dot-separated core parts are present. function test_parse_lessThanThreeParts_reverts() external { - vm.expectRevert(SemverComp.SemverComp_InvalidSemverParts.selector); - harness.parse("1.2"); - - vm.expectRevert(SemverComp.SemverComp_InvalidSemverParts.selector); - harness.parse("1"); - - vm.expectRevert(SemverComp.SemverComp_InvalidSemverParts.selector); - harness.parse(""); + assertParseReverts("1.2", SemverComp.SemverComp_InvalidSemverParts.selector); + assertParseReverts("1", SemverComp.SemverComp_InvalidSemverParts.selector); + assertParseReverts("", SemverComp.SemverComp_InvalidSemverParts.selector); } - /// @notice Current behavior: extra dot-components beyond the core 3 are ignored. function test_parse_extraDotComponents_succeeds() external view { assertParsedEq("1.2.3.4", 1, 2, 3); assertParsedEq("1.2.3.4.5", 1, 2, 3); } - /// @notice Reverts on non-numeric core parts. function test_parse_nonNumeric_reverts() external { - vm.expectRevert(JSONParserLib.ParsingFailed.selector); - harness.parse("a.b.c"); - - vm.expectRevert(JSONParserLib.ParsingFailed.selector); - harness.parse("1.b.3"); - - vm.expectRevert(JSONParserLib.ParsingFailed.selector); - harness.parse("1.2.c"); + assertParseReverts("a.b.c", JSONParserLib.ParsingFailed.selector); + assertParseReverts("1.b.3", JSONParserLib.ParsingFailed.selector); + assertParseReverts("1.2.c", JSONParserLib.ParsingFailed.selector); } - /// @notice Reverts on certain commonly malformed inputs. function test_parse_malformedInputs_reverts() external { - // Leading/trailing whitespace - vm.expectRevert(JSONParserLib.ParsingFailed.selector); - harness.parse(" 1.2.3"); - vm.expectRevert(JSONParserLib.ParsingFailed.selector); - harness.parse("1.2.3 "); - - // "v" prefix - vm.expectRevert(JSONParserLib.ParsingFailed.selector); - harness.parse("v1.2.3"); + assertParseReverts(" 1.2.3", JSONParserLib.ParsingFailed.selector); + assertParseReverts("1.2.3 ", JSONParserLib.ParsingFailed.selector); + assertParseReverts("v1.2.3", JSONParserLib.ParsingFailed.selector); } } -/// @title SemverComp_Eq_Test -/// @notice Tests the `eq` function behavior. -contract SemverComp_Eq_Test is SemverComp_TestInit { +contract SemverComp_Eq_Test is Test { function test_eq_succeeds() external pure { assertTrue(SemverComp.eq("1.2.3", "1.2.3")); @@ -135,9 +92,7 @@ contract SemverComp_Eq_Test is SemverComp_TestInit { } } -/// @title SemverComp_Lt_Test -/// @notice Tests the `lt` function behavior. -contract SemverComp_Lt_Test is SemverComp_TestInit { +contract SemverComp_Lt_Test is Test { function test_lt_succeeds() external pure { assertTrue(SemverComp.lt("1.2.3", "1.2.4")); assertTrue(SemverComp.lt("1.2.3", "1.3.0")); @@ -149,9 +104,7 @@ contract SemverComp_Lt_Test is SemverComp_TestInit { } } -/// @title SemverComp_Lte_Test -/// @notice Tests the `lte` function behavior. -contract SemverComp_Lte_Test is SemverComp_TestInit { +contract SemverComp_Lte_Test is Test { function test_lte_succeeds() external pure { assertTrue(SemverComp.lte("1.2.3", "1.2.3")); assertTrue(SemverComp.lte("1.2.3", "1.2.4")); @@ -163,9 +116,7 @@ contract SemverComp_Lte_Test is SemverComp_TestInit { } } -/// @title SemverComp_Gt_Test -/// @notice Tests the `gt` function behavior. -contract SemverComp_Gt_Test is SemverComp_TestInit { +contract SemverComp_Gt_Test is Test { function test_gt_succeeds() external pure { assertTrue(SemverComp.gt("1.2.4", "1.2.3")); assertTrue(SemverComp.gt("1.3.0", "1.2.3")); @@ -177,9 +128,7 @@ contract SemverComp_Gt_Test is SemverComp_TestInit { } } -/// @title SemverComp_Gte_Test -/// @notice Tests the `gte` function behavior. -contract SemverComp_Gte_Test is SemverComp_TestInit { +contract SemverComp_Gte_Test is Test { function test_gte_succeeds() external pure { assertTrue(SemverComp.gte("1.2.3", "1.2.3")); assertTrue(SemverComp.gte("1.2.4", "1.2.3")); diff --git a/test/libraries/bridge/LibGameId.t.sol b/test/libraries/bridge/LibGameId.t.sol index 02f78005..0dad1483 100644 --- a/test/libraries/bridge/LibGameId.t.sol +++ b/test/libraries/bridge/LibGameId.t.sol @@ -3,14 +3,9 @@ pragma solidity ^0.8.15; import { Test } from "lib/forge-std/src/Test.sol"; -import { Timestamp, GameId, LibGameId } from "src/libraries/bridge/LibUDT.sol"; -import "src/libraries/bridge/Types.sol"; +import { Timestamp, GameId, GameType, LibGameId } from "src/libraries/bridge/LibUDT.sol"; -/// @title LibGameId_Pack_Test -/// @notice Tests the `pack` and `unpack` functions of the `LibGameId` library. contract LibGameId_Pack_Test is Test { - /// @notice Tests that a round trip of packing and unpacking a `GameId` maintains the same - /// values. function testFuzz_pack_roundTrip_succeeds( GameType _gameType, Timestamp _timestamp, @@ -22,8 +17,8 @@ contract LibGameId_Pack_Test is Test { GameId gameId = LibGameId.pack(_gameType, _timestamp, _gameProxy); (GameType gameType_, Timestamp timestamp_, address gameProxy_) = LibGameId.unpack(gameId); - assertEq(GameType.unwrap(gameType_), GameType.unwrap(_gameType)); - assertEq(Timestamp.unwrap(timestamp_), Timestamp.unwrap(_timestamp)); + assertEq(gameType_.raw(), _gameType.raw()); + assertEq(timestamp_.raw(), _timestamp.raw()); assertEq(gameProxy_, _gameProxy); } } diff --git a/test/libraries/rlp/RLPReader.t.sol b/test/libraries/rlp/RLPReader.t.sol index c0068c47..047a55f1 100644 --- a/test/libraries/rlp/RLPReader.t.sol +++ b/test/libraries/rlp/RLPReader.t.sol @@ -8,8 +8,6 @@ import "src/libraries/rlp/RLPErrors.sol"; /// @title RLPReader_readBytes_Test /// @notice Tests the `readBytes` function of the `RLPReader` library. -/// @dev Here we allow internal reverts as readRawBytes uses memory allocations and can only be -/// tested internally. contract RLPReader_readBytes_Test is Test { /// @notice Tests that the `readBytes` function returns the correct bytes when given a null /// byte. @@ -66,9 +64,19 @@ contract RLPReader_readBytes_Test is Test { } } +/// @title RLPReader_TestInit +/// @notice Reusable helpers for `RLPReader` tests. +abstract contract RLPReader_TestInit is Test { + function assertRepeatedRawBytes(RLPReader.RLPItem[] memory _list, bytes memory _expected) internal pure { + for (uint256 i = 0; i < _list.length; i++) { + assertEq(RLPReader.readRawBytes(_list[i]), _expected); + } + } +} + /// @title RLPReader_readList_Test /// @notice Tests the `readList` function of the `RLPReader` library. -contract RLPReader_readList_Test is Test { +contract RLPReader_readList_Test is RLPReader_TestInit { /// @notice Tests that the `readList` function returns an empty array when given an empty list. function test_readList_empty_succeeds() external pure { RLPReader.RLPItem[] memory list = RLPReader.readList(hex"c0"); @@ -114,10 +122,7 @@ contract RLPReader_readList_Test is Test { ); assertEq(list.length, 4); - assertEq(RLPReader.readRawBytes(list[0]), hex"cf84617364668471776572847a786376"); - assertEq(RLPReader.readRawBytes(list[1]), hex"cf84617364668471776572847a786376"); - assertEq(RLPReader.readRawBytes(list[2]), hex"cf84617364668471776572847a786376"); - assertEq(RLPReader.readRawBytes(list[3]), hex"cf84617364668471776572847a786376"); + assertRepeatedRawBytes(list, hex"cf84617364668471776572847a786376"); } /// @notice Tests that the `readList` function correctly parses a very long list with 32 nested @@ -127,10 +132,7 @@ contract RLPReader_readList_Test is Test { hex"f90200cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376" ); assertEq(list.length, 32); - - for (uint256 i = 0; i < 32; i++) { - assertEq(RLPReader.readRawBytes(list[i]), hex"cf84617364668471776572847a786376"); - } + assertRepeatedRawBytes(list, hex"cf84617364668471776572847a786376"); } /// @notice Tests that the `readList` function reverts when given a list longer than 32 @@ -196,18 +198,18 @@ contract RLPReader_readList_Test is Test { RLPReader.readList(hex"efdebdaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); } - /// @notice Tests that the `readList` function reverts when given data that causes int32 - /// overflow. + /// @notice Tests that the `readList` function reverts when the declared string length exceeds + /// the available payload. /// forge-config: default.allow_internal_expect_revert = true - function test_readList_int32Overflow_reverts() external { + function test_readList_declaredStringLengthExceedsPayload_reverts() external { vm.expectRevert(ContentLengthMismatch.selector); RLPReader.readList(hex"bf0f000000000000021111"); } - /// @notice Tests that the `readList` function reverts when given data that causes int32 - /// overflow with a different prefix. + /// @notice Tests that the `readList` function reverts when the declared list length exceeds + /// the available payload. /// forge-config: default.allow_internal_expect_revert = true - function test_readList_int32Overflow2_reverts() external { + function test_readList_declaredListLengthExceedsPayload_reverts() external { vm.expectRevert(ContentLengthMismatch.selector); RLPReader.readList(hex"ff0f000000000000021111"); } diff --git a/test/libraries/rlp/RLPWriter.t.sol b/test/libraries/rlp/RLPWriter.t.sol index 83705b12..61572179 100644 --- a/test/libraries/rlp/RLPWriter.t.sol +++ b/test/libraries/rlp/RLPWriter.t.sol @@ -125,10 +125,22 @@ contract RLPWriter_writeUint_Test is Test { assertGt(encoded.length, 0); } } + +/// @title RLPWriter_TestInit +/// @notice Reusable helpers for `RLPWriter` tests. +abstract contract RLPWriter_TestInit is Test { + function encodedThreeStringList() internal pure returns (bytes[] memory list_) { + list_ = new bytes[](3); + list_[0] = RLPWriter.writeString("asdf"); + list_[1] = RLPWriter.writeString("qwer"); + list_[2] = RLPWriter.writeString("zxcv"); + } +} + /// @title RLPWriter_writeList_Test /// @notice Tests the `writeList` function of the `RLPWriter` library. -contract RLPWriter_writeList_Test is Test { +contract RLPWriter_writeList_Test is RLPWriter_TestInit { /// @notice Tests that the `writeList` function returns the correct RLP encoding when given an /// empty list. function test_writeList_empty_succeeds() external pure { @@ -186,16 +198,11 @@ contract RLPWriter_writeList_Test is Test { /// long list containing nested lists. function test_writeList_longlist1_succeeds() external pure { bytes[] memory list = new bytes[](4); - bytes[] memory list2 = new bytes[](3); - - list2[0] = RLPWriter.writeString("asdf"); - list2[1] = RLPWriter.writeString("qwer"); - list2[2] = RLPWriter.writeString("zxcv"); + bytes memory nestedList = RLPWriter.writeList(encodedThreeStringList()); - list[0] = RLPWriter.writeList(list2); - list[1] = RLPWriter.writeList(list2); - list[2] = RLPWriter.writeList(list2); - list[3] = RLPWriter.writeList(list2); + for (uint256 i = 0; i < list.length; i++) { + list[i] = nestedList; + } assertEq( RLPWriter.writeList(list), @@ -207,14 +214,10 @@ contract RLPWriter_writeList_Test is Test { /// very long list with 32 nested lists. function test_writeList_longlist2_succeeds() external pure { bytes[] memory list = new bytes[](32); - bytes[] memory list2 = new bytes[](3); - - list2[0] = RLPWriter.writeString("asdf"); - list2[1] = RLPWriter.writeString("qwer"); - list2[2] = RLPWriter.writeString("zxcv"); + bytes memory nestedList = RLPWriter.writeList(encodedThreeStringList()); for (uint256 i = 0; i < 32; i++) { - list[i] = RLPWriter.writeList(list2); + list[i] = nestedList; } assertEq( @@ -229,12 +232,13 @@ contract RLPWriter_writeList_Test is Test { // [ [ [], [] ], [] ] bytes[] memory list = new bytes[](2); bytes[] memory list2 = new bytes[](2); + bytes memory emptyList = RLPWriter.writeList(new bytes[](0)); - list2[0] = RLPWriter.writeList(new bytes[](0)); - list2[1] = RLPWriter.writeList(new bytes[](0)); + list2[0] = emptyList; + list2[1] = emptyList; list[0] = RLPWriter.writeList(list2); - list[1] = RLPWriter.writeList(new bytes[](0)); + list[1] = emptyList; assertEq(RLPWriter.writeList(list), hex"c4c2c0c0c0"); } @@ -243,16 +247,17 @@ contract RLPWriter_writeList_Test is Test { /// complex nested list structure. function test_writeList_listoflists2_succeeds() external pure { // [ [], [[]], [ [], [[]] ] ] + bytes memory emptyList = RLPWriter.writeList(new bytes[](0)); bytes[] memory list = new bytes[](3); - list[0] = RLPWriter.writeList(new bytes[](0)); + list[0] = emptyList; bytes[] memory list2 = new bytes[](1); - list2[0] = RLPWriter.writeList(new bytes[](0)); + list2[0] = emptyList; list[1] = RLPWriter.writeList(list2); bytes[] memory list3 = new bytes[](2); - list3[0] = RLPWriter.writeList(new bytes[](0)); + list3[0] = emptyList; list3[1] = RLPWriter.writeList(list2); list[2] = RLPWriter.writeList(list3); @@ -316,7 +321,7 @@ contract RLPWriter_writeAddress_Test is Test { function testFuzz_writeAddress_anyAddress_succeeds(address _addr) external pure { bytes memory encoded = RLPWriter.writeAddress(_addr); assertEq(encoded.length, 21); - assertEq(uint8(encoded[0]), 0x94); + assertEq(encoded[0], bytes1(0x94)); } } diff --git a/test/libraries/trie/MerkleTrie.t.sol b/test/libraries/trie/MerkleTrie.t.sol index 71041488..a860b9c6 100644 --- a/test/libraries/trie/MerkleTrie.t.sol +++ b/test/libraries/trie/MerkleTrie.t.sol @@ -5,7 +5,7 @@ import { Test } from "lib/forge-std/src/Test.sol"; import { MerkleTrie } from "src/libraries/trie/MerkleTrie.sol"; import { RLPReader } from "src/libraries/rlp/RLPReader.sol"; import { FFIInterface } from "test/setup/FFIInterface.sol"; -import "src/libraries/rlp/RLPErrors.sol"; +import { InvalidDataRemainder, UnexpectedString } from "src/libraries/rlp/RLPErrors.sol"; contract MerkleTrie_Harness { function exposed_get(bytes memory _key, bytes[] memory _proof, bytes32 _root) public pure returns (bytes memory) { @@ -417,31 +417,18 @@ contract MerkleTrie_Get_Test is MerkleTrie_TestInit { /// @notice Tests that `get` reverts if a proof node has an unknown prefix function test_get_unknownNodePrefix_reverts(uint8 prefix) external { - // bound it to only have prefixes where the first nibble is >= 4 prefix = uint8(bound(prefix, 0x40, 0xff)); - // if the first nibble of the prefix is odd, make it even by adding 16 if (((prefix / 16) % 2) == 1) { unchecked { prefix += 16; } - // bound it again in case it overflowed prefix = uint8(bound(prefix, 0x40, 0xff)); } - MerkleTrie_Harness wrapper = new MerkleTrie_Harness(); - bytes memory key = abi.encodePacked( keccak256(abi.encodePacked(bytes32(0xa15bc60c955c405d20d9149c709e2460f1c2d9a497496a7f46004d1772c3054c))) ); bytes[] memory proof = new bytes[](5); - proof[0] = - hex"f90211a085ed702d58e6a962ad0e785e5c9036e06d878fd065eb9669122447f6aee7957da05badb8cfd5a7493d928614730af6e14eabe2c93fbac93c853dde3270c446309da01de85a57c524ac56a5bd4bed0b0aa7d963e364ad930ea964d0a42631a77ded4da0fe3143892366faeb9fae1117b888263afe0f74e6c73555fee53a604bf7188431a0af2c79f0dddd15d6f62e3fa60d515c44d58444ad3915c7ca4bddb31c8f148d0ca08f37a2f9093a4aee39519f3a06fe4674cc670fbbbd7a5f4eb096310b7bc1fdc9a086bd12d2031d9714130c687e6822250fa24b3147824780bea96cf8a7406c8966a03e42538ba2da8adaa0eca4550ef62de4dabde8ca06b71ac1b276ff31b67a7655a04a439f7eb6a62c77ec921139925af3359f61d16e083076e0e425583289107d7da0c453a51991b5a4c6174ddff77c0b7d9cc86f05ffda6ff523e2365f19797c7a00a06f43b7b9a118264ab4b6c24d7d3de77f39071a298426bfc27180adfca57d590da0032e0db4dcf122d4bdb1d4ec3c5df5fabd3127bcefe412cb046b7f0d80d11c9fa0560c2b8c9466b8cb5ffd600f24ea0ed9838bfdab7870d505d4387c2468d3c498a0597996e939ff8c29c9e59dc47c111e760450a9c4fe2b065825762da2a1f32495a0e3411c9af104364230c49de5a4d0802c84df18beee9778673364e1747a875622a02a6928825356d8280f361a02285af30e73889f4e55ecb63ed85c8581e07061d680"; - proof[1] = - hex"f90211a0db246208c4cef45e9aeb9f1df1baa8572675bc453f7da538165a2bb9e6a4c416a0d26d82a9ddff901d2a1f9e18506120dec0e5b3c95549c5bff0efc355061ea73fa04f1cedbb5c7df7ee5cc3210980baf5537affb29c661c6a4eeb193bc42e7fbc74a0acea389e0cf577d0e13483d98b15c82618ac18b7cc4a479981e3e672ddd16867a0ef59a06aeea1eb5ba1313bbe1fa74ff264d84d7319ab6178213734b5b5efa9c1a08f85dc6001713d77aa4e12982dfdb28fd1c7dcc290d46f2749e8a7d67ba2a694a0f6698ff794881edc50340b75311de64ce3da5f97debcfdfd4d20de57ef3ba7eba0680071ce05e9c7915f731bac8b9673332d1d77ea1f7dadab36d9b233eea32ba4a035ad3686f436232360c2aa364c9f2aa2081318b9fb33cd1050d69ee46f791d62a03b495b3d65d9ae39680a0f835c1d1378d218f7b1fb88d2b2c6ac6ef916f09172a0a808d1e8c632d9a6cfeb3c2c123a58b5b3e1998d4bd02c5fb0f7c5d4ba1338e6a0369376e9152831135ff3a902c9740cf22951d67edd51bf0541565e379d7efc25a0cc26d7fa1c326bc14950e92d9de78e4ed8372ff9727dec34602f24057b3a9b30a0278081783022e748dc70874a72377038935c00c1f0a24bbb8cd0fc208d8b68f4a06c4e83593571b94d08cb78ece0de4920b02a650a47a16583f94c8fe35f724707a0cd7eb9d730e5138fd943200b577e7bbb827d010a50d19af2f4b19ef19658156d80"; - proof[2] = - hex"f90211a0065f58fbe63e8e3387e91047d5b7a4532e7d9de0abc47f04791aef27a140fdb5a0858beea29778551c01b0d3e542d707675856da9c3f1d065c845e55c24d77be89a0e90a410489eff6f4f8d70b0cce1fb1339117ec0f6f1db195a6cc410509a2ebaea078ba7fe504e8d01d57f6bee52c4938d779108e500b5923272441ed2964b8c45da0f0430ed9fa807e5fb6ace75f8766ea60009d8539e00006e359f5f7bc38a76596a0a98a7938db99a2d80abea6349de42bf2576c9e51cc715c85fbacab365ec16f5ba026fadc7d124a456c62ada928eaede3e80611e3e6f99041f7393f062e9e788c8ca0ca48cad1e00d22d6146173341a6060378e738be727a7265a923cf6bfd1f8b610a0f8a4aae21a78ac28e2a61f50396f9a80f6c8232fe4afa203160361c0962242baa09a1029479959fb29b4da7239393fd6ca20bc376d860324f429a51b0e0565a158a0eefb84d3943d680e176258dffe0104ac48c171a8574a811535256a2d8ba531dea062a3d709a2f70ba1970842c4f20a602291273d1f6022e7a14cde2afdcd51e795a0397e6b9b87012cd79cbd0bb7daa4cc43830a673d80b65fb88c0449140175d89ca0f8a4c73c0078cbd32961227910e3f9315bc587716062e39f66be19747ccf9b67a0ea4bdd1b187fdba273a8625f88f284994d19c38ec58651839852665717d953d9a0319ebf356f45da83c7f106f1fd3decbf15f651fad3389a0d279602cdea8ee11480"; - proof[3] = - hex"f8f1a069a092c7a950214e7e45b99012dc8ad112eab0fc94ae5ca9efbd6949068384f280a0b25c46db67ef7cf0c47bb400c31c85a26c5a204431527c964c8ecaf3d63e52cc80a01911a2a74db0d8d182447176e23f25556d1a1eaa0afad96453f2d64876ad88e480808080a04a0ca9e3bed1bc3e3c819384d19b6d5e523164a6520c4eb42e828a63ef730ae38080a03b598ed1b9269d4b05e2e75cfb54298d25437669870c919a59a147d2d256fdba80a0db2d655057c83107a73d086cfdd8fcc74739bb48c652eb0ce597178ecf96b39aa05c66ac392a761341b9c22b773ea19af311f34ef537640b9bb96842ec6ace913280"; proof[4] = bytes.concat( hex"f69f", @@ -453,7 +440,7 @@ contract MerkleTrie_Get_Test is MerkleTrie_TestInit { (proof[0], proof[1], proof[2], proof[3], root) = rehashOtherElements(proof[4]); vm.expectRevert("MerkleTrie: received a node with an unknown prefix"); - wrapper.exposed_get(key, proof, root); + harness.exposed_get(key, proof, root); } /// @notice Tests that `get` reverts if a proof node is unparsable i.e list length is not 2 or 17 @@ -463,8 +450,6 @@ contract MerkleTrie_Get_Test is MerkleTrie_TestInit { listLen++; } - MerkleTrie_Harness wrapper = new MerkleTrie_Harness(); - bytes memory key = abi.encodePacked( keccak256(abi.encodePacked(bytes32(0xa15bc60c955c405d20d9149c709e2460f1c2d9a497496a7f46004d1772c3054c))) ); @@ -478,44 +463,36 @@ contract MerkleTrie_Get_Test is MerkleTrie_TestInit { proof[3] = hex"f8f1a069a092c7a950214e7e45b99012dc8ad112eab0fc94ae5ca9efbd6949068384f280a0b25c46db67ef7cf0c47bb400c31c85a26c5a204431527c964c8ecaf3d63e52cc80a01911a2a74db0d8d182447176e23f25556d1a1eaa0afad96453f2d64876ad88e480808080a04a0ca9e3bed1bc3e3c819384d19b6d5e523164a6520c4eb42e828a63ef730ae38080a03b598ed1b9269d4b05e2e75cfb54298d25437669870c919a59a147d2d256fdba80a0db2d655057c83107a73d086cfdd8fcc74739bb48c652eb0ce597178ecf96b39aa05c66ac392a761341b9c22b773ea19af311f34ef537640b9bb96842ec6ace913280"; proof[4] = - hex"f69f204dcf44e265ba93879b2da89e1b16ab48fc5eb8e31bc16b0612d6da8463f195942536c09e5f5691498805884fa37811be3b2bddb4"; // Correct - // leaf node + hex"f69f204dcf44e265ba93879b2da89e1b16ab48fc5eb8e31bc16b0612d6da8463f195942536c09e5f5691498805884fa37811be3b2bddb4"; bytes32 root = keccak256(proof[0]); - // Should not revert - wrapper.exposed_get(key, proof, root); + harness.exposed_get(key, proof, root); if (listLen > 3) { - // Node with list > 3 proof[4] = hex"f8379f204dcf44e265ba93879b2da89e1b16ab48fc5eb8e31bc16b0612d6da8463f195942536c09e5f5691498805884fa37811be3b2bddb480"; for (uint256 i; i < listLen - 3; i++) { proof[4] = bytes.concat(proof[4], hex"80"); } proof[4][1] = bytes1(uint8(proof[4][1]) + (listLen - 3)); - // rehash all proof elements and insert it into the proof element above it (proof[0], proof[1], proof[2], proof[3], root) = rehashOtherElements(proof[4]); vm.expectRevert("MerkleTrie: received an unparseable node"); - wrapper.exposed_get(key, proof, root); + harness.exposed_get(key, proof, root); } else if (listLen == 1) { - // Node with list of 1 proof[4] = hex"e09f204dcf44e265ba93879b2da89e1b16ab48fc5eb8e31bc16b0612d6da8463f1"; - // rehash all proof elements and insert it into the proof element above it (proof[0], proof[1], proof[2], proof[3], root) = rehashOtherElements(proof[4]); vm.expectRevert("MerkleTrie: received an unparseable node"); - wrapper.exposed_get(key, proof, root); + harness.exposed_get(key, proof, root); } else if (listLen == 3) { - // Node with list of 3 proof[4] = hex"f79f204dcf44e265ba93879b2da89e1b16ab48fc5eb8e31bc16b0612d6da8463f195942536c09e5f5691498805884fa37811be3b2bddb480"; - // rehash all proof elements and insert it into the proof element above it (proof[0], proof[1], proof[2], proof[3], root) = rehashOtherElements(proof[4]); vm.expectRevert("MerkleTrie: received an unparseable node"); - wrapper.exposed_get(key, proof, root); + harness.exposed_get(key, proof, root); } } } diff --git a/test/mocks/Callers.sol b/test/mocks/Callers.sol index 965d1eaa..0d1dbed6 100644 --- a/test/mocks/Callers.sol +++ b/test/mocks/Callers.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -// Libraries import { Burn } from "src/libraries/Burn.sol"; contract CallRecorder { @@ -35,21 +34,17 @@ contract Reverter { /// @dev Can be etched in to any address to test making a delegatecall from that address. contract DelegateCaller { - function dcForward(address _target, bytes memory _data) external { + function dcForward(address _target, bytes calldata _data) external { assembly { - // Perform the delegatecall, make sure to pass all available gas. - let success := delegatecall(gas(), _target, add(_data, 0x20), mload(_data), 0x0, 0x0) + calldatacopy(0x0, _data.offset, _data.length) + let success := delegatecall(gas(), _target, 0x0, _data.length, 0x0, 0x0) - // Copy returndata into memory at 0x0....returndatasize. Note that this *will* - // overwrite the calldata that we just copied into memory but that doesn't really - // matter because we'll be returning in a second anyway. - returndatacopy(0x0, 0x0, returndatasize()) + let size := returndatasize() + returndatacopy(0x0, 0x0, size) - // Success == 0 means a revert. We'll revert too and pass the data up. - if iszero(success) { revert(0x0, returndatasize()) } + if iszero(success) { revert(0x0, size) } - // Otherwise we'll just return and pass the data up. - return(0x0, returndatasize()) + return(0x0, size) } } } @@ -57,28 +52,18 @@ contract DelegateCaller { /// @title GasBurner /// @notice Contract that burns a specified amount of gas on receive or fallback. contract GasBurner { - /// @notice The amount of gas to burn on receive or fallback. - uint256 immutable GAS_TO_BURN; + uint256 internal constant GAS_BURN_OVERHEAD = 500; + uint256 internal immutable GAS_TO_BURN; - /// @notice Constructor. - /// @param _gas The amount of gas to burn on receive or fallback. constructor(uint256 _gas) { - // 500 gas buffer for Solidity overhead. - GAS_TO_BURN = _gas - 500; + GAS_TO_BURN = _gas - GAS_BURN_OVERHEAD; } - /// @notice Receive function that burns the specified amount of gas. receive() external payable { - _burn(); + Burn.gas(GAS_TO_BURN); } - /// @notice Fallback function that burns the specified amount of gas. fallback() external payable { - _burn(); - } - - /// @notice Internal function that burns the specified amount of gas. - function _burn() internal view { Burn.gas(GAS_TO_BURN); } } diff --git a/test/mocks/EIP1967Helper.sol b/test/mocks/EIP1967Helper.sol index 4720b7e9..22badcc8 100644 --- a/test/mocks/EIP1967Helper.sol +++ b/test/mocks/EIP1967Helper.sol @@ -2,34 +2,26 @@ pragma solidity ^0.8.0; import { Vm } from "lib/forge-std/src/Vm.sol"; +import { Constants } from "src/libraries/Constants.sol"; /// @title EIP1967Helper /// @dev Testing library to help with reading EIP 1967 variables from state library EIP1967Helper { - /// @notice The storage slot that holds the address of a proxy implementation. - /// @dev `bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1)` - bytes32 internal constant PROXY_IMPLEMENTATION_SLOT = - 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; - - /// @notice The storage slot that holds the address of the owner. - /// @dev `bytes32(uint256(keccak256('eip1967.proxy.admin')) - 1)` - bytes32 internal constant PROXY_ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; - Vm internal constant vm = Vm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); function getAdmin(address _proxy) internal view returns (address) { - return address(uint160(uint256(vm.load(address(_proxy), PROXY_ADMIN_SLOT)))); + return address(uint160(uint256(vm.load(_proxy, Constants.PROXY_OWNER_ADDRESS)))); } - function setAdmin(address _addr, address _admin) internal { - vm.store(_addr, PROXY_ADMIN_SLOT, bytes32(uint256(uint160(_admin)))); + function setAdmin(address _proxy, address _admin) internal { + vm.store(_proxy, Constants.PROXY_OWNER_ADDRESS, bytes32(uint256(uint160(_admin)))); } function getImplementation(address _proxy) internal view returns (address) { - return address(uint160(uint256(vm.load(address(_proxy), PROXY_IMPLEMENTATION_SLOT)))); + return address(uint160(uint256(vm.load(_proxy, Constants.PROXY_IMPLEMENTATION_ADDRESS)))); } - function setImplementation(address _addr, address _impl) internal { - vm.store(_addr, PROXY_IMPLEMENTATION_SLOT, bytes32(uint256(uint160(_impl)))); + function setImplementation(address _proxy, address _impl) internal { + vm.store(_proxy, Constants.PROXY_IMPLEMENTATION_ADDRESS, bytes32(uint256(uint160(_impl)))); } } diff --git a/test/mocks/MockDevTEEProverRegistry.sol b/test/mocks/MockDevTEEProverRegistry.sol index 523a9615..dfe9546f 100644 --- a/test/mocks/MockDevTEEProverRegistry.sol +++ b/test/mocks/MockDevTEEProverRegistry.sol @@ -8,9 +8,8 @@ import { EnumerableSetLib } from "lib/solady-v0.0.245/src/utils/EnumerableSetLib import { TEEProverRegistry } from "src/L1/proofs/tee/TEEProverRegistry.sol"; /// @title DevTEEProverRegistry -/// @notice Development version of TEEProverRegistry with bypassed attestation for testing. -/// @dev This contract adds addDevSigner() which bypasses AWS Nitro attestation verification. -/// DO NOT deploy this contract to production networks. +/// @notice Test/development registry that can register signers without Nitro attestation verification. +/// @dev DO NOT deploy this contract to production networks. contract DevTEEProverRegistry is TEEProverRegistry { using EnumerableSetLib for EnumerableSetLib.AddressSet; @@ -21,10 +20,8 @@ contract DevTEEProverRegistry is TEEProverRegistry { TEEProverRegistry(nitroVerifier, factory) { } - /// @notice Registers a signer for testing (bypasses attestation verification). + /// @notice Registers a signer and image hash without attestation verification. /// @dev Only callable by owner. For development/testing use only. - /// The imageHash parameter is stored so isValidSigner() can validate against - /// the current AggregateVerifier's TEE_IMAGE_HASH. /// @param signer The address of the signer to register. /// @param imageHash The TEE image hash to associate with this signer. function addDevSigner(address signer, bytes32 imageHash) external onlyOwner { diff --git a/test/mocks/NextImpl.sol b/test/mocks/NextImpl.sol index 9840e710..a52d14e3 100644 --- a/test/mocks/NextImpl.sol +++ b/test/mocks/NextImpl.sol @@ -1,21 +1,20 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { Initializable } from "lib/openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol"; +import { Initializable } from "lib/openzeppelin-contracts/contracts/proxy/utils/Initializable.sol"; /// @title NextImpl -/// @dev Used for testing a future upgrade beyond the current implementations. -// We include some variables so that we can sanity check accessing storage values after an upgrade. +/// @dev Mock future implementation used to verify storage access after an upgrade. contract NextImpl is Initializable { - // Initializable occupies the zero-th slot. - bytes32 slot1; - bytes32[19] __gap; - bytes32 slot21; + // Initializable stores its flags in slot 0, so these fields place slot21 at storage slot 21. + bytes32 private slot1; + uint256[19] private __gap; + bytes32 private slot21; + bytes32 public constant slot21Init = bytes32(hex"1337"); function initialize(uint8 _init) public reinitializer(_init) { - // Slot21 is unused by an of our upgradeable contracts. - // This is used to verify that we can access this value after an upgrade. + // Slot 21 is unused by any current upgrade target and proves the new implementation can write to it. slot21 = slot21Init; } } diff --git a/test/scripts/L2Genesis.t.sol b/test/scripts/L2Genesis.t.sol index 8d7c9807..c0bd8093 100644 --- a/test/scripts/L2Genesis.t.sol +++ b/test/scripts/L2Genesis.t.sol @@ -6,10 +6,6 @@ import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; import { L2Genesis } from "scripts/L2Genesis.s.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; import { LATEST_FORK } from "scripts/libraries/Config.sol"; -import { ISequencerFeeVault } from "interfaces/L2/ISequencerFeeVault.sol"; -import { IBaseFeeVault } from "interfaces/L2/IBaseFeeVault.sol"; -import { IL1FeeVault } from "interfaces/L2/IL1FeeVault.sol"; -import { IOperatorFeeVault } from "interfaces/L2/IOperatorFeeVault.sol"; import { IOptimismMintableERC20Factory } from "interfaces/universal/IOptimismMintableERC20Factory.sol"; import { IOptimismMintableERC721Factory } from "interfaces/L2/IOptimismMintableERC721Factory.sol"; import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; @@ -28,12 +24,9 @@ abstract contract L2Genesis_TestInit is Test { genesis = new L2Genesis(); } - function testProxyAdmin() internal view { - // Verify owner in the proxy + function _assertProxyAdmin() internal view { assertEq(input.opChainProxyAdminOwner, IProxyAdmin(Predeploys.PROXY_ADMIN).owner()); - // Verify owner in the implementation to catch storage shifting issues - // The implementation is stored in the code namespace address proxyAdminImpl = Predeploys.predeployToCodeNamespace(Predeploys.PROXY_ADMIN); assertEq( input.opChainProxyAdminOwner, @@ -42,71 +35,77 @@ abstract contract L2Genesis_TestInit is Test { ); } - function testPredeploys() internal view { + function _assertPredeploys() internal view { uint160 prefix = uint160(0x420) << 148; for (uint256 i = 0; i < Predeploys.PREDEPLOY_COUNT; i++) { address addr = address(prefix | uint160(i)); - // If it's not proxied, skip next checks. if (Predeploys.notProxied(addr)) { continue; } - // All predeploys should have code assertGt(addr.code.length, 0); - // All proxied predeploys should have the 1967 admin slot set to the ProxyAdmin assertEq(Predeploys.PROXY_ADMIN, EIP1967Helper.getAdmin(addr)); - // If it's not a supported predeploy, skip next checks. if (!Predeploys.isSupportedPredeploy(addr)) { continue; } - // All proxied predeploys should have the 1967 admin slot set to the ProxyAdmin - // predeploy address impl = Predeploys.predeployToCodeNamespace(addr); + assertEq(impl, EIP1967Helper.getImplementation(addr)); assertGt(impl.code.length, 0); } assertGt(Predeploys.WETH.code.length, 0); } - function testVaultsWithoutRevenueShare() internal view { - IBaseFeeVault baseFeeVault = IBaseFeeVault(payable(Predeploys.BASE_FEE_VAULT)); - IL1FeeVault l1FeeVault = IL1FeeVault(payable(Predeploys.L1_FEE_VAULT)); - ISequencerFeeVault sequencerFeeVault = ISequencerFeeVault(payable(Predeploys.SEQUENCER_FEE_WALLET)); - IOperatorFeeVault operatorFeeVault = IOperatorFeeVault(payable(Predeploys.OPERATOR_FEE_VAULT)); - - assertEq(baseFeeVault.RECIPIENT(), input.baseFeeVaultRecipient); - assertEq(baseFeeVault.recipient(), input.baseFeeVaultRecipient); - assertEq(baseFeeVault.MIN_WITHDRAWAL_AMOUNT(), input.baseFeeVaultMinimumWithdrawalAmount); - assertEq(baseFeeVault.minWithdrawalAmount(), input.baseFeeVaultMinimumWithdrawalAmount); - assertEq(uint8(baseFeeVault.WITHDRAWAL_NETWORK()), uint8(input.baseFeeVaultWithdrawalNetwork)); - assertEq(uint8(baseFeeVault.withdrawalNetwork()), uint8(input.baseFeeVaultWithdrawalNetwork)); - - assertEq(l1FeeVault.RECIPIENT(), input.l1FeeVaultRecipient); - assertEq(l1FeeVault.recipient(), input.l1FeeVaultRecipient); - assertEq(l1FeeVault.MIN_WITHDRAWAL_AMOUNT(), input.l1FeeVaultMinimumWithdrawalAmount); - assertEq(l1FeeVault.minWithdrawalAmount(), input.l1FeeVaultMinimumWithdrawalAmount); - assertEq(uint8(l1FeeVault.WITHDRAWAL_NETWORK()), uint8(input.l1FeeVaultWithdrawalNetwork)); - assertEq(uint8(l1FeeVault.withdrawalNetwork()), uint8(input.l1FeeVaultWithdrawalNetwork)); - - assertEq(sequencerFeeVault.RECIPIENT(), input.sequencerFeeVaultRecipient); - assertEq(sequencerFeeVault.recipient(), input.sequencerFeeVaultRecipient); - assertEq(sequencerFeeVault.MIN_WITHDRAWAL_AMOUNT(), input.sequencerFeeVaultMinimumWithdrawalAmount); - assertEq(sequencerFeeVault.minWithdrawalAmount(), input.sequencerFeeVaultMinimumWithdrawalAmount); - assertEq(uint8(sequencerFeeVault.WITHDRAWAL_NETWORK()), uint8(input.sequencerFeeVaultWithdrawalNetwork)); - assertEq(uint8(sequencerFeeVault.withdrawalNetwork()), uint8(input.sequencerFeeVaultWithdrawalNetwork)); - - assertEq(operatorFeeVault.RECIPIENT(), input.operatorFeeVaultRecipient); - assertEq(operatorFeeVault.recipient(), input.operatorFeeVaultRecipient); - assertEq(operatorFeeVault.MIN_WITHDRAWAL_AMOUNT(), input.operatorFeeVaultMinimumWithdrawalAmount); - assertEq(operatorFeeVault.minWithdrawalAmount(), input.operatorFeeVaultMinimumWithdrawalAmount); - assertEq(uint8(operatorFeeVault.WITHDRAWAL_NETWORK()), uint8(input.operatorFeeVaultWithdrawalNetwork)); - assertEq(uint8(operatorFeeVault.withdrawalNetwork()), uint8(input.operatorFeeVaultWithdrawalNetwork)); + function _assertFeeVaultsWithoutRevenueShare() internal view { + _assertFeeVault( + Predeploys.BASE_FEE_VAULT, + input.baseFeeVaultRecipient, + input.baseFeeVaultMinimumWithdrawalAmount, + input.baseFeeVaultWithdrawalNetwork + ); + _assertFeeVault( + Predeploys.L1_FEE_VAULT, + input.l1FeeVaultRecipient, + input.l1FeeVaultMinimumWithdrawalAmount, + input.l1FeeVaultWithdrawalNetwork + ); + _assertFeeVault( + Predeploys.SEQUENCER_FEE_WALLET, + input.sequencerFeeVaultRecipient, + input.sequencerFeeVaultMinimumWithdrawalAmount, + input.sequencerFeeVaultWithdrawalNetwork + ); + _assertFeeVault( + Predeploys.OPERATOR_FEE_VAULT, + input.operatorFeeVaultRecipient, + input.operatorFeeVaultMinimumWithdrawalAmount, + input.operatorFeeVaultWithdrawalNetwork + ); } - function testFactories() internal view { + function _assertFeeVault( + address _vault, + address _recipient, + uint256 _minWithdrawalAmount, + uint256 _withdrawalNetwork + ) + internal + view + { + IFeeVault vault = IFeeVault(payable(_vault)); + + assertEq(vault.RECIPIENT(), _recipient); + assertEq(vault.recipient(), _recipient); + assertEq(vault.MIN_WITHDRAWAL_AMOUNT(), _minWithdrawalAmount); + assertEq(vault.minWithdrawalAmount(), _minWithdrawalAmount); + assertEq(uint256(vault.WITHDRAWAL_NETWORK()), _withdrawalNetwork); + assertEq(uint256(vault.withdrawalNetwork()), _withdrawalNetwork); + } + + function _assertFactories() internal view { IOptimismMintableERC20Factory erc20Factory = IOptimismMintableERC20Factory(payable(Predeploys.OPTIMISM_MINTABLE_ERC20_FACTORY)); IOptimismMintableERC721Factory erc721Factory = @@ -117,12 +116,12 @@ abstract contract L2Genesis_TestInit is Test { assertEq(erc721Factory.remoteChainID(), input.l1ChainID); } - function testForks() internal view { - // The fork should be set to Isthmus at least. Check by validating the GasPriceOracle + function _assertForks() internal view { IGasPriceOracle gasPriceOracle = IGasPriceOracle(payable(Predeploys.GAS_PRICE_ORACLE)); - assertEq(gasPriceOracle.isEcotone(), true); - assertEq(gasPriceOracle.isFjord(), true); - assertEq(gasPriceOracle.isIsthmus(), true); + assertTrue(gasPriceOracle.isEcotone()); + assertTrue(gasPriceOracle.isFjord()); + assertTrue(gasPriceOracle.isIsthmus()); + assertTrue(gasPriceOracle.isJovian()); } } @@ -131,26 +130,26 @@ abstract contract L2Genesis_TestInit is Test { contract L2Genesis_Run_Test is L2Genesis_TestInit { function setUp() public override { super.setUp(); - // Set up default input configuration + input = L2Genesis.Input({ l1ChainID: 1, l2ChainID: 2, - l1CrossDomainMessengerProxy: payable(address(0x0000000000000000000000000000000000000001)), - l1StandardBridgeProxy: payable(address(0x0000000000000000000000000000000000000002)), - l1ERC721BridgeProxy: payable(address(0x0000000000000000000000000000000000000003)), - opChainProxyAdminOwner: address(0x0000000000000000000000000000000000000004), - sequencerFeeVaultRecipient: address(0x0000000000000000000000000000000000000005), + l1CrossDomainMessengerProxy: payable(makeAddr("L1CrossDomainMessengerProxy")), + l1StandardBridgeProxy: payable(makeAddr("L1StandardBridgeProxy")), + l1ERC721BridgeProxy: payable(makeAddr("L1ERC721BridgeProxy")), + opChainProxyAdminOwner: makeAddr("ProxyAdminOwner"), + sequencerFeeVaultRecipient: makeAddr("SequencerFeeVaultRecipient"), sequencerFeeVaultMinimumWithdrawalAmount: 1, - sequencerFeeVaultWithdrawalNetwork: 1, - baseFeeVaultRecipient: address(0x0000000000000000000000000000000000000006), + sequencerFeeVaultWithdrawalNetwork: uint256(Types.WithdrawalNetwork.L2), + baseFeeVaultRecipient: makeAddr("BaseFeeVaultRecipient"), baseFeeVaultMinimumWithdrawalAmount: 1, - baseFeeVaultWithdrawalNetwork: 1, - l1FeeVaultRecipient: address(0x0000000000000000000000000000000000000007), + baseFeeVaultWithdrawalNetwork: uint256(Types.WithdrawalNetwork.L2), + l1FeeVaultRecipient: makeAddr("L1FeeVaultRecipient"), l1FeeVaultMinimumWithdrawalAmount: 1, - l1FeeVaultWithdrawalNetwork: 1, - operatorFeeVaultRecipient: address(0x0000000000000000000000000000000000000008), + l1FeeVaultWithdrawalNetwork: uint256(Types.WithdrawalNetwork.L2), + operatorFeeVaultRecipient: makeAddr("OperatorFeeVaultRecipient"), operatorFeeVaultMinimumWithdrawalAmount: 1, - operatorFeeVaultWithdrawalNetwork: 1, + operatorFeeVaultWithdrawalNetwork: uint256(Types.WithdrawalNetwork.L2), fork: uint256(LATEST_FORK), fundDevAccounts: true }); @@ -159,9 +158,10 @@ contract L2Genesis_Run_Test is L2Genesis_TestInit { function test_run_succeeds() external { genesis.run(input); - testProxyAdmin(); - testPredeploys(); - testFactories(); - testForks(); + _assertProxyAdmin(); + _assertPredeploys(); + _assertFeeVaultsWithoutRevenueShare(); + _assertFactories(); + _assertForks(); } } diff --git a/test/setup/CommonTest.sol b/test/setup/CommonTest.sol index 63b78c5a..3ee0a0fe 100644 --- a/test/setup/CommonTest.sol +++ b/test/setup/CommonTest.sol @@ -12,18 +12,18 @@ import { FFIInterface } from "test/setup/FFIInterface.sol"; // Contracts import { ERC20 } from "lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; -import { console } from "lib/forge-std/src/console.sol"; +import { console2 as console } from "lib/forge-std/src/console2.sol"; // Interfaces import { IOptimismMintableERC20Full } from "interfaces/universal/IOptimismMintableERC20Full.sol"; /// @title CommonTest -/// @dev An extension to `Test` that sets up the optimism smart contracts. +/// @dev An extension to `Test` that sets up the Optimism smart contracts. abstract contract CommonTest is Test, Setup, Events { address alice; address bob; - bytes32 constant nonZeroHash = keccak256(abi.encode("NON_ZERO")); + uint256 constant DEFAULT_TEST_BALANCE = 10_000 ether; FFIInterface constant ffi = FFIInterface(address(uint160(uint256(keccak256(abi.encode("optimism.ffi")))))); @@ -52,8 +52,8 @@ abstract contract CommonTest is Test, Setup, Events { alice = makeAddr("alice"); bob = makeAddr("bob"); - vm.deal(alice, 10000 ether); - vm.deal(bob, 10000 ether); + vm.deal(alice, DEFAULT_TEST_BALANCE); + vm.deal(bob, DEFAULT_TEST_BALANCE); // Override the config after the deploy script initialized the config if (useUpgradedFork) { @@ -76,23 +76,18 @@ abstract contract CommonTest is Test, Setup, Events { excludeContract(address(deploy)); excludeContract(address(deploy.cfg())); - // Deploy L1 Setup.L1(); - // Deploy L2 Setup.L2(); - // Call bridge initializer setup function bridgeInitializerSetUp(); } - function bridgeInitializerSetUp() public { + function bridgeInitializerSetUp() internal { L1Token = new ERC20("Native L1 Token", "L1T"); if (isForkTest()) { - console.log("CommonTest: fork test detected, skipping L2 setup"); L2Token = IOptimismMintableERC20Full(makeAddr("L2Token")); } else { - // Deploy the L2 ERC20 now L2Token = IOptimismMintableERC20Full( l2OptimismMintableERC20Factory.createStandardL2Token( address(L1Token), @@ -104,23 +99,18 @@ abstract contract CommonTest is Test, Setup, Events { NativeL2Token = new ERC20("Native L2 Token", "L2T"); + string memory remoteL1TokenName = string(abi.encodePacked("L1-", NativeL2Token.name())); + string memory remoteL1TokenSymbol = string(abi.encodePacked("L1-", NativeL2Token.symbol())); + RemoteL1Token = IOptimismMintableERC20Full( l1OptimismMintableERC20Factory.createStandardL2Token( - address(NativeL2Token), - string(abi.encodePacked("L1-", NativeL2Token.name())), - string(abi.encodePacked("L1-", NativeL2Token.symbol())) + address(NativeL2Token), remoteL1TokenName, remoteL1TokenSymbol ) ); BadL1Token = ERC20( - l1OptimismMintableERC20Factory.createStandardL2Token( - address(1), - string(abi.encodePacked("L1-", NativeL2Token.name())), - string(abi.encodePacked("L1-", NativeL2Token.symbol())) - ) + l1OptimismMintableERC20Factory.createStandardL2Token(address(1), remoteL1TokenName, remoteL1TokenSymbol) ); - - console.log("CommonTest: SetUp complete!"); } /// @dev Helper function that wraps `TransactionDeposited` event. @@ -151,9 +141,9 @@ abstract contract CommonTest is Test, Setup, Events { } /// @dev Disables upgrade mode for testing. By default the fork testing env will be upgraded to the latest - /// implementation. This can be used to disable the upgrade which, is useful for tests targeting the upgrade + /// implementation. This can be used to disable the upgrade, which is useful for tests targeting the upgrade /// process itself. - function disableUpgradedFork() public { + function disableUpgradedFork() internal { _checkNotDeployed("non-upgraded fork"); useUpgradedFork = false; diff --git a/test/setup/Events.sol b/test/setup/Events.sol index d3119ed8..4ed90fc2 100644 --- a/test/setup/Events.sol +++ b/test/setup/Events.sol @@ -1,28 +1,19 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -// Libraries import { Types } from "src/libraries/Types.sol"; -import { Timestamp } from "src/libraries/bridge/LibUDT.sol"; -import "src/libraries/bridge/Types.sol"; - -// Interfaces -import { IDisputeGame } from "interfaces/L1/proofs/IDisputeGame.sol"; /// @title Events /// @dev Contains various events that are tested against. This contract needs to /// exist until we either modularize the implementations or use a newer version of /// solc that allows for referencing events from other contracts. abstract contract Events { - /// @dev OpenZeppelin Ownable.sol transferOwnership event event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); event TransactionDeposited(address indexed from, address indexed to, uint256 indexed version, bytes opaqueData); event WithdrawalFinalized(bytes32 indexed withdrawalHash, bool success); event WithdrawalProven(bytes32 indexed withdrawalHash, address indexed from, address indexed to); event WithdrawalProvenExtension1(bytes32 indexed withdrawalHash, address indexed proofSubmitter); - event DisputeGameBlacklisted(IDisputeGame indexed disputeGame); - event RespectedGameTypeSet(GameType indexed newGameType, Timestamp indexed updatedAt); event SentMessage(address indexed target, address sender, bytes message, uint256 messageNonce, uint256 gasLimit); event SentMessageExtension1(address indexed sender, uint256 value); @@ -38,22 +29,6 @@ abstract contract Events { event WithdrawerBalanceBurnt(uint256 indexed amount); event RelayedMessage(bytes32 indexed msgHash); event FailedRelayedMessage(bytes32 indexed msgHash); - event TransactionDeposited( - address indexed from, - address indexed to, - uint256 mint, - uint256 value, - uint64 gasLimit, - bool isCreation, - bytes data - ); - event WhatHappened(bool success, bytes returndata); - - event OutputProposed( - bytes32 indexed outputRoot, uint256 indexed l2OutputIndex, uint256 indexed l2BlockNumber, uint256 l1Timestamp - ); - - event OutputsDeleted(uint256 indexed prevNextOutputIndex, uint256 indexed newNextOutputIndex); event Withdrawal(uint256 value, address to, address from); event Withdrawal(uint256 value, address to, address from, Types.WithdrawalNetwork withdrawalNetwork); @@ -78,10 +53,6 @@ abstract contract Events { address indexed l1Token, address indexed l2Token, address indexed from, address to, uint256 amount, bytes data ); - event DepositFailed( - address indexed l1Token, address indexed l2Token, address indexed from, address to, uint256 amount, bytes data - ); - event ETHBridgeInitiated(address indexed from, address indexed to, uint256 amount, bytes data); event ETHBridgeFinalized(address indexed from, address indexed to, uint256 amount, bytes data); @@ -109,12 +80,4 @@ abstract contract Events { event Unpaused(address identifier); event PauseExtended(address identifier); - - event BalanceChanged(address account, uint256 balance); - - event ETHMigrated(address indexed lockbox, uint256 ethBalance); - - event PortalMigrated( - address oldLockbox, address newLockbox, address oldAnchorStateRegistry, address newAnchorStateRegistry - ); } diff --git a/test/setup/FFIInterface.sol b/test/setup/FFIInterface.sol index ca2fd4d8..313796b5 100644 --- a/test/setup/FFIInterface.sol +++ b/test/setup/FFIInterface.sol @@ -11,32 +11,17 @@ import { Strings } from "lib/openzeppelin-contracts/contracts/utils/Strings.sol" /// are multiple artifacts for different compiler versions. contract FFIInterface { Vm internal constant vm = Vm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + string internal constant GO_FFI = "scripts/go-ffi/go-ffi"; + string internal constant DIFF_MODE = "diff"; function getProveWithdrawalTransactionInputs(Types.WithdrawalTransaction memory _tx) external returns (bytes32, bytes32, bytes32, bytes32, bytes[] memory) { - string[] memory cmds = new string[](9); - cmds[0] = "scripts/go-ffi/go-ffi"; - cmds[1] = "diff"; - cmds[2] = "getProveWithdrawalTransactionInputs"; - cmds[3] = vm.toString(_tx.nonce); - cmds[4] = vm.toString(_tx.sender); - cmds[5] = vm.toString(_tx.target); - cmds[6] = vm.toString(_tx.value); - cmds[7] = vm.toString(_tx.gasLimit); - cmds[8] = vm.toString(_tx.data); - - bytes memory result = vm.ffi(cmds); - ( - bytes32 stateRoot, - bytes32 storageRoot, - bytes32 outputRoot, - bytes32 withdrawalHash, - bytes[] memory withdrawalProof - ) = abi.decode(result, (bytes32, bytes32, bytes32, bytes32, bytes[])); - - return (stateRoot, storageRoot, outputRoot, withdrawalHash, withdrawalProof); + string[] memory cmds = _newDiffCommand("getProveWithdrawalTransactionInputs", 6); + _setCrossDomainArgs(cmds, _tx.nonce, _tx.sender, _tx.target, _tx.value, _tx.gasLimit, _tx.data); + + return abi.decode(vm.ffi(cmds), (bytes32, bytes32, bytes32, bytes32, bytes[])); } function hashCrossDomainMessage( @@ -50,19 +35,10 @@ contract FFIInterface { external returns (bytes32) { - string[] memory cmds = new string[](9); - cmds[0] = "scripts/go-ffi/go-ffi"; - cmds[1] = "diff"; - cmds[2] = "hashCrossDomainMessage"; - cmds[3] = vm.toString(_nonce); - cmds[4] = vm.toString(_sender); - cmds[5] = vm.toString(_target); - cmds[6] = vm.toString(_value); - cmds[7] = vm.toString(_gasLimit); - cmds[8] = vm.toString(_data); + string[] memory cmds = _newDiffCommand("hashCrossDomainMessage", 6); + _setCrossDomainArgs(cmds, _nonce, _sender, _target, _value, _gasLimit, _data); - bytes memory result = vm.ffi(cmds); - return abi.decode(result, (bytes32)); + return abi.decode(vm.ffi(cmds), (bytes32)); } function hashWithdrawal( @@ -76,19 +52,10 @@ contract FFIInterface { external returns (bytes32) { - string[] memory cmds = new string[](9); - cmds[0] = "scripts/go-ffi/go-ffi"; - cmds[1] = "diff"; - cmds[2] = "hashWithdrawal"; - cmds[3] = vm.toString(_nonce); - cmds[4] = vm.toString(_sender); - cmds[5] = vm.toString(_target); - cmds[6] = vm.toString(_value); - cmds[7] = vm.toString(_gasLimit); - cmds[8] = vm.toString(_data); + string[] memory cmds = _newDiffCommand("hashWithdrawal", 6); + _setCrossDomainArgs(cmds, _nonce, _sender, _target, _value, _gasLimit, _data); - bytes memory result = vm.ffi(cmds); - return abi.decode(result, (bytes32)); + return abi.decode(vm.ffi(cmds), (bytes32)); } function hashOutputRootProof( @@ -100,17 +67,13 @@ contract FFIInterface { external returns (bytes32) { - string[] memory cmds = new string[](7); - cmds[0] = "scripts/go-ffi/go-ffi"; - cmds[1] = "diff"; - cmds[2] = "hashOutputRootProof"; + string[] memory cmds = _newDiffCommand("hashOutputRootProof", 4); cmds[3] = Strings.toHexString(uint256(_version)); cmds[4] = Strings.toHexString(uint256(_stateRoot)); cmds[5] = Strings.toHexString(uint256(_messagePasserStorageRoot)); cmds[6] = Strings.toHexString(uint256(_latestBlockhash)); - bytes memory result = vm.ffi(cmds); - return abi.decode(result, (bytes32)); + return abi.decode(vm.ffi(cmds), (bytes32)); } function hashDepositTransaction( @@ -125,10 +88,7 @@ contract FFIInterface { external returns (bytes32) { - string[] memory cmds = new string[](11); - cmds[0] = "scripts/go-ffi/go-ffi"; - cmds[1] = "diff"; - cmds[2] = "hashDepositTransaction"; + string[] memory cmds = _newDiffCommand("hashDepositTransaction", 8); cmds[3] = "0x0000000000000000000000000000000000000000000000000000000000000000"; cmds[4] = vm.toString(_logIndex); cmds[5] = vm.toString(_from); @@ -138,15 +98,11 @@ contract FFIInterface { cmds[9] = vm.toString(_gas); cmds[10] = vm.toString(_data); - bytes memory result = vm.ffi(cmds); - return abi.decode(result, (bytes32)); + return abi.decode(vm.ffi(cmds), (bytes32)); } function encodeDepositTransaction(Types.UserDepositTransaction calldata txn) external returns (bytes memory) { - string[] memory cmds = new string[](12); - cmds[0] = "scripts/go-ffi/go-ffi"; - cmds[1] = "diff"; - cmds[2] = "encodeDepositTransaction"; + string[] memory cmds = _newDiffCommand("encodeDepositTransaction", 9); cmds[3] = vm.toString(txn.from); cmds[4] = vm.toString(txn.to); cmds[5] = vm.toString(txn.value); @@ -157,8 +113,7 @@ contract FFIInterface { cmds[10] = vm.toString(txn.l1BlockHash); cmds[11] = vm.toString(txn.logIndex); - bytes memory result = vm.ffi(cmds); - return abi.decode(result, (bytes)); + return abi.decode(vm.ffi(cmds), (bytes)); } function encodeCrossDomainMessage( @@ -172,84 +127,93 @@ contract FFIInterface { external returns (bytes memory) { - string[] memory cmds = new string[](9); - cmds[0] = "scripts/go-ffi/go-ffi"; - cmds[1] = "diff"; - cmds[2] = "encodeCrossDomainMessage"; - cmds[3] = vm.toString(_nonce); - cmds[4] = vm.toString(_sender); - cmds[5] = vm.toString(_target); - cmds[6] = vm.toString(_value); - cmds[7] = vm.toString(_gasLimit); - cmds[8] = vm.toString(_data); + string[] memory cmds = _newDiffCommand("encodeCrossDomainMessage", 6); + _setCrossDomainArgs(cmds, _nonce, _sender, _target, _value, _gasLimit, _data); - bytes memory result = vm.ffi(cmds); - return abi.decode(result, (bytes)); + return abi.decode(vm.ffi(cmds), (bytes)); } function encodeSuperRootProof(Types.SuperRootProof calldata proof) external returns (bytes memory) { - string[] memory cmds = new string[](4); - cmds[0] = "scripts/go-ffi/go-ffi"; - cmds[1] = "diff"; - cmds[2] = "encodeSuperRootProof"; + string[] memory cmds = _newDiffCommand("encodeSuperRootProof", 1); cmds[3] = vm.toString(abi.encode(proof)); - bytes memory result = vm.ffi(cmds); - return abi.decode(result, (bytes)); + return abi.decode(vm.ffi(cmds), (bytes)); } function hashSuperRootProof(Types.SuperRootProof calldata proof) external returns (bytes32) { - string[] memory cmds = new string[](4); - cmds[0] = "scripts/go-ffi/go-ffi"; - cmds[1] = "diff"; - cmds[2] = "hashSuperRootProof"; + string[] memory cmds = _newDiffCommand("hashSuperRootProof", 1); cmds[3] = vm.toString(abi.encode(proof)); - bytes memory result = vm.ffi(cmds); - return abi.decode(result, (bytes32)); + return abi.decode(vm.ffi(cmds), (bytes32)); } function decodeVersionedNonce(uint256 nonce) external returns (uint256, uint256) { - string[] memory cmds = new string[](4); - cmds[0] = "scripts/go-ffi/go-ffi"; - cmds[1] = "diff"; - cmds[2] = "decodeVersionedNonce"; + string[] memory cmds = _newDiffCommand("decodeVersionedNonce", 1); cmds[3] = vm.toString(nonce); - bytes memory result = vm.ffi(cmds); - return abi.decode(result, (uint256, uint256)); + return abi.decode(vm.ffi(cmds), (uint256, uint256)); } function getMerkleTrieFuzzCase(string memory variant) external returns (bytes32, bytes memory, bytes memory, bytes[] memory) { - string[] memory cmds = new string[](3); - cmds[0] = "./scripts/go-ffi/go-ffi"; - cmds[1] = "trie"; - cmds[2] = variant; + string[] memory cmds = _newCommand("trie", variant, 0); return abi.decode(vm.ffi(cmds), (bytes32, bytes, bytes, bytes[])); } function encodeScalarEcotone(uint32 _basefeeScalar, uint32 _blobbasefeeScalar) external returns (bytes32) { - string[] memory cmds = new string[](5); - cmds[0] = "scripts/go-ffi/go-ffi"; - cmds[1] = "diff"; - cmds[2] = "encodeScalarEcotone"; + string[] memory cmds = _newDiffCommand("encodeScalarEcotone", 2); cmds[3] = vm.toString(_basefeeScalar); cmds[4] = vm.toString(_blobbasefeeScalar); - bytes memory result = vm.ffi(cmds); - return abi.decode(result, (bytes32)); + + return abi.decode(vm.ffi(cmds), (bytes32)); } function decodeScalarEcotone(bytes32 _scalar) external returns (uint32, uint32) { - string[] memory cmds = new string[](4); - cmds[0] = "scripts/go-ffi/go-ffi"; - cmds[1] = "diff"; - cmds[2] = "decodeScalarEcotone"; + string[] memory cmds = _newDiffCommand("decodeScalarEcotone", 1); cmds[3] = vm.toString(_scalar); - bytes memory result = vm.ffi(cmds); - return abi.decode(result, (uint32, uint32)); + + return abi.decode(vm.ffi(cmds), (uint32, uint32)); + } + + function _newDiffCommand(string memory _variant, uint256 _argCount) private pure returns (string[] memory cmds) { + return _newCommand(DIFF_MODE, _variant, _argCount); + } + + function _newCommand( + string memory _mode, + string memory _variant, + uint256 _argCount + ) + private + pure + returns (string[] memory cmds) + { + cmds = new string[](3 + _argCount); + cmds[0] = GO_FFI; + cmds[1] = _mode; + cmds[2] = _variant; + } + + function _setCrossDomainArgs( + string[] memory _cmds, + uint256 _nonce, + address _sender, + address _target, + uint256 _value, + uint256 _gasLimit, + bytes memory _data + ) + private + pure + { + _cmds[3] = vm.toString(_nonce); + _cmds[4] = vm.toString(_sender); + _cmds[5] = vm.toString(_target); + _cmds[6] = vm.toString(_value); + _cmds[7] = vm.toString(_gasLimit); + _cmds[8] = vm.toString(_data); } } diff --git a/test/setup/FeatureFlags.sol b/test/setup/FeatureFlags.sol index f41543d5..a28cdeb3 100644 --- a/test/setup/FeatureFlags.sol +++ b/test/setup/FeatureFlags.sol @@ -6,39 +6,16 @@ import { Vm } from "lib/forge-std/src/Vm.sol"; // Interfaces import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; -/// @notice FeatureFlags manages the feature bitmap by either direct user input or via environment -/// variables. +/// @notice Provides helpers for checking SystemConfig-gated test behavior. abstract contract FeatureFlags { - /// @notice The address of the foundry Vm contract. Vm private constant vm = Vm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); - /// @notice The development feature bitmap. - bytes32 internal devFeatureBitmap; + ISystemConfig private sysCfg; - /// @notice The address of the SystemConfig contract. - ISystemConfig internal sysCfg; - - /// @notice Sets the address of the SystemConfig contract. - /// @param _sysCfg The address of the SystemConfig contract. - function setSystemConfig(ISystemConfig _sysCfg) public { + function setSystemConfig(ISystemConfig _sysCfg) internal { sysCfg = _sysCfg; } - /// @notice Enables a feature. - /// @param _feature The feature to set. - function setDevFeatureEnabled(bytes32 _feature) public { - devFeatureBitmap |= _feature; - } - - /// @notice Disables a feature. - /// @param _feature The feature to set. - function setDevFeatureDisabled(bytes32 _feature) public { - devFeatureBitmap &= ~_feature; - } - - /// @notice Checks if a system feature is enabled. - /// @param _feature The feature to check. - /// @return True if the feature is enabled, false otherwise. function isSysFeatureEnabled(bytes32 _feature) public view returns (bool) { return sysCfg.isFeatureEnabled(_feature); } diff --git a/test/setup/ForkLive.s.sol b/test/setup/ForkLive.s.sol index 281a82cb..2b1b0cea 100644 --- a/test/setup/ForkLive.s.sol +++ b/test/setup/ForkLive.s.sol @@ -3,9 +3,6 @@ pragma solidity ^0.8.0; import { console2 as console } from "lib/forge-std/src/console2.sol"; import { Script } from "lib/forge-std/src/Script.sol"; -import { StdAssertions } from "lib/forge-std/src/StdAssertions.sol"; - -import { FeatureFlags } from "test/setup/FeatureFlags.sol"; // Scripts import { Artifacts } from "scripts/Artifacts.s.sol"; @@ -15,21 +12,16 @@ import { Config } from "scripts/libraries/Config.sol"; // Libraries import { GameTypes } from "src/libraries/bridge/Types.sol"; -import { Claim } from "src/libraries/bridge/LibUDT.sol"; import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; import { Types } from "scripts/libraries/Types.sol"; // Interfaces import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; import { IDisputeGameFactory } from "interfaces/L1/proofs/IDisputeGameFactory.sol"; -import { IDisputeGame } from "interfaces/L1/proofs/IDisputeGame.sol"; -import { IAggregateVerifier } from "interfaces/L1/proofs/IAggregateVerifier.sol"; import { IAggregateVerifier } from "interfaces/L1/proofs/IAggregateVerifier.sol"; -import { IDelayedWETH } from "interfaces/L1/proofs/IDelayedWETH.sol"; import { IAddressManager } from "interfaces/legacy/IAddressManager.sol"; import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; -import { IAnchorStateRegistry } from "interfaces/L1/proofs/IAnchorStateRegistry.sol"; import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; @@ -43,13 +35,16 @@ import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; /// `forkSystemAddresses`. /// This contract must not have constructor logic because it is set into state using `etch`. -contract ForkLive is Script, StdAssertions, FeatureFlags { +contract ForkLive is Script { DeployConfig internal constant cfg = DeployConfig(address(uint160(uint256(keccak256(abi.encode("optimism.deployconfig")))))); Artifacts internal constant artifacts = Artifacts(address(uint160(uint256(keccak256(abi.encode("optimism.artifacts")))))); + SystemDeploy internal constant deploy = + SystemDeploy(address(uint160(uint256(keccak256(abi.encode("optimism.deploy")))))); + bool public useOpsRepo; struct SystemAddresses { @@ -98,22 +93,15 @@ contract ForkLive is Script, StdAssertions, FeatureFlags { useOpsRepo = bytes(superchainOpsAllocsPath).length > 0; if (useOpsRepo) { console.log("ForkLive: loading state from %s", superchainOpsAllocsPath); - // Set the resultant state from the superchain ops repo upgrades. - // The allocs are generated when simulating an upgrade task that runs vm.dumpState. - // These allocs represent the state of the EVM after the upgrade has been simulated. + // Load the simulated post-upgrade state before deriving implementation addresses. vm.loadAllocs(superchainOpsAllocsPath); - // Next, fetch the addresses from the configured fork entrypoints. This function uses a local EVM - // to retrieve implementation addresses by reading from the live proxy addresses. - // Setting the allocs first ensures the correct implementation addresses are retrieved. - _readForkAddresses(); - } else { - // Read the live system and save the addresses to the Artifacts contract. - _readForkAddresses(); - // Now deploy the updated implementations of the contracts. + } + + _readForkAddresses(); + if (!useOpsRepo) { _deployNewImplementations(); } - // Now upgrade the contracts (if the config is set to do so) if (useOpsRepo) { console.log("ForkLive: using ops repo to upgrade"); } else if (cfg.useUpgradedFork()) { @@ -189,18 +177,16 @@ contract ForkLive is Script, StdAssertions, FeatureFlags { /// @notice Calls to the SystemDeploy.s.sol contract etched by Setup.sol to a deterministic address, sets up the /// environment, and deploys new implementations. function _deployNewImplementations() internal { - SystemDeploy deploy = SystemDeploy(address(uint160(uint256(keccak256(abi.encode("optimism.deploy")))))); deploy.deployImplementations(); } /// @notice Performs a script-level upgrade without a manager delegatecall. /// @param _upgrader The address of the OP Chain ProxyAdmin owner to use for the chain upgrade. - function _doUpgrade(address _upgrader) internal { + /// @param _systemConfigProxy The OP Chain SystemConfig proxy to upgrade. + function _doUpgrade(address _upgrader, ISystemConfig _systemConfigProxy) internal { SystemDeploy systemDeploy = new SystemDeploy(); Types.Implementations memory implementations = _latestImplementations(); - ISystemConfig systemConfigProxy = ISystemConfig(artifacts.mustGetAddress("SystemConfigProxy")); - ISuperchainConfig superchainConfig = ISuperchainConfig(artifacts.mustGetAddress("SuperchainConfigProxy")); IProxyAdmin superchainProxyAdmin = IProxyAdmin(EIP1967Helper.getAdmin(address(superchainConfig))); address superchainPAO = superchainProxyAdmin.owner(); @@ -224,7 +210,7 @@ contract ForkLive is Script, StdAssertions, FeatureFlags { saveArtifacts: false, superchainConfigProxy: ISuperchainConfig(address(0)), implementations: implementations, - systemConfigProxy: systemConfigProxy + systemConfigProxy: _systemConfigProxy }) ); } @@ -237,43 +223,33 @@ contract ForkLive is Script, StdAssertions, FeatureFlags { address upgrader = proxyAdmin.owner(); vm.label(upgrader, "ProxyAdmin Owner"); - // Run past upgrades depending on network. - if (block.chainid == 1) { - // Mainnet - // This is empty because the block number in the justfile is after the most recent upgrade so there are no - // past upgrades to run. - } else { + if (block.chainid != 1) { revert UnsupportedChainId(); } - // Current upgrade. - _doUpgrade(upgrader); + _doUpgrade(upgrader, systemConfig); console.log("ForkLive: Saving newly deployed contracts"); // A new ASR and new dispute games were deployed, so we need to update them IDisputeGameFactory disputeGameFactory = IDisputeGameFactory(artifacts.mustGetAddress("DisputeGameFactoryProxy")); - IDisputeGame av = disputeGameFactory.gameImpls(GameTypes.AGGREGATE_VERIFIER); - artifacts.save("AggregateVerifier", address(av)); - - IAnchorStateRegistry newAnchorStateRegistry = av.anchorStateRegistry(); - artifacts.save("AnchorStateRegistryProxy", address(newAnchorStateRegistry)); + IAggregateVerifier aggregateVerifier = + IAggregateVerifier(address(disputeGameFactory.gameImpls(GameTypes.AGGREGATE_VERIFIER))); + artifacts.save("AggregateVerifier", address(aggregateVerifier)); - // Get the lockbox address from the portal, and save it IOptimismPortal2 portal = IOptimismPortal2(artifacts.mustGetAddress("OptimismPortalProxy")); address lockboxAddress = address(portal.ethLockbox()); artifacts.save("ETHLockboxProxy", lockboxAddress); - // Get the new DelayedWETH address and save it (might be a new proxy). - IDelayedWETH newDelayedWeth = IAggregateVerifier(address(av)).DELAYED_WETH(); - artifacts.save("DelayedWETHProxy", address(newDelayedWeth)); - artifacts.save("DelayedWETHImpl", EIP1967Helper.getImplementation(address(newDelayedWeth))); + GameAddresses memory gameAddresses = _aggregateVerifierAddresses(aggregateVerifier); + artifacts.save("AnchorStateRegistryProxy", gameAddresses.anchorStateRegistry); + artifacts.save("DelayedWETHProxy", gameAddresses.weth); + artifacts.save("DelayedWETHImpl", EIP1967Helper.getImplementation(gameAddresses.weth)); } /// @notice Returns the latest implementation set saved by deployImplementations. function _latestImplementations() internal view returns (Types.Implementations memory) { - SystemDeploy deploy = SystemDeploy(address(uint160(uint256(keccak256(abi.encode("optimism.deploy")))))); return deploy.getImplementations(); } diff --git a/test/setup/Setup.sol b/test/setup/Setup.sol index dd70a798..a415ccfd 100644 --- a/test/setup/Setup.sol +++ b/test/setup/Setup.sol @@ -8,6 +8,7 @@ import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; import { FeatureFlags } from "test/setup/FeatureFlags.sol"; // Scripts +import { DeployConfig } from "scripts/deploy/DeployConfig.s.sol"; import { SystemDeploy } from "scripts/deploy/SystemDeploy.s.sol"; import { ForkLive } from "test/setup/ForkLive.s.sol"; import { LATEST_FORK } from "scripts/libraries/Config.sol"; @@ -48,11 +49,10 @@ import { IGasPriceOracle } from "interfaces/L2/IGasPriceOracle.sol"; import { IL1Block } from "interfaces/L2/IL1Block.sol"; import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; import { IWETH98 } from "interfaces/universal/IWETH98.sol"; -import { IVerifier } from "interfaces/L1/proofs/IVerifier.sol"; import { TEEProverRegistry } from "src/L1/proofs/tee/TEEProverRegistry.sol"; /// @title Setup -/// @dev This contact is responsible for setting up the contracts in state. It currently +/// @dev This contract is responsible for setting up the contracts in state. It currently /// sets the L2 contracts directly at the predeploy addresses instead of setting them /// up behind proxies. In the future we will migrate to importing the genesis JSON /// file that is created to set up the L2 contracts instead of setting them up manually. @@ -123,7 +123,6 @@ abstract contract Setup is FeatureFlags { IGasPriceOracle gasPriceOracle = IGasPriceOracle(Predeploys.GAS_PRICE_ORACLE); IL1Block l1Block = IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES); IWETH98 weth = IWETH98(payable(Predeploys.WETH)); - IVerifier aggregateVerifier; TEEProverRegistry teeProverRegistry; /// @notice Indicates whether a test is running against a forked production network. @@ -131,13 +130,7 @@ abstract contract Setup is FeatureFlags { return Config.forkTest(); } - /// @notice Indicates whether a test is running against a forked network that is OP. - function isOpFork() public view returns (bool) { - string memory opChain = Config.forkOpChain(); - return keccak256(bytes(opChain)) == keccak256(bytes("op")); - } - - /// @dev Deploys either the SystemDeploy.s.sol or Fork.s.sol contract, by fetching the bytecode dynamically using + /// @dev Deploys the SystemDeploy and ForkLive setup contracts by fetching the bytecode dynamically using /// `vm.getDeployedCode()` and etching it into the state. /// This enables us to avoid including the bytecode of those contracts in the bytecode of this contract. /// If the bytecode of those contracts was included in this contract, then it will double @@ -147,7 +140,8 @@ abstract contract Setup is FeatureFlags { function setUp() public virtual { console.log("Setup: L1 setup start!"); - if (isForkTest()) { + bool forkTest = isForkTest(); + if (forkTest) { vm.createSelectFork(Config.forkRpcUrl(), Config.forkBlockNumber()); console.log("Setup: fork selected!"); require( @@ -164,15 +158,13 @@ abstract contract Setup is FeatureFlags { console.log("Setup: L1 setup done!"); - if (isForkTest()) { - // Return early if this is a fork test as we don't need to setup L2 + if (forkTest) { console.log("Setup: fork test detected, skipping L2 genesis generation"); return; } console.log("Setup: L2 setup start!"); - vm.etch(address(l2Genesis), vm.getDeployedCode("L2Genesis.s.sol:L2Genesis")); - vm.allowCheatcodes(address(l2Genesis)); + DeployUtils.etchLabelAndAllowCheatcodes({ _etchTo: address(l2Genesis), _cname: "L2Genesis" }); console.log("Setup: L2 setup done!"); } @@ -213,13 +205,10 @@ abstract contract Setup is FeatureFlags { /// @dev Sets up the L1 contracts. function L1() public { console.log("Setup: creating L1 deployments"); - // Set the deterministic deployer in state to ensure that it is there - vm.etch( - 0x4e59b44847b379578588920cA78FbF26c0B4956C, - hex"7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf3" - ); + vm.etch(Preinstalls.DeterministicDeploymentProxy, Preinstalls.DeterministicDeploymentProxyCode); - if (isForkTest()) { + bool forkTest = isForkTest(); + if (forkTest) { forkLive.run(); } else { deploy.run(); @@ -231,7 +220,7 @@ abstract contract Setup is FeatureFlags { // Only skip ETHLockbox assignment if we're in a fork test with non-upgraded fork // TODO(#14691): Remove this check once Upgrade 15 is deployed on Mainnet. - if (!isForkTest() || deploy.cfg().useUpgradedFork()) { + if (!forkTest || deploy.cfg().useUpgradedFork()) { // Here we use getAddress instead of mustGetAddress because some chains might not have // the ETHLockbox proxy. Chains that don't have the ETHLockbox proxy will just return // address(0) and cause a revert if we use mustGetAddress. @@ -256,7 +245,6 @@ abstract contract Setup is FeatureFlags { proxyAdminOwner = proxyAdmin.owner(); superchainProxyAdmin = IProxyAdmin(EIP1967Helper.getAdmin(address(superchainConfig))); superchainProxyAdminOwner = superchainProxyAdmin.owner(); - aggregateVerifier = IVerifier(artifacts.getAddress("AggregateVerifier")); teeProverRegistry = TEEProverRegistry(artifacts.getAddress("TEEProverRegistry")); console.log("Setup: registered L1 deployments"); @@ -274,28 +262,29 @@ abstract contract Setup is FeatureFlags { } console.log("Setup: creating L2 genesis with fork %s", l2Fork.toString()); + DeployConfig cfg = deploy.cfg(); l2Genesis.run( L2Genesis.Input({ - l1ChainID: deploy.cfg().l1ChainId(), - l2ChainID: deploy.cfg().l2ChainId(), + l1ChainID: cfg.l1ChainId(), + l2ChainID: cfg.l2ChainId(), l1CrossDomainMessengerProxy: payable(address(l1CrossDomainMessenger)), l1StandardBridgeProxy: payable(address(l1StandardBridge)), l1ERC721BridgeProxy: payable(address(l1ERC721Bridge)), - opChainProxyAdminOwner: deploy.cfg().proxyAdminOwner(), - sequencerFeeVaultRecipient: deploy.cfg().sequencerFeeVaultRecipient(), - sequencerFeeVaultMinimumWithdrawalAmount: deploy.cfg().sequencerFeeVaultMinimumWithdrawalAmount(), - sequencerFeeVaultWithdrawalNetwork: deploy.cfg().sequencerFeeVaultWithdrawalNetwork(), - baseFeeVaultRecipient: deploy.cfg().baseFeeVaultRecipient(), - baseFeeVaultMinimumWithdrawalAmount: deploy.cfg().baseFeeVaultMinimumWithdrawalAmount(), - baseFeeVaultWithdrawalNetwork: deploy.cfg().baseFeeVaultWithdrawalNetwork(), - l1FeeVaultRecipient: deploy.cfg().l1FeeVaultRecipient(), - l1FeeVaultMinimumWithdrawalAmount: deploy.cfg().l1FeeVaultMinimumWithdrawalAmount(), - l1FeeVaultWithdrawalNetwork: deploy.cfg().l1FeeVaultWithdrawalNetwork(), - operatorFeeVaultRecipient: deploy.cfg().operatorFeeVaultRecipient(), - operatorFeeVaultMinimumWithdrawalAmount: deploy.cfg().operatorFeeVaultMinimumWithdrawalAmount(), - operatorFeeVaultWithdrawalNetwork: deploy.cfg().operatorFeeVaultWithdrawalNetwork(), + opChainProxyAdminOwner: cfg.proxyAdminOwner(), + sequencerFeeVaultRecipient: cfg.sequencerFeeVaultRecipient(), + sequencerFeeVaultMinimumWithdrawalAmount: cfg.sequencerFeeVaultMinimumWithdrawalAmount(), + sequencerFeeVaultWithdrawalNetwork: cfg.sequencerFeeVaultWithdrawalNetwork(), + baseFeeVaultRecipient: cfg.baseFeeVaultRecipient(), + baseFeeVaultMinimumWithdrawalAmount: cfg.baseFeeVaultMinimumWithdrawalAmount(), + baseFeeVaultWithdrawalNetwork: cfg.baseFeeVaultWithdrawalNetwork(), + l1FeeVaultRecipient: cfg.l1FeeVaultRecipient(), + l1FeeVaultMinimumWithdrawalAmount: cfg.l1FeeVaultMinimumWithdrawalAmount(), + l1FeeVaultWithdrawalNetwork: cfg.l1FeeVaultWithdrawalNetwork(), + operatorFeeVaultRecipient: cfg.operatorFeeVaultRecipient(), + operatorFeeVaultMinimumWithdrawalAmount: cfg.operatorFeeVaultMinimumWithdrawalAmount(), + operatorFeeVaultWithdrawalNetwork: cfg.operatorFeeVaultWithdrawalNetwork(), fork: uint256(l2Fork), - fundDevAccounts: deploy.cfg().fundDevAccounts() + fundDevAccounts: cfg.fundDevAccounts() }) ); diff --git a/test/universal/Counter.sol b/test/universal/Counter.sol index 65bb4e18..5517cf5c 100644 --- a/test/universal/Counter.sol +++ b/test/universal/Counter.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.25; contract Counter { address internal immutable OWNER; - uint256 public count = 0; + uint256 public count; constructor(address owner) { OWNER = owner; @@ -16,7 +16,7 @@ contract Counter { } function incrementPayable() external payable { - require(msg.value != 0, "value must be greater than 0"); + require(msg.value > 0, "value must be greater than 0"); count += 1; } } diff --git a/test/universal/CrossDomainMessenger.t.sol b/test/universal/CrossDomainMessenger.t.sol index 5efd4bad..bcd09724 100644 --- a/test/universal/CrossDomainMessenger.t.sol +++ b/test/universal/CrossDomainMessenger.t.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.15; // Testing utilities import { Test } from "lib/forge-std/src/Test.sol"; import { CommonTest } from "test/setup/CommonTest.sol"; +import { ForgeArtifacts, StorageSlot } from "scripts/libraries/ForgeArtifacts.sol"; // Libraries import { Math } from "lib/openzeppelin-contracts/contracts/utils/math/Math.sol"; @@ -17,9 +18,9 @@ import { IL1CrossDomainMessenger } from "interfaces/L1/IL1CrossDomainMessenger.s /// @notice A mock external contract called via the SafeCall inside the CrossDomainMessenger's /// `relayMessage` function. contract CrossDomainMessenger_ExternalRelay_Harness is Test { - address internal op; + address internal immutable op; address internal fuzzedSender; - IL1CrossDomainMessenger internal l1CrossDomainMessenger; + IL1CrossDomainMessenger internal immutable l1CrossDomainMessenger; event FailedRelayedMessage(bytes32 indexed msgHash); @@ -28,19 +29,12 @@ contract CrossDomainMessenger_ExternalRelay_Harness is Test { op = _op; } - /// @notice Internal helper function to relay a message and perform assertions. - function _internalRelay(address _innerSender) internal { + function _internalRelay(address _innerSender, bytes memory _callMessage) internal { address initialSender = l1CrossDomainMessenger.xDomainMessageSender(); - - bytes memory callMessage = getCallData(); + uint256 nonce = Encoding.encodeVersionedNonce({ _nonce: 0, _version: 1 }); bytes32 hash = Hashing.hashCrossDomainMessage({ - _nonce: Encoding.encodeVersionedNonce({ _nonce: 0, _version: 1 }), - _sender: _innerSender, - _target: address(this), - _value: 0, - _gasLimit: 0, - _data: callMessage + _nonce: nonce, _sender: _innerSender, _target: address(this), _value: 0, _gasLimit: 0, _data: _callMessage }); vm.expectEmit(true, true, true, true); @@ -48,12 +42,12 @@ contract CrossDomainMessenger_ExternalRelay_Harness is Test { vm.prank(address(op)); l1CrossDomainMessenger.relayMessage({ - _nonce: Encoding.encodeVersionedNonce({ _nonce: 0, _version: 1 }), + _nonce: nonce, _sender: _innerSender, _target: address(this), _value: 0, _minGasLimit: 0, - _message: callMessage + _message: _callMessage }); assertTrue(l1CrossDomainMessenger.failedMessages(hash)); @@ -63,21 +57,21 @@ contract CrossDomainMessenger_ExternalRelay_Harness is Test { /// @notice externalCallWithMinGas is called by the CrossDomainMessenger. function externalCallWithMinGas() external payable { + bytes memory callMessage = getCallData(); + for (uint256 i = 0; i < 10; i++) { address _innerSender; unchecked { _innerSender = address(uint160(uint256(uint160(fuzzedSender)) + i)); } - _internalRelay(_innerSender); + _internalRelay(_innerSender, callMessage); } } - /// @notice Helper function to get the callData for an `externalCallWithMinGas function getCallData() public pure returns (bytes memory) { return abi.encodeCall(CrossDomainMessenger_ExternalRelay_Harness.externalCallWithMinGas, ()); } - /// @notice Helper function to set the fuzzed sender function setFuzzedSender(address _fuzzedSender) public { fuzzedSender = _fuzzedSender; } @@ -86,15 +80,26 @@ contract CrossDomainMessenger_ExternalRelay_Harness is Test { /// @title CrossDomainMessenger_TestInit /// @notice Reusable test initialization for `CrossDomainMessenger` tests. abstract contract CrossDomainMessenger_TestInit is CommonTest { - // Storage slot of the l2Sender - uint256 constant senderSlotIndex = 50; - CrossDomainMessenger_ExternalRelay_Harness public er; function setUp() public override { super.setUp(); er = new CrossDomainMessenger_ExternalRelay_Harness(l1CrossDomainMessenger, address(optimismPortal2)); } + + function _versionedNonce(uint16 _version) internal pure returns (uint256) { + return Encoding.encodeVersionedNonce({ _nonce: 0, _version: _version }); + } + + function _setPortalL2Sender(address _sender) internal { + StorageSlot memory senderSlot = ForgeArtifacts.getSlot("OptimismPortal2", "l2Sender"); + vm.store(address(optimismPortal2), bytes32(senderSlot.slot), bytes32(abi.encode(_sender))); + } + + function _assertMessageStatus(bytes32 _hash, bool _successful, bool _failed) internal view { + assertEq(l1CrossDomainMessenger.successfulMessages(_hash), _successful); + assertEq(l1CrossDomainMessenger.failedMessages(_hash), _failed); + } } /// @title CrossDomainMessenger_RelayMessage_Test @@ -118,32 +123,20 @@ contract CrossDomainMessenger_RelayMessage_Test is CrossDomainMessenger_TestInit vm.expectCall(target, callMessage); uint64 gasLimit = uint64(bound(_gasLimit, 0, 30_000_000)); + uint256 nonce = _versionedNonce(1); bytes32 hash = Hashing.hashCrossDomainMessage({ - _nonce: Encoding.encodeVersionedNonce({ _nonce: 0, _version: 1 }), - _sender: sender, - _target: target, - _value: 0, - _gasLimit: gasLimit, - _data: callMessage + _nonce: nonce, _sender: sender, _target: target, _value: 0, _gasLimit: gasLimit, _data: callMessage }); - // Set the value of `op.l2Sender()` to be the L2 Cross Domain Messenger. - vm.store(address(optimismPortal2), bytes32(senderSlotIndex), bytes32(abi.encode(sender))); + _setPortalL2Sender(sender); vm.prank(address(optimismPortal2)); l1CrossDomainMessenger.relayMessage({ - _nonce: Encoding.encodeVersionedNonce({ _nonce: 0, _version: 1 }), - _sender: sender, - _target: target, - _value: 0, - _minGasLimit: gasLimit, - _message: callMessage + _nonce: nonce, _sender: sender, _target: target, _value: 0, _minGasLimit: gasLimit, _message: callMessage }); - assertTrue(l1CrossDomainMessenger.successfulMessages(hash)); - assertEq(l1CrossDomainMessenger.failedMessages(hash), false); + _assertMessageStatus(hash, true, false); - // Ensures that the `xDomainMsgSender` is set back to `Predeploys.L2_CROSS_DOMAIN_MESSENGER` vm.expectRevert("CrossDomainMessenger: xDomainMessageSender is not set"); l1CrossDomainMessenger.xDomainMessageSender(); } @@ -153,6 +146,40 @@ contract CrossDomainMessenger_RelayMessage_Test is CrossDomainMessenger_TestInit /// L2 CrossDomainMessenger contracts. For simplicity, we use the L1 Messenger as the test /// contract. contract CrossDomainMessenger_BaseGas_Test is CommonTest { + function _totalMessageSize(uint256 _messageLength) internal view returns (uint64) { + return uint64(_messageLength + l1CrossDomainMessenger.ENCODING_OVERHEAD()); + } + + function _executionGas(uint32 _minGasLimit) internal view returns (uint64) { + return uint64( + l1CrossDomainMessenger.RELAY_CONSTANT_OVERHEAD() + l1CrossDomainMessenger.RELAY_CALL_OVERHEAD() + + l1CrossDomainMessenger.RELAY_RESERVED_GAS() + l1CrossDomainMessenger.RELAY_GAS_CHECK_BUFFER() + + ((_minGasLimit * l1CrossDomainMessenger.MIN_GAS_DYNAMIC_OVERHEAD_NUMERATOR()) + / l1CrossDomainMessenger.MIN_GAS_DYNAMIC_OVERHEAD_DENOMINATOR()) + ); + } + + function _floorGas(uint256 _messageLength) internal view returns (uint64) { + return l1CrossDomainMessenger.TX_BASE_GAS() + + (_totalMessageSize(_messageLength) * l1CrossDomainMessenger.FLOOR_CALLDATA_OVERHEAD()); + } + + function _executionGasWithOverhead(uint256 _messageLength, uint32 _minGasLimit) internal view returns (uint64) { + return l1CrossDomainMessenger.TX_BASE_GAS() + _executionGas(_minGasLimit) + + (_totalMessageSize(_messageLength) * l1CrossDomainMessenger.MIN_GAS_CALLDATA_OVERHEAD()); + } + + function _baseGasMinimum(uint256 _messageLength, uint32 _minGasLimit) internal view returns (uint64) { + uint64 totalMessageSize = _totalMessageSize(_messageLength); + return l1CrossDomainMessenger.TX_BASE_GAS() + + uint64( + Math.max( + _executionGas(_minGasLimit) + (totalMessageSize * l1CrossDomainMessenger.MIN_GAS_CALLDATA_OVERHEAD()), + totalMessageSize * l1CrossDomainMessenger.FLOOR_CALLDATA_OVERHEAD() + ) + ); + } + /// @notice Ensure that `baseGas` passes for the max value of `_minGasLimit`, this is about /// 4 Billion. function test_baseGas_succeeds() external view { @@ -179,50 +206,26 @@ contract CrossDomainMessenger_BaseGas_Test is CommonTest { /// @notice Test that `baseGas` returns at least the floor cost for calldata function test_baseGas_floor_succeeds() external view { - // Create a message large enough that the floor cost would be higher than the execution gas bytes memory largeMessage = new bytes(100_000); uint64 baseGasResult = l1CrossDomainMessenger.baseGas(largeMessage, 0); - // Calculate the expected floor cost - uint64 expectedFloorCost = l1CrossDomainMessenger.TX_BASE_GAS() - + (uint64(largeMessage.length + l1CrossDomainMessenger.ENCODING_OVERHEAD()) - * l1CrossDomainMessenger.FLOOR_CALLDATA_OVERHEAD()); - - // Verify that the result is at least the floor cost - assertTrue(baseGasResult >= expectedFloorCost, "baseGas should return at least the floor cost"); + assertGe(baseGasResult, _floorGas(largeMessage.length), "baseGas should return at least the floor cost"); } /// @notice Test that `baseGas` returns the execution gas when it's higher than the floor cost function test_baseGas_executionGas_succeeds() external view { - // Create a small message where execution gas would be higher than floor cost bytes memory smallMessage = new bytes(10); uint32 highGasLimit = 1_000_000; uint64 baseGasResult = l1CrossDomainMessenger.baseGas(smallMessage, highGasLimit); + uint64 expectedExecutionGasWithOverhead = _executionGasWithOverhead(smallMessage.length, highGasLimit); - // Calculate the expected floor cost - uint64 floorCost = l1CrossDomainMessenger.TX_BASE_GAS() - + (uint64(smallMessage.length + l1CrossDomainMessenger.ENCODING_OVERHEAD()) - * l1CrossDomainMessenger.FLOOR_CALLDATA_OVERHEAD()); - - // Calculate the expected execution gas (simplified version of what's in the contract) - uint64 executionGas = l1CrossDomainMessenger.RELAY_CONSTANT_OVERHEAD() - + l1CrossDomainMessenger.RELAY_CALL_OVERHEAD() + l1CrossDomainMessenger.RELAY_RESERVED_GAS() - + l1CrossDomainMessenger.RELAY_GAS_CHECK_BUFFER() - + ((highGasLimit * l1CrossDomainMessenger.MIN_GAS_DYNAMIC_OVERHEAD_NUMERATOR()) - / l1CrossDomainMessenger.MIN_GAS_DYNAMIC_OVERHEAD_DENOMINATOR()); - - uint64 expectedExecutionGasWithOverhead = l1CrossDomainMessenger.TX_BASE_GAS() + executionGas - + (uint64(smallMessage.length + l1CrossDomainMessenger.ENCODING_OVERHEAD()) - * l1CrossDomainMessenger.MIN_GAS_CALLDATA_OVERHEAD()); - - // Verify that the result is the execution gas (which should be higher than floor cost) - assertTrue( - baseGasResult >= expectedExecutionGasWithOverhead, "baseGas should return at least the execution gas" - ); - assertTrue( - expectedExecutionGasWithOverhead > floorCost, "Execution gas should be higher than floor cost for this test" + assertGe(baseGasResult, expectedExecutionGasWithOverhead, "baseGas should return at least the execution gas"); + assertGt( + expectedExecutionGasWithOverhead, + _floorGas(smallMessage.length), + "Execution gas should be higher than floor cost for this test" ); } @@ -232,31 +235,8 @@ contract CrossDomainMessenger_BaseGas_Test is CommonTest { /// @param _minGasLimit The minimum gas limit to test function testFuzz_baseGas_maxLogic_succeeds(bytes calldata _message, uint32 _minGasLimit) external view { uint64 baseGasResult = l1CrossDomainMessenger.baseGas(_message, _minGasLimit); + uint64 expectedMinimum = _baseGasMinimum(_message.length, _minGasLimit); - // Calculate the expected execution gas - uint64 executionGas = l1CrossDomainMessenger.RELAY_CONSTANT_OVERHEAD() - + l1CrossDomainMessenger.RELAY_CALL_OVERHEAD() + l1CrossDomainMessenger.RELAY_RESERVED_GAS() - + l1CrossDomainMessenger.RELAY_GAS_CHECK_BUFFER() - + ((_minGasLimit * l1CrossDomainMessenger.MIN_GAS_DYNAMIC_OVERHEAD_NUMERATOR()) - / l1CrossDomainMessenger.MIN_GAS_DYNAMIC_OVERHEAD_DENOMINATOR()); - - uint64 executionGasWithOverhead = executionGas - + (uint64(_message.length + l1CrossDomainMessenger.ENCODING_OVERHEAD()) - * l1CrossDomainMessenger.MIN_GAS_CALLDATA_OVERHEAD()); - - // The result should be at least the maximum of the two calculations - uint64 expectedMinimum = uint64( - Math.max( - executionGasWithOverhead, - uint64(_message.length + l1CrossDomainMessenger.ENCODING_OVERHEAD()) - * l1CrossDomainMessenger.FLOOR_CALLDATA_OVERHEAD() - ) - ); - expectedMinimum += l1CrossDomainMessenger.TX_BASE_GAS(); - - assertTrue( - baseGasResult >= expectedMinimum, - "baseGas should return at least the maximum of execution gas and floor cost" - ); + assertGe(baseGasResult, expectedMinimum, "baseGas should return at least the max gas path"); } } diff --git a/test/universal/ExtendedPause.t.sol b/test/universal/ExtendedPause.t.sol index 16dc061a..19e2b171 100644 --- a/test/universal/ExtendedPause.t.sol +++ b/test/universal/ExtendedPause.t.sol @@ -9,34 +9,35 @@ import { CommonTest } from "test/setup/CommonTest.sol"; /// that the behavior is consistent. contract ExtendedPause_Test is CommonTest { /// @notice Tests that other contracts are paused when the superchain config is paused - function test_pause_fullSystem_succeeds() public { - assertFalse(superchainConfig.paused(address(0))); + function test_pause_fullSystem_succeeds() external { + _assertFullSystemPaused(false); - vm.prank(superchainConfig.guardian()); - superchainConfig.pause(address(0)); + _pauseSuperchain(); - // validate the paused state - assertTrue(superchainConfig.paused(address(0))); - assertTrue(optimismPortal2.paused()); - assertTrue(l1CrossDomainMessenger.paused()); - assertTrue(l1StandardBridge.paused()); - assertTrue(l1ERC721Bridge.paused()); + _assertFullSystemPaused(true); } /// @notice Tests that other contracts are unpaused when the superchain config is paused and /// then unpaused. function test_unpause_fullSystem_succeeds() external { - // first use the test above to pause the system - test_pause_fullSystem_succeeds(); + _pauseSuperchain(); vm.prank(superchainConfig.guardian()); superchainConfig.unpause(address(0)); - // validate the unpaused state - assertFalse(superchainConfig.paused(address(0))); - assertFalse(optimismPortal2.paused()); - assertFalse(l1CrossDomainMessenger.paused()); - assertFalse(l1StandardBridge.paused()); - assertFalse(l1ERC721Bridge.paused()); + _assertFullSystemPaused(false); + } + + function _pauseSuperchain() internal { + vm.prank(superchainConfig.guardian()); + superchainConfig.pause(address(0)); + } + + function _assertFullSystemPaused(bool _paused) internal view { + assertEq(superchainConfig.paused(address(0)), _paused); + assertEq(optimismPortal2.paused(), _paused); + assertEq(l1CrossDomainMessenger.paused(), _paused); + assertEq(l1StandardBridge.paused(), _paused); + assertEq(l1ERC721Bridge.paused(), _paused); } } diff --git a/test/universal/MultisigScript.t.sol b/test/universal/MultisigScript.t.sol index d9d89f3e..82baf76b 100644 --- a/test/universal/MultisigScript.t.sol +++ b/test/universal/MultisigScript.t.sol @@ -20,7 +20,7 @@ contract MultisigScriptTest is Test, MultisigScript { Vm.Wallet internal wallet3 = vm.createWallet("3"); address internal safe = address(1001); - Counter internal counter = new Counter(address(safe)); + Counter internal counter = new Counter(safe); /// @dev Controls whether to use hash-based or EIP-712 JSON output. True by default. bool internal _useDataHashes = true; @@ -58,7 +58,7 @@ contract MultisigScriptTest is Test, MultisigScript { assertEq(counterValue, 6, "Counter value is not 6"); uint256 counterBalance = address(counter).balance; - assertEq(counterBalance, 3 ether, "Counter balance is not 1 ether"); + assertEq(counterBalance, 3 ether, "Counter balance is not 3 ether"); } /// @inheritdoc MultisigScript @@ -94,12 +94,7 @@ contract MultisigScriptTest is Test, MultisigScript { Call[] memory calls = new Call[](4); - calls[0] = Call({ - operation: Enum.Operation.Call, - target: address(counter), - data: abi.encodeCall(Counter.increment, ()), - value: 0 - }); + calls[0] = counterIncrementCall; // Use multicall to test the delegatecall use case calls[1] = Call({ @@ -109,12 +104,7 @@ contract MultisigScriptTest is Test, MultisigScript { value: 0 }); - calls[2] = Call({ - operation: Enum.Operation.Call, - target: address(counter), - data: abi.encodeCall(Counter.incrementPayable, ()), - value: 1 ether - }); + calls[2] = counterIncrementCallPayable; calls[3] = Call({ operation: Enum.Operation.DelegateCall, @@ -128,12 +118,10 @@ contract MultisigScriptTest is Test, MultisigScript { /// @inheritdoc MultisigScript function _ownerSafe() internal view override returns (address) { - return address(safe); + return safe; } /// @inheritdoc MultisigScript - /// - /// @dev Returns `_useDataHashes` which is true by default (hash-based signing). function _printDataHashes() internal view override returns (bool) { return _useDataHashes; } @@ -145,6 +133,16 @@ contract MultisigScriptTest is Test, MultisigScript { return _encodeTransactionData(_ownerSafe(), _buildAggregatedScriptCall({ scriptCalls: _buildCalls() })); } + function _signDigest(Vm.Wallet memory wallet, bytes32 digest) internal returns (bytes memory) { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(wallet, digest); + return abi.encodePacked(r, s, v); + } + + function _lastLoggedBytes() internal returns (bytes memory) { + Vm.Log[] memory logs = vm.getRecordedLogs(); + return abi.decode(logs[logs.length - 1].data, (bytes)); + } + /// @notice Tests that sign() emits the correct data to sign function test_sign() external { vm.recordLogs(); @@ -152,43 +150,33 @@ contract MultisigScriptTest is Test, MultisigScript { vm.prank(wallet1.addr); this.sign(new address[](0)); - Vm.Log[] memory logs = vm.getRecordedLogs(); - bytes memory logged = abi.decode(logs[logs.length - 1].data, (bytes)); + bytes memory logged = _lastLoggedBytes(); bytes memory expected = _expectedTxDataForCurrentBuildCalls(); - assertEq(keccak256(logged), keccak256(expected)); + assertEq(logged, expected); } /// @notice Tests that verify() accepts valid 2-of-2 signatures function test_verify_valid_signatures() external { - // Two-of-two signatures over the encoded transaction data should verify bytes32 digest = keccak256(_expectedTxDataForCurrentBuildCalls()); - (uint8 v1, bytes32 r1, bytes32 s1) = vm.sign(wallet1, digest); - (uint8 v2, bytes32 r2, bytes32 s2) = vm.sign(wallet2, digest); - bytes memory signatures = abi.encodePacked(r1, s1, v1, r2, s2, v2); + bytes memory signatures = abi.encodePacked(_signDigest(wallet1, digest), _signDigest(wallet2, digest)); verify(new address[](0), signatures); } /// @notice Tests that verify() reverts when given an invalid signature function test_verify_reverts_with_invalid_signature() external { - // One valid, one invalid should revert bytes32 digest = keccak256(_expectedTxDataForCurrentBuildCalls()); - (uint8 v1, bytes32 r1, bytes32 s1) = vm.sign(wallet1, digest); - bytes memory signatures = abi.encodePacked(r1, s1, v1, bytes32(0), bytes32(0), uint8(27)); - bytes memory callData = abi.encodeCall(this.verify, (new address[](0), signatures)); - (bool success, bytes memory ret) = address(this).call(callData); - assertFalse(success); - assertTrue(ret.length > 0); + bytes memory signatures = abi.encodePacked(_signDigest(wallet1, digest), bytes32(0), bytes32(0), uint8(27)); + + vm.expectRevert(); // nosemgrep: sol-safety-expectrevert-no-args + this.verify(new address[](0), signatures); } /// @notice Tests that simulate() executes the transaction without broadcasting function test_simulate_only() external { bytes32 digest = keccak256(_expectedTxDataForCurrentBuildCalls()); - (uint8 v1, bytes32 r1, bytes32 s1) = vm.sign(wallet1, digest); - (uint8 v2, bytes32 r2, bytes32 s2) = vm.sign(wallet2, digest); - bytes memory signatures = abi.encodePacked(r1, s1, v1, r2, s2, v2); + bytes memory signatures = abi.encodePacked(_signDigest(wallet1, digest), _signDigest(wallet2, digest)); - // Simulate should execute successfully and satisfy _postCheck simulate(signatures); } @@ -196,13 +184,9 @@ contract MultisigScriptTest is Test, MultisigScript { /// /// @dev Safe is 2/3, but we provide all 3 signatures function test_run_with_more_signatures_than_threshold() external { - // Sign with all 3 owners (threshold is 2, but we provide 3) bytes32 digest = keccak256(_expectedTxDataForCurrentBuildCalls()); - (uint8 v1, bytes32 r1, bytes32 s1) = vm.sign(wallet1, digest); - (uint8 v2, bytes32 r2, bytes32 s2) = vm.sign(wallet2, digest); - (uint8 v3, bytes32 r3, bytes32 s3) = vm.sign(wallet3, digest); - - bytes memory signatures = abi.encodePacked(r1, s1, v1, r2, s2, v2, r3, s3, v3); + bytes memory signatures = + abi.encodePacked(_signDigest(wallet1, digest), _signDigest(wallet2, digest), _signDigest(wallet3, digest)); run(signatures); } @@ -217,10 +201,8 @@ contract MultisigScriptTest is Test, MultisigScript { vm.prank(wallet1.addr); this.sign(new address[](0)); - Vm.Log[] memory logs = vm.getRecordedLogs(); - bytes memory logged = abi.decode(logs[logs.length - 1].data, (bytes)); + bytes memory logged = _lastLoggedBytes(); - // Verify the logged data contains EIP-712 JSON structure markers string memory loggedStr = string(logged); assertTrue(LibString.contains(loggedStr, "EIP712Domain"), "EIP-712 output should contain EIP712Domain"); assertTrue(LibString.contains(loggedStr, "SafeTx"), "EIP-712 output should contain SafeTx type"); @@ -280,12 +262,24 @@ abstract contract MultisigScriptNestedBase is Test, MultisigScript { } function _assertSignOutput(address[] memory safes, bytes memory dataToSign) internal { - bytes memory txData = abi.encodeWithSelector(this.sign.selector, safes); - (bool success,) = address(this).call(txData); - assertTrue(success); + this.sign(safes); + assertEq(_lastLoggedBytes(), dataToSign); + } + + function _signData(Vm.Wallet memory wallet, bytes memory dataToSign) internal returns (bytes memory) { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(wallet, keccak256(dataToSign)); + return abi.encodePacked(r, s, v); + } + + function _approveFrom(address signerSafe, Vm.Wallet memory wallet) internal { + (address[] memory safes, bytes memory dataToSign) = _getSignerData(signerSafe); + approve(safes, _signData(wallet, dataToSign)); + } + + function _lastLoggedBytes() internal returns (bytes memory) { Vm.Log[] memory logs = vm.getRecordedLogs(); - assertEq(keccak256(logs[logs.length - 1].data), keccak256(abi.encode(dataToSign))); + return abi.decode(logs[logs.length - 1].data, (bytes)); } function _signerSafes(address signerSafe) internal view virtual returns (address[] memory safes); @@ -299,7 +293,7 @@ contract MultisigScriptNestedTest is MultisigScriptNestedBase { vm.etch(safe2, safeCode); vm.etch(safe3, safeCode); - nestedCounter = new Counter(address(safe3)); + nestedCounter = new Counter(safe3); _setupLeafSafes(); @@ -339,55 +333,37 @@ contract MultisigScriptNestedTest is MultisigScriptNestedBase { /// @notice Tests that approve() succeeds with valid signature from safe1 function test_approve_safe1() external { - (address[] memory safes, bytes memory dataToSign) = _getSignerData(safe1); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(nestedWallet1, keccak256(dataToSign)); - approve(safes, abi.encodePacked(r, s, v)); + _approveFrom(safe1, nestedWallet1); } /// @notice Tests that approve() succeeds with valid signature from safe2 function test_approve_safe2() external { - (address[] memory safes, bytes memory dataToSign) = _getSignerData(safe2); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(nestedWallet2, keccak256(dataToSign)); - approve(safes, abi.encodePacked(r, s, v)); + _approveFrom(safe2, nestedWallet2); } /// @notice Tests that approve() fails when signature doesn't match the safe function test_approve_notOwner() external { (, bytes memory dataToSign) = _getSignerData(safe1); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(nestedWallet1, keccak256(dataToSign)); - (address[] memory safes2,) = _getSignerData(safe2); + bytes memory signature = _signData(nestedWallet1, dataToSign); - bytes memory data = abi.encodeCall(this.approve, (safes2, abi.encodePacked(r, s, v))); - (bool success, bytes memory result) = address(this).call(data); - assertFalse(success); - assertEq(result, abi.encodeWithSignature("Error(string)", "not enough signatures")); + vm.expectRevert("not enough signatures"); + this.approve(_signerSafes(safe2), signature); } /// @notice Tests the full flow: approve from both safes, then run function test_run() external { - (address[] memory safes1, bytes memory dataToSign1) = _getSignerData(safe1); - (uint8 v1, bytes32 r1, bytes32 s1) = vm.sign(nestedWallet1, keccak256(dataToSign1)); - - (address[] memory safes2, bytes memory dataToSign2) = _getSignerData(safe2); - (uint8 v2, bytes32 r2, bytes32 s2) = vm.sign(nestedWallet2, keccak256(dataToSign2)); - - approve(safes1, abi.encodePacked(r1, s1, v1)); - approve(safes2, abi.encodePacked(r2, s2, v2)); + _approveFrom(safe1, nestedWallet1); + _approveFrom(safe2, nestedWallet2); run(""); } /// @notice Tests that run() fails when not all nested safes have approved function test_run_notApproved() external { - (address[] memory safes1, bytes memory dataToSign) = _getSignerData(safe1); - (uint8 v1, bytes32 r1, bytes32 s1) = vm.sign(nestedWallet1, keccak256(dataToSign)); + _approveFrom(safe1, nestedWallet1); - approve(safes1, abi.encodePacked(r1, s1, v1)); - - bytes memory data = abi.encodeCall(this.run, ("")); - (bool success, bytes memory result) = address(this).call(data); - assertFalse(success); - assertEq(result, abi.encodeWithSignature("Error(string)", "not enough signatures")); + vm.expectRevert("not enough signatures"); + this.run(""); } } @@ -400,7 +376,7 @@ contract MultisigScriptDoubleNestedTest is MultisigScriptNestedBase { vm.etch(safe3, safeCode); vm.etch(safe4, safeCode); - nestedCounter = new Counter(address(safe4)); + nestedCounter = new Counter(safe4); _setupLeafSafes(); @@ -432,43 +408,26 @@ contract MultisigScriptDoubleNestedTest is MultisigScriptNestedBase { _assertSignOutput(safes, dataToSign); } - /// @notice Tests the approval flow through all nested levels - function test_runInit_double_nested() external { - (address[] memory safes1, bytes memory dataToSign1) = _getSignerData(safe1); - (uint8 v1, bytes32 r1, bytes32 s1) = vm.sign(nestedWallet1, keccak256(dataToSign1)); - - (address[] memory safes2, bytes memory dataToSign2) = _getSignerData(safe2); - (uint8 v2, bytes32 r2, bytes32 s2) = vm.sign(nestedWallet2, keccak256(dataToSign2)); - - approve(safes1, abi.encodePacked(r1, s1, v1)); - approve(safes2, abi.encodePacked(r2, s2, v2)); + /// @notice Tests the intermediate approval flow through nested safes + function test_approve_double_nested() external { + _approveFrom(safe1, nestedWallet1); + _approveFrom(safe2, nestedWallet2); approve(_toArray(safe3), ""); } /// @notice Tests that intermediate approve fails when not all leaf safes have approved function test_runInit_double_nested_notApproved() external { - (address[] memory safes1, bytes memory dataToSign) = _getSignerData(safe1); - (uint8 v1, bytes32 r1, bytes32 s1) = vm.sign(nestedWallet1, keccak256(dataToSign)); - - approve(safes1, abi.encodePacked(r1, s1, v1)); + _approveFrom(safe1, nestedWallet1); - bytes memory data = abi.encodeCall(this.approve, (_toArray(safe3), "")); - (bool success, bytes memory result) = address(this).call(data); - assertFalse(success); - assertEq(result, abi.encodeWithSignature("Error(string)", "not enough signatures")); + vm.expectRevert("not enough signatures"); + this.approve(_toArray(safe3), ""); } /// @notice Tests the full flow: approve from all nested safes, then run function test_run_double_nested() external { - (address[] memory safes1, bytes memory dataToSign1) = _getSignerData(safe1); - (uint8 v1, bytes32 r1, bytes32 s1) = vm.sign(nestedWallet1, keccak256(dataToSign1)); - - (address[] memory safes2, bytes memory dataToSign2) = _getSignerData(safe2); - (uint8 v2, bytes32 r2, bytes32 s2) = vm.sign(nestedWallet2, keccak256(dataToSign2)); - - approve(safes1, abi.encodePacked(r1, s1, v1)); - approve(safes2, abi.encodePacked(r2, s2, v2)); + _approveFrom(safe1, nestedWallet1); + _approveFrom(safe2, nestedWallet2); approve(_toArray(safe3), ""); run(""); diff --git a/test/universal/MultisigScriptDeposit.t.sol b/test/universal/MultisigScriptDeposit.t.sol index 1874925e..0329a21c 100644 --- a/test/universal/MultisigScriptDeposit.t.sol +++ b/test/universal/MultisigScriptDeposit.t.sol @@ -4,9 +4,10 @@ pragma solidity 0.8.25; import { ICBMulticall, Call3Value } from "interfaces/universal/ICBMulticall.sol"; import { Test } from "lib/forge-std/src/Test.sol"; import { Vm } from "lib/forge-std/src/Vm.sol"; +import { Bytes } from "src/libraries/Bytes.sol"; import { Preinstalls } from "src/libraries/Preinstalls.sol"; -import { MultisigScriptDeposit } from "scripts/universal/MultisigScriptDeposit.sol"; +import { IOptimismPortal2, MultisigScriptDeposit } from "scripts/universal/MultisigScriptDeposit.sol"; import { Simulation } from "scripts/universal/Simulation.sol"; import { IGnosisSafe } from "scripts/universal/IGnosisSafe.sol"; @@ -55,21 +56,15 @@ contract MultisigScriptDepositTest is Test, MultisigScriptDeposit { MockOptimismPortal internal portal; Counter internal l2Counter; - // Test configuration - address internal testL2Target; uint64 internal testGasLimit = 200_000; function() internal view returns (Call3Value[] memory) buildL2CallsInternal; function setUp() public { - // Deploy mock portal portal = new MockOptimismPortal(); - // Deploy a counter to use as L2 target (for calldata encoding) l2Counter = new Counter(address(this)); - testL2Target = address(l2Counter); - // Setup Safe vm.etch(safe, Preinstalls.getDeployedCode(Preinstalls.Safe_v130, block.chainid)); vm.etch(Preinstalls.MultiCall3, Preinstalls.getDeployedCode(Preinstalls.MultiCall3, block.chainid)); vm.deal(safe, 100 ether); @@ -114,24 +109,8 @@ contract MultisigScriptDepositTest is Test, MultisigScriptDeposit { Call[] memory calls = _buildCalls(); - // Should produce exactly one L1 call to the portal - assertEq(calls.length, 1, "Should have one L1 call"); - assertEq(calls[0].target, address(portal), "Target should be portal"); - assertEq(calls[0].value, 0, "Value should be 0"); - - // Decode the depositTransaction call - (address to, uint256 value, uint64 gasLimit, bool isCreation, bytes memory data) = - _decodeDepositTransaction(calls[0].data); - - assertEq(to, CB_MULTICALL, "L2 target should be CB_MULTICALL"); - assertEq(value, 0, "Bridged value should be 0"); - assertEq(gasLimit, testGasLimit, "Gas limit should match"); - assertFalse(isCreation, "Should not be creation"); - assertTrue(data.length > 0, "Data should not be empty"); - - // Verify the L2 data is an aggregate3Value call - bytes4 selector = bytes4(data); - assertEq(selector, ICBMulticall.aggregate3Value.selector, "Should be aggregate3Value call"); + bytes memory l2Data = _assertSinglePortalDeposit(calls, 0); + assertEq(bytes4(l2Data), ICBMulticall.aggregate3Value.selector, "Should be aggregate3Value call"); } /// @notice Test that multiple L2 calls are batched correctly @@ -140,28 +119,13 @@ contract MultisigScriptDepositTest is Test, MultisigScriptDeposit { Call[] memory calls = _buildCalls(); - // Should still produce exactly one L1 call - assertEq(calls.length, 1, "Should have one L1 call"); - - // Decode and verify - (address to, uint256 value, uint64 gasLimit, bool isCreation, bytes memory data) = - _decodeDepositTransaction(calls[0].data); - - assertEq(to, CB_MULTICALL, "L2 target should be CB_MULTICALL"); - assertEq(value, 0, "Bridged value should be 0"); - assertEq(gasLimit, testGasLimit, "Gas limit should match"); - assertFalse(isCreation, "Should not be creation"); - - // Decode the aggregate3Value call to verify multiple L2 calls are included - Call3Value[] memory l2Calls = abi.decode(_stripSelector(data), (Call3Value[])); + Call3Value[] memory l2Calls = _decodeAggregate3Value(_assertSinglePortalDeposit(calls, 0)); assertEq(l2Calls.length, 3, "Should have 3 L2 calls"); - // Verify call parameters are preserved through the wrapping - assertEq(l2Calls[0].target, testL2Target, "First call target should be preserved"); - assertEq(l2Calls[1].target, testL2Target, "Second call target should be preserved"); - assertEq(l2Calls[2].target, testL2Target, "Third call target should be preserved"); + assertEq(l2Calls[0].target, address(l2Counter), "First call target should be preserved"); + assertEq(l2Calls[1].target, address(l2Counter), "Second call target should be preserved"); + assertEq(l2Calls[2].target, address(l2Counter), "Third call target should be preserved"); - // Verify allowFailure flags are preserved (first two are false, third is true) assertFalse(l2Calls[0].allowFailure, "First call allowFailure should be false"); assertFalse(l2Calls[1].allowFailure, "Second call allowFailure should be false"); assertTrue(l2Calls[2].allowFailure, "Third call allowFailure should be true (preserved)"); @@ -173,12 +137,7 @@ contract MultisigScriptDepositTest is Test, MultisigScriptDeposit { Call[] memory calls = _buildCalls(); - // Value should be sum of all L2 call values (1 + 2 + 0.5 = 3.5 ether) - assertEq(calls[0].value, 3.5 ether, "L1 call value should be sum of L2 values"); - - // Decode and verify bridged value - (, uint256 value,,,) = _decodeDepositTransaction(calls[0].data); - assertEq(value, 3.5 ether, "Bridged value should be 3.5 ether"); + _assertSinglePortalDeposit(calls, 3.5 ether); } /// @notice Test that single L2 call still goes through multicall (consistent behavior) @@ -186,11 +145,9 @@ contract MultisigScriptDepositTest is Test, MultisigScriptDeposit { buildL2CallsInternal = _buildSingleL2CallNoValue; Call[] memory calls = _buildCalls(); - (,,,, bytes memory data) = _decodeDepositTransaction(calls[0].data); + bytes memory l2Data = _assertSinglePortalDeposit(calls, 0); - // Even single calls should be wrapped in aggregate3Value for consistency - bytes4 selector = bytes4(data); - assertEq(selector, ICBMulticall.aggregate3Value.selector, "Single call should still use aggregate3Value"); + assertEq(bytes4(l2Data), ICBMulticall.aggregate3Value.selector, "Single call should still use aggregate3Value"); } /// @notice Test the full sign flow with deposit transaction @@ -198,32 +155,16 @@ contract MultisigScriptDepositTest is Test, MultisigScriptDeposit { buildL2CallsInternal = _buildSingleL2CallNoValue; vm.recordLogs(); - bytes memory txData = abi.encodeWithSelector(this.sign.selector, new address[](0)); - vm.prank(wallet1.addr); - (bool success,) = address(this).call(txData); - assertTrue(success, "Sign should succeed"); + _signFrom(wallet1.addr); - // Verify DataToSign event was emitted - Vm.Log[] memory logs = vm.getRecordedLogs(); - bool foundDataToSign = false; - for (uint256 i; i < logs.length; i++) { - if (logs[i].topics[0] == keccak256("DataToSign(bytes)")) { - foundDataToSign = true; - break; - } - } - assertTrue(foundDataToSign, "DataToSign event should be emitted"); + assertEq(_lastLoggedBytes(), _expectedTxDataForCurrentBuildCalls()); } /// @notice Test sign flow with ETH value function test_sign_depositTransaction_withValue() external { buildL2CallsInternal = _buildL2CallsWithValue; - vm.recordLogs(); - bytes memory txData = abi.encodeWithSelector(this.sign.selector, new address[](0)); - vm.prank(wallet1.addr); - (bool success,) = address(this).call(txData); - assertTrue(success, "Sign with value should succeed"); + _signFrom(wallet1.addr); } /// @notice Test default _optimismPortal() returns correct address for mainnet @@ -260,70 +201,98 @@ contract MultisigScriptDepositTest is Test, MultisigScriptDeposit { function _buildSingleL2CallNoValue() internal view returns (Call3Value[] memory) { Call3Value[] memory calls = new Call3Value[](1); calls[0] = Call3Value({ - target: testL2Target, allowFailure: false, callData: abi.encodeCall(Counter.increment, ()), value: 0 + target: address(l2Counter), allowFailure: false, callData: abi.encodeCall(Counter.increment, ()), value: 0 }); return calls; } function _buildMultipleL2CallsNoValue() internal view returns (Call3Value[] memory) { + bytes memory incrementCall = abi.encodeCall(Counter.increment, ()); Call3Value[] memory calls = new Call3Value[](3); - calls[0] = Call3Value({ - target: testL2Target, allowFailure: false, callData: abi.encodeCall(Counter.increment, ()), value: 0 - }); - calls[1] = Call3Value({ - target: testL2Target, allowFailure: false, callData: abi.encodeCall(Counter.increment, ()), value: 0 - }); + calls[0] = Call3Value({ target: address(l2Counter), allowFailure: false, callData: incrementCall, value: 0 }); + calls[1] = Call3Value({ target: address(l2Counter), allowFailure: false, callData: incrementCall, value: 0 }); calls[2] = Call3Value({ - target: testL2Target, + target: address(l2Counter), allowFailure: true, // Test allowFailure flag preservation - callData: abi.encodeCall(Counter.increment, ()), + callData: incrementCall, value: 0 }); return calls; } function _buildL2CallsWithValue() internal view returns (Call3Value[] memory) { + bytes memory incrementPayableCall = abi.encodeCall(Counter.incrementPayable, ()); Call3Value[] memory calls = new Call3Value[](3); calls[0] = Call3Value({ - target: testL2Target, - allowFailure: false, - callData: abi.encodeCall(Counter.incrementPayable, ()), - value: 1 ether + target: address(l2Counter), allowFailure: false, callData: incrementPayableCall, value: 1 ether }); calls[1] = Call3Value({ - target: testL2Target, - allowFailure: false, - callData: abi.encodeCall(Counter.incrementPayable, ()), - value: 2 ether + target: address(l2Counter), allowFailure: false, callData: incrementPayableCall, value: 2 ether }); calls[2] = Call3Value({ - target: testL2Target, - allowFailure: false, - callData: abi.encodeCall(Counter.incrementPayable, ()), - value: 0.5 ether + target: address(l2Counter), allowFailure: false, callData: incrementPayableCall, value: 0.5 ether }); return calls; } + function _assertSinglePortalDeposit( + Call[] memory calls, + uint256 expectedValue + ) + internal + view + returns (bytes memory l2Data) + { + assertEq(calls.length, 1, "Should have one L1 call"); + assertEq(calls[0].target, address(portal), "Target should be portal"); + assertEq(calls[0].value, expectedValue, "L1 call value should match bridged value"); + + (address to, uint256 value, uint64 gasLimit, bool isCreation, bytes memory data) = + _decodeDepositTransaction(calls[0].data); + + assertEq(to, CB_MULTICALL, "L2 target should be CB_MULTICALL"); + assertEq(value, expectedValue, "Bridged value should match"); + assertEq(gasLimit, testGasLimit, "Gas limit should match"); + assertFalse(isCreation, "Should not be creation"); + + return data; + } + + function _expectedTxDataForCurrentBuildCalls() internal view returns (bytes memory) { + return _encodeTransactionData(_ownerSafe(), _buildAggregatedScriptCall({ scriptCalls: _buildCalls() })); + } + + function _lastLoggedBytes() internal returns (bytes memory) { + Vm.Log[] memory logs = vm.getRecordedLogs(); + return abi.decode(logs[logs.length - 1].data, (bytes)); + } + + function _signFrom(address signer) internal { + vm.prank(signer); + this.sign(new address[](0)); + } + /// @notice Decode depositTransaction calldata function _decodeDepositTransaction(bytes memory callData) internal pure returns (address to, uint256 value, uint64 gasLimit, bool isCreation, bytes memory data) { - // Skip the 4-byte selector bytes memory params = _stripSelector(callData); + require(bytes4(callData) == IOptimismPortal2.depositTransaction.selector, "Unexpected deposit selector"); (to, value, gasLimit, isCreation, data) = abi.decode(params, (address, uint256, uint64, bool, bytes)); } + function _decodeAggregate3Value(bytes memory data) internal pure returns (Call3Value[] memory) { + bytes memory params = _stripSelector(data); + require(bytes4(data) == ICBMulticall.aggregate3Value.selector, "Unexpected aggregate selector"); + return abi.decode(params, (Call3Value[])); + } + /// @notice Strip the 4-byte function selector from calldata function _stripSelector(bytes memory data) internal pure returns (bytes memory) { require(data.length >= 4, "Data too short"); - bytes memory result = new bytes(data.length - 4); - for (uint256 i = 4; i < data.length; i++) { - result[i - 4] = data[i]; - } - return result; + return Bytes.slice(data, 4); } } diff --git a/test/universal/OptimismMintableERC20.t.sol b/test/universal/OptimismMintableERC20.t.sol index b3be8191..4b0475b9 100644 --- a/test/universal/OptimismMintableERC20.t.sol +++ b/test/universal/OptimismMintableERC20.t.sol @@ -11,16 +11,19 @@ import { IERC165 } from "lib/openzeppelin-contracts/contracts/utils/introspectio abstract contract OptimismMintableERC20_TestInit is CommonTest { event Mint(address indexed account, uint256 amount); event Burn(address indexed account, uint256 amount); + + function _bridgeMint(address _to, uint256 _amount) internal { + vm.prank(address(l2StandardBridge)); + L2Token.mint(_to, _amount); + } } /// @title OptimismMintableERC20_Permit2_Test /// @notice Tests the `permit2` function of the `OptimismMintableERC20` contract. contract OptimismMintableERC20_Permit2_Test is OptimismMintableERC20_TestInit { function test_permit2_transferFrom_succeeds() external { - vm.prank(address(l2StandardBridge)); - L2Token.mint(alice, 100); + _bridgeMint(alice, 100); - assertEq(L2Token.balanceOf(bob), 0); vm.prank(L2Token.PERMIT2()); L2Token.transferFrom(alice, bob, 100); assertEq(L2Token.balanceOf(bob), 100); @@ -42,14 +45,12 @@ contract OptimismMintableERC20_Mint_Test is OptimismMintableERC20_TestInit { vm.expectEmit(true, true, true, true); emit Mint(alice, 100); - vm.prank(address(l2StandardBridge)); - L2Token.mint(alice, 100); + _bridgeMint(alice, 100); assertEq(L2Token.balanceOf(alice), 100); } function test_mint_notBridge_reverts() external { - // NOT the bridge vm.expectRevert("OptimismMintableERC20: only bridge can mint and burn"); vm.prank(address(alice)); L2Token.mint(alice, 100); @@ -60,8 +61,7 @@ contract OptimismMintableERC20_Mint_Test is OptimismMintableERC20_TestInit { /// @notice Tests the `burn` function of the `OptimismMintableERC20` contract. contract OptimismMintableERC20_Burn_Test is OptimismMintableERC20_TestInit { function test_burn_succeeds() external { - vm.prank(address(l2StandardBridge)); - L2Token.mint(alice, 100); + _bridgeMint(alice, 100); vm.expectEmit(true, true, true, true); emit Burn(alice, 100); @@ -73,7 +73,6 @@ contract OptimismMintableERC20_Burn_Test is OptimismMintableERC20_TestInit { } function test_burn_notBridge_reverts() external { - // NOT the bridge vm.expectRevert("OptimismMintableERC20: only bridge can mint and burn"); vm.prank(address(alice)); L2Token.burn(alice, 100); @@ -84,65 +83,19 @@ contract OptimismMintableERC20_Burn_Test is OptimismMintableERC20_TestInit { /// @notice Tests the `supportsInterface` function of the `OptimismMintableERC20` contract. contract OptimismMintableERC20_SupportsInterface_Test is OptimismMintableERC20_TestInit { function test_erc165_supportsInterface_succeeds() external view { - // The assertEq calls in this test are comparing the manual calculation of the iface, with - // what is returned by the solidity's type().interfaceId, just to be safe. - bytes4 iface1 = bytes4(keccak256("supportsInterface(bytes4)")); - assertEq(iface1, type(IERC165).interfaceId); - assert(L2Token.supportsInterface(iface1)); - - bytes4 iface2 = L2Token.l1Token.selector ^ L2Token.mint.selector ^ L2Token.burn.selector; - assertEq(iface2, type(ILegacyMintableERC20).interfaceId); - assert(L2Token.supportsInterface(iface2)); - - bytes4 iface3 = - L2Token.remoteToken.selector ^ L2Token.bridge.selector ^ L2Token.mint.selector ^ L2Token.burn.selector; - assertEq(iface3, type(IOptimismMintableERC20).interfaceId); - assert(L2Token.supportsInterface(iface3)); - } -} - -/// @title OptimismMintableERC20_L1Token_Test -/// @notice Tests the `l1Token` function of the `OptimismMintableERC20` contract. -contract OptimismMintableERC20_L1Token_Test is OptimismMintableERC20_TestInit { - function test_l1Token_succeeds() external view { - assertEq(L2Token.l1Token(), address(L1Token)); - } -} - -/// @title OptimismMintableERC20_L2Bridge_Test -/// @notice Tests the `l2Bridge` function of the `OptimismMintableERC20` contract. -contract OptimismMintableERC20_L2Bridge_Test is OptimismMintableERC20_TestInit { - function test_l2Bridge_succeeds() external view { - assertEq(L2Token.l2Bridge(), address(l2StandardBridge)); - } -} - -/// @title OptimismMintableERC20_RemoteToken_Test -/// @notice Tests the `remoteToken` function of the `OptimismMintableERC20` contract. -contract OptimismMintableERC20_RemoteToken_Test is OptimismMintableERC20_TestInit { - function test_remoteToken_succeeds() external view { - assertEq(L2Token.remoteToken(), address(L1Token)); - } -} - -/// @title OptimismMintableERC20_Bridge_Test -/// @notice Tests the `bridge` function of the `OptimismMintableERC20` contract. -contract OptimismMintableERC20_Bridge_Test is OptimismMintableERC20_TestInit { - function test_bridge_succeeds() external view { - assertEq(L2Token.bridge(), address(l2StandardBridge)); + assertTrue(L2Token.supportsInterface(type(IERC165).interfaceId)); + assertTrue(L2Token.supportsInterface(type(ILegacyMintableERC20).interfaceId)); + assertTrue(L2Token.supportsInterface(type(IOptimismMintableERC20).interfaceId)); } } -/// @title OptimismMintableERC20_Uncategorized_Test -/// @notice General tests that are not testing any function directly of the `OptimismMintableERC20` -/// contract. -contract OptimismMintableERC20_Uncategorized_Test is OptimismMintableERC20_TestInit { - function test_legacy_succeeds() external view { - // Getters for the remote token +/// @title OptimismMintableERC20_Getters_Test +/// @notice Tests getter and legacy getter functions of the `OptimismMintableERC20` contract. +contract OptimismMintableERC20_Getters_Test is OptimismMintableERC20_TestInit { + function test_getters_succeeds() external view { assertEq(L2Token.REMOTE_TOKEN(), address(L1Token)); assertEq(L2Token.remoteToken(), address(L1Token)); assertEq(L2Token.l1Token(), address(L1Token)); - // Getters for the bridge assertEq(L2Token.BRIDGE(), address(l2StandardBridge)); assertEq(L2Token.bridge(), address(l2StandardBridge)); assertEq(L2Token.l2Bridge(), address(l2StandardBridge)); diff --git a/test/universal/OptimismMintableERC20Factory.t.sol b/test/universal/OptimismMintableERC20Factory.t.sol index 0dfb5b96..62adc83f 100644 --- a/test/universal/OptimismMintableERC20Factory.t.sol +++ b/test/universal/OptimismMintableERC20Factory.t.sol @@ -20,6 +20,9 @@ abstract contract OptimismMintableERC20Factory_TestInit is CommonTest { event StandardL2TokenCreated(address indexed remoteToken, address indexed localToken); event OptimismMintableERC20Created(address indexed localToken, address indexed remoteToken, address deployer); + string internal constant REMOTE_TOKEN_ZERO_REVERT = + "OptimismMintableERC20Factory: must provide remote token address"; + /// @notice Precalculates the address of the token contract. function _calculateTokenAddress( address _remote, @@ -75,11 +78,8 @@ contract OptimismMintableERC20Factory_CreateStandardL2Token_Test is OptimismMint ) external { - // Assume vm.assume(_remoteToken != address(0)); - // Arrange - // Defaults to 18 decimals address local = _calculateTokenAddress(_remoteToken, _name, _symbol, 18); vm.expectEmit(address(l2OptimismMintableERC20Factory)); @@ -88,19 +88,17 @@ contract OptimismMintableERC20Factory_CreateStandardL2Token_Test is OptimismMint vm.expectEmit(address(l2OptimismMintableERC20Factory)); emit OptimismMintableERC20Created(local, _remoteToken, _caller); - // Act vm.prank(_caller); address addr = l2OptimismMintableERC20Factory.createStandardL2Token(_remoteToken, _name, _symbol); - // Assert - assertTrue(addr == local); - assertTrue(OptimismMintableERC20(local).decimals() == 18); + assertEq(addr, local); + assertEq(OptimismMintableERC20(local).decimals(), 18); assertEq(l2OptimismMintableERC20Factory.deployments(local), _remoteToken); } /// @notice Test that calling `createOptimismMintableERC20WithDecimals` with valid parameters /// succeeds. - function test_createStandardL2TokenWithDecimals_succeeds( + function test_createOptimismMintableERC20WithDecimals_succeeds( address _caller, address _remoteToken, string memory _name, @@ -109,10 +107,8 @@ contract OptimismMintableERC20Factory_CreateStandardL2Token_Test is OptimismMint ) external { - // Assume vm.assume(_remoteToken != address(0)); - // Arrange address local = _calculateTokenAddress(_remoteToken, _name, _symbol, _decimals); vm.expectEmit(address(l2OptimismMintableERC20Factory)); @@ -121,15 +117,13 @@ contract OptimismMintableERC20Factory_CreateStandardL2Token_Test is OptimismMint vm.expectEmit(address(l2OptimismMintableERC20Factory)); emit OptimismMintableERC20Created(local, _remoteToken, _caller); - // Act vm.prank(_caller); address addr = l2OptimismMintableERC20Factory.createOptimismMintableERC20WithDecimals( _remoteToken, _name, _symbol, _decimals ); - // Assert - assertTrue(addr == local); - assertTrue(OptimismMintableERC20(local).decimals() == _decimals); + assertEq(addr, local); + assertEq(OptimismMintableERC20(local).decimals(), _decimals); assertEq(l2OptimismMintableERC20Factory.deployments(local), _remoteToken); } @@ -142,23 +136,20 @@ contract OptimismMintableERC20Factory_CreateStandardL2Token_Test is OptimismMint ) external { - // Assume vm.assume(_remoteToken != address(0)); vm.prank(_caller); l2OptimismMintableERC20Factory.createStandardL2Token(_remoteToken, _name, _symbol); - // Arrange - vm.expectRevert(); // nosemgrep: sol-safety-expectrevert-no-args + vm.expectRevert(bytes("")); - // Act vm.prank(_caller); l2OptimismMintableERC20Factory.createStandardL2Token(_remoteToken, _name, _symbol); } - /// @notice Test that calling `createStandardL2TokenWithDecimals` with the same parameters + /// @notice Test that calling `createOptimismMintableERC20WithDecimals` with the same parameters /// twice reverts. - function test_createStandardL2TokenWithDecimals_sameTwice_reverts( + function test_createOptimismMintableERC20WithDecimals_sameTwice_reverts( address _caller, address _remoteToken, string memory _name, @@ -167,16 +158,13 @@ contract OptimismMintableERC20Factory_CreateStandardL2Token_Test is OptimismMint ) external { - // Assume vm.assume(_remoteToken != address(0)); vm.prank(_caller); l2OptimismMintableERC20Factory.createOptimismMintableERC20WithDecimals(_remoteToken, _name, _symbol, _decimals); - // Arrange - vm.expectRevert(); // nosemgrep: sol-safety-expectrevert-no-args + vm.expectRevert(bytes("")); - // Act vm.prank(_caller); l2OptimismMintableERC20Factory.createOptimismMintableERC20WithDecimals(_remoteToken, _name, _symbol, _decimals); } @@ -189,18 +177,15 @@ contract OptimismMintableERC20Factory_CreateStandardL2Token_Test is OptimismMint ) external { - // Arrange - address remote = address(0); - vm.expectRevert("OptimismMintableERC20Factory: must provide remote token address"); + vm.expectRevert(bytes(REMOTE_TOKEN_ZERO_REVERT)); - // Act vm.prank(_caller); - l2OptimismMintableERC20Factory.createStandardL2Token(remote, _name, _symbol); + l2OptimismMintableERC20Factory.createStandardL2Token(address(0), _name, _symbol); } - /// @notice Test that calling `createStandardL2TokenWithDecimals` with a zero remote token + /// @notice Test that calling `createOptimismMintableERC20WithDecimals` with a zero remote token /// address reverts. - function test_createStandardL2TokenWithDecimals_remoteIsZero_reverts( + function test_createOptimismMintableERC20WithDecimals_remoteIsZero_reverts( address _caller, string memory _name, string memory _symbol, @@ -208,13 +193,10 @@ contract OptimismMintableERC20Factory_CreateStandardL2Token_Test is OptimismMint ) external { - // Arrange - address remote = address(0); - vm.expectRevert("OptimismMintableERC20Factory: must provide remote token address"); + vm.expectRevert(bytes(REMOTE_TOKEN_ZERO_REVERT)); - // Act vm.prank(_caller); - l2OptimismMintableERC20Factory.createOptimismMintableERC20WithDecimals(remote, _name, _symbol, _decimals); + l2OptimismMintableERC20Factory.createOptimismMintableERC20WithDecimals(address(0), _name, _symbol, _decimals); } } @@ -236,7 +218,7 @@ contract OptimismMintableERC20Factory_Uncategorized_Test is OptimismMintableERC2 proxy.upgradeToAndCall(address(nextImpl), abi.encodeCall(NextImpl.initialize, (2))); assertEq(proxy.implementation(), address(nextImpl)); - // Verify that the NextImpl contract initialized its values according as expected + // Verify that the NextImpl contract initialized its values as expected. bytes32 slot21After = vm.load(address(l1OptimismMintableERC20Factory), bytes32(uint256(21))); bytes32 slot21Expected = NextImpl(address(l1OptimismMintableERC20Factory)).slot21Init(); assertEq(slot21Expected, slot21After); diff --git a/test/universal/Proxy.t.sol b/test/universal/Proxy.t.sol index 9af41bed..89114cee 100644 --- a/test/universal/Proxy.t.sol +++ b/test/universal/Proxy.t.sol @@ -2,9 +2,9 @@ pragma solidity 0.8.15; import { Test } from "lib/forge-std/src/Test.sol"; -import { Bytes32AddressLib } from "lib/solmate/src/utils/Bytes32AddressLib.sol"; import { IProxy } from "interfaces/universal/IProxy.sol"; -import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; +import { Proxy } from "src/universal/Proxy.sol"; +import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; contract Proxy_SimpleStorage_Harness { mapping(uint256 => uint256) internal store; @@ -30,27 +30,35 @@ abstract contract Proxy_TestInit is Test { event Upgraded(address indexed implementation); event AdminChanged(address previousAdmin, address newAdmin); - address alice = address(64); - - bytes32 internal constant IMPLEMENTATION_KEY = bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1); - - bytes32 internal constant OWNER_KEY = bytes32(uint256(keccak256("eip1967.proxy.admin")) - 1); + address internal constant ADMIN = address(64); IProxy proxy; Proxy_SimpleStorage_Harness simpleStorage; function setUp() external { - // Deploy a proxy and simple storage contract as the implementation - proxy = IProxy( - DeployUtils.create1({ - _name: "src/universal/Proxy.sol:Proxy", - _args: DeployUtils.encodeConstructor(abi.encodeCall(IProxy.__constructor__, (alice))) - }) - ); + proxy = IProxy(payable(address(new Proxy(ADMIN)))); simpleStorage = new Proxy_SimpleStorage_Harness(); - vm.prank(alice); - proxy.upgradeTo(address(simpleStorage)); + _upgradeTo(address(simpleStorage)); + } + + function _proxyStorage() internal view returns (Proxy_SimpleStorage_Harness) { + return Proxy_SimpleStorage_Harness(address(proxy)); + } + + function _upgradeTo(address _newImplementation) internal { + vm.prank(ADMIN); + proxy.upgradeTo(_newImplementation); + } + + function _getImplementation() internal returns (address) { + vm.prank(ADMIN); + return proxy.implementation(); + } + + function _adminAs(address _caller) internal returns (address) { + vm.prank(_caller); + return proxy.admin(); } } @@ -58,52 +66,31 @@ abstract contract Proxy_TestInit is Test { /// @notice Tests the `upgradeTo` function of the `Proxy` contract. contract Proxy_UpgradeTo_Test is Proxy_TestInit { function test_upgradeTo_notAdmin_succeeds() external { - // The implementation does not have a `upgradeTo` method, calling `upgradeTo` not as the - // owner should revert. - vm.expectRevert(); // nosemgrep: sol-safety-expectrevert-no-args - proxy.upgradeTo(address(64)); + address newImplementation = address(128); + + vm.expectRevert(bytes("")); + proxy.upgradeTo(newImplementation); - // Call `upgradeTo` as the owner, it should succeed and emit the `Upgraded` event. vm.expectEmit(true, true, true, true); - emit Upgraded(address(64)); - vm.prank(alice); - proxy.upgradeTo(address(64)); + emit Upgraded(newImplementation); + _upgradeTo(newImplementation); - // Get the implementation as the owner - vm.prank(alice); - address impl = proxy.implementation(); - assertEq(impl, address(64)); + assertEq(_getImplementation(), newImplementation); } function test_upgradeTo_clashingFunctionSignatures_succeeds() external { - // Clasher has a clashing function with the proxy. Proxy_Clasher_Harness clasher = new Proxy_Clasher_Harness(); - // Set the clasher as the implementation. - vm.prank(alice); - proxy.upgradeTo(address(clasher)); - - { - // Assert that the implementation was set properly. - vm.prank(alice); - address impl = proxy.implementation(); - assertEq(impl, address(clasher)); - } + _upgradeTo(address(clasher)); + assertEq(_getImplementation(), address(clasher)); // Call the clashing function on the proxy not as the owner so that the call passes // through. The implementation will revert so we can be sure that the call passed through. vm.expectRevert(bytes("Clasher: upgradeTo")); proxy.upgradeTo(address(0)); - { - // Now call the clashing function as the owner and be sure that it doesn't pass through - // to the implementation. - vm.prank(alice); - proxy.upgradeTo(address(0)); - vm.prank(alice); - address impl = proxy.implementation(); - assertEq(impl, address(0)); - } + _upgradeTo(address(0)); + assertEq(_getImplementation(), address(0)); } } @@ -111,67 +98,47 @@ contract Proxy_UpgradeTo_Test is Proxy_TestInit { /// @notice Tests the `upgradeToAndCall` function of the `Proxy` contract. contract Proxy_UpgradeToAndCall_Test is Proxy_TestInit { function test_upgradeToAndCall_succeeds() external { - { - // There should be nothing in the current proxy storage - uint256 expect = Proxy_SimpleStorage_Harness(address(proxy)).get(1); - assertEq(expect, 0); - } + assertEq(_proxyStorage().get(1), 0); - // Deploy a new SimpleStorage - simpleStorage = new Proxy_SimpleStorage_Harness(); + Proxy_SimpleStorage_Harness newSimpleStorage = new Proxy_SimpleStorage_Harness(); - // Set the new SimpleStorage as the implementation and call. vm.expectEmit(true, true, true, true); - emit Upgraded(address(simpleStorage)); - vm.prank(alice); - proxy.upgradeToAndCall(address(simpleStorage), abi.encodeCall(Proxy_SimpleStorage_Harness.set, (1, 1))); + emit Upgraded(address(newSimpleStorage)); + vm.prank(ADMIN); + proxy.upgradeToAndCall(address(newSimpleStorage), abi.encodeCall(Proxy_SimpleStorage_Harness.set, (1, 1))); - // The call should have impacted the state - uint256 result = Proxy_SimpleStorage_Harness(address(proxy)).get(1); - assertEq(result, 1); + assertEq(_proxyStorage().get(1), 1); } function test_upgradeToAndCall_functionDoesNotExist_reverts() external { - // Get the current implementation address - vm.prank(alice); - address impl = proxy.implementation(); - assertEq(impl, address(simpleStorage)); + address initialImplementation = _getImplementation(); + assertEq(initialImplementation, address(simpleStorage)); - // Deploy a new SimpleStorage - simpleStorage = new Proxy_SimpleStorage_Harness(); + Proxy_SimpleStorage_Harness newSimpleStorage = new Proxy_SimpleStorage_Harness(); // Set the new SimpleStorage as the implementation and call. This reverts because the // calldata doesn't match a function on the implementation. vm.expectRevert("Proxy: delegatecall to new implementation contract failed"); - vm.prank(alice); - proxy.upgradeToAndCall(address(simpleStorage), hex""); - - // The implementation address should have not updated because the call to - // `upgradeToAndCall` reverted. - vm.prank(alice); - address postImpl = proxy.implementation(); - assertEq(impl, postImpl); - - // The attempt to `upgradeToAndCall` should revert when it is not called by the owner. - vm.expectRevert(); // nosemgrep: sol-safety-expectrevert-no-args - proxy.upgradeToAndCall(address(simpleStorage), abi.encodeCall(simpleStorage.set, (1, 1))); + vm.prank(ADMIN); + proxy.upgradeToAndCall(address(newSimpleStorage), hex""); + + assertEq(_getImplementation(), initialImplementation); + + vm.expectRevert(bytes("")); + proxy.upgradeToAndCall(address(newSimpleStorage), abi.encodeCall(Proxy_SimpleStorage_Harness.set, (1, 1))); } function test_upgradeToAndCall_isPayable_succeeds() external { - // Give alice some funds - vm.deal(alice, 1 ether); + uint256 value = 1 ether; - // Set the implementation and call and send value. - vm.prank(alice); - proxy.upgradeToAndCall{ value: 1 ether }(address(simpleStorage), abi.encodeCall(simpleStorage.set, (1, 1))); - - // The implementation address should be correct - vm.prank(alice); - address impl = proxy.implementation(); - assertEq(impl, address(simpleStorage)); + vm.deal(ADMIN, value); + vm.prank(ADMIN); + proxy.upgradeToAndCall{ value: value }( + address(simpleStorage), abi.encodeCall(Proxy_SimpleStorage_Harness.set, (1, 1)) + ); - // The proxy should have a balance - assertEq(address(proxy).balance, 1 ether); + assertEq(_getImplementation(), address(simpleStorage)); + assertEq(address(proxy).balance, value); } } @@ -179,16 +146,13 @@ contract Proxy_UpgradeToAndCall_Test is Proxy_TestInit { /// @notice Tests the `changeAdmin` function of the `Proxy` contract. contract Proxy_ChangeAdmin_Test is Proxy_TestInit { function test_changeAdmin_ownerKey_succeeds() external { - // The hardcoded owner key should be correct - vm.prank(alice); - proxy.changeAdmin(address(6)); + address newAdmin = address(6); - bytes32 key = vm.load(address(proxy), OWNER_KEY); - assertEq(address(6), Bytes32AddressLib.fromLast20Bytes(key)); + vm.prank(ADMIN); + proxy.changeAdmin(newAdmin); - vm.prank(address(6)); - address owner = proxy.admin(); - assertEq(owner, address(6)); + assertEq(EIP1967Helper.getAdmin(address(proxy)), newAdmin); + assertEq(_adminAs(newAdmin), newAdmin); } } @@ -196,26 +160,20 @@ contract Proxy_ChangeAdmin_Test is Proxy_TestInit { /// @notice Tests the `admin` function of the `Proxy` contract. contract Proxy_Admin_Test is Proxy_TestInit { function test_admin_notAdmin_succeeds() external { - // Calling `changeAdmin` not as the owner should revert as the implementation does not have - // a `changeAdmin` method. - vm.expectRevert(); // nosemgrep: sol-safety-expectrevert-no-args - proxy.changeAdmin(address(1)); + address newAdmin = address(1); + + vm.expectRevert(bytes("")); + proxy.changeAdmin(newAdmin); - // Call `changeAdmin` as the owner, it should succeed and emit the `AdminChanged` event. vm.expectEmit(true, true, true, true); - emit AdminChanged(alice, address(1)); - vm.prank(alice); - proxy.changeAdmin(address(1)); + emit AdminChanged(ADMIN, newAdmin); + vm.prank(ADMIN); + proxy.changeAdmin(newAdmin); - // Calling `admin` not as the owner should revert as the implementation does not have a - // `admin` method. - vm.expectRevert(); // nosemgrep: sol-safety-expectrevert-no-args + vm.expectRevert(bytes("")); proxy.admin(); - // Calling `admin` as the owner should work. - vm.prank(address(1)); - address owner = proxy.admin(); - assertEq(owner, address(1)); + assertEq(_adminAs(newAdmin), newAdmin); } } @@ -223,16 +181,12 @@ contract Proxy_Admin_Test is Proxy_TestInit { /// @notice Tests the `implementation` function of the `Proxy` contract. contract Proxy_Implementation_Test is Proxy_TestInit { function test_implementation_key_succeeds() external { - // The hardcoded implementation key should be correct - vm.prank(alice); - proxy.upgradeTo(address(6)); + address newImplementation = address(6); - bytes32 key = vm.load(address(proxy), IMPLEMENTATION_KEY); - assertEq(address(6), Bytes32AddressLib.fromLast20Bytes(key)); + _upgradeTo(newImplementation); - vm.prank(alice); - address impl = proxy.implementation(); - assertEq(impl, address(6)); + assertEq(EIP1967Helper.getImplementation(address(proxy)), newImplementation); + assertEq(_getImplementation(), newImplementation); } // Allow for `eth_call` to call proxy methods by setting "from" to `address(0)`. @@ -243,42 +197,24 @@ contract Proxy_Implementation_Test is Proxy_TestInit { } function test_implementation_isZeroAddress_reverts() external { - // Set `address(0)` as the implementation. - vm.prank(alice); - proxy.upgradeTo(address(0)); - - (bool success, bytes memory returndata) = address(proxy).call(hex""); - assertEq(success, false); + _upgradeTo(address(0)); - bytes memory err = abi.encodeWithSignature("Error(string)", "Proxy: implementation not initialized"); // nosemgrep: - // sol-style-use-abi-encodecall - - assertEq(returndata, err); + vm.expectRevert("Proxy: implementation not initialized"); + _proxyStorage().get(1); } } -/// @title Proxy_Uncategorized_Test -/// @notice General tests that are not testing any function directly of the `Proxy` contract. -contract Proxy_Uncategorized_Test is Proxy_TestInit { +/// @title Proxy_Delegation_Test +/// @notice Tests proxy delegation behavior. +contract Proxy_Delegation_Test is Proxy_TestInit { function test_delegatesToImpl_succeeds() external { - // Call the storage setter on the proxy - Proxy_SimpleStorage_Harness(address(proxy)).set(1, 1); - - // The key should not be set in the implementation - uint256 result = simpleStorage.get(1); - assertEq(result, 0); - { - // The key should be set in the proxy - uint256 expect = Proxy_SimpleStorage_Harness(address(proxy)).get(1); - assertEq(expect, 1); - } - - { - // The owner should be able to call through the proxy when there is not a function - // selector crash. - vm.prank(alice); - uint256 expect = Proxy_SimpleStorage_Harness(address(proxy)).get(1); - assertEq(expect, 1); - } + _proxyStorage().set(1, 1); + + assertEq(simpleStorage.get(1), 0); + assertEq(_proxyStorage().get(1), 1); + + // The owner should be able to call through the proxy when there is no selector clash. + vm.prank(ADMIN); + assertEq(_proxyStorage().get(1), 1); } } diff --git a/test/universal/ProxyAdmin.t.sol b/test/universal/ProxyAdmin.t.sol index 88fdc4ec..17aa6469 100644 --- a/test/universal/ProxyAdmin.t.sol +++ b/test/universal/ProxyAdmin.t.sol @@ -17,7 +17,8 @@ import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; /// @title ProxyAdmin_TestInit /// @notice Reusable test initialization for `ProxyAdmin` tests. abstract contract ProxyAdmin_TestInit is Test { - address alice = address(64); + address internal constant PROXY_ADMIN_OWNER = address(64); + address internal constant NEW_PROXY_ADMIN = address(128); IProxy proxy; IL1ChugSplashProxy chugsplash; @@ -30,15 +31,13 @@ abstract contract ProxyAdmin_TestInit is Test { Proxy_SimpleStorage_Harness implementation; function setUp() external { - // Deploy the proxy admin admin = IProxyAdmin( DeployUtils.create1({ _name: "ProxyAdmin", - _args: DeployUtils.encodeConstructor(abi.encodeCall(IProxyAdmin.__constructor__, (alice))) + _args: DeployUtils.encodeConstructor(abi.encodeCall(IProxyAdmin.__constructor__, (PROXY_ADMIN_OWNER))) }) ); - // Deploy the standard proxy proxy = IProxy( DeployUtils.create1({ _name: "src/universal/Proxy.sol:Proxy", @@ -46,7 +45,6 @@ abstract contract ProxyAdmin_TestInit is Test { }) ); - // Deploy the legacy L1ChugSplashProxy with the admin as the owner chugsplash = IL1ChugSplashProxy( DeployUtils.create1({ _name: "L1ChugSplashProxy", @@ -56,15 +54,14 @@ abstract contract ProxyAdmin_TestInit is Test { }) ); - // Deploy the legacy AddressManager addressManager = IAddressManager( DeployUtils.create1({ _name: "AddressManager", _args: DeployUtils.encodeConstructor(abi.encodeCall(IAddressManager.__constructor__, ())) }) ); - // The proxy admin must be the new owner of the address manager addressManager.transferOwnership(address(admin)); + // Deploy a legacy ResolvedDelegateProxy with the name `a`. Whatever `a` is set to in // AddressManager will be the address that is used for the implementation. resolved = IResolvedDelegateProxy( @@ -75,15 +72,13 @@ abstract contract ProxyAdmin_TestInit is Test { ) }) ); - // Impersonate alice for setting up the admin. - vm.startPrank(alice); - // Set the address of the address manager in the admin so that it can resolve the + + vm.startPrank(PROXY_ADMIN_OWNER); + // Set the address manager so the admin can resolve the // implementation address of legacy ResolvedDelegateProxy based proxies. - admin.setAddressManager(IAddressManager(address(addressManager))); - // Set the reverse lookup of the ResolvedDelegateProxy proxy + admin.setAddressManager(addressManager); admin.setImplementationName(address(resolved), "a"); - // Set the proxy types admin.setProxyType(address(proxy), IProxyAdmin.ProxyType.ERC1967); admin.setProxyType(address(chugsplash), IProxyAdmin.ProxyType.CHUGSPLASH); admin.setProxyType(address(resolved), IProxyAdmin.ProxyType.RESOLVED); @@ -106,7 +101,7 @@ contract ProxyAdmin_SetProxyType_Test is ProxyAdmin_TestInit { /// @notice Tests the `setImplementationName` function of the `ProxyAdmin` contract. contract ProxyAdmin_SetImplementationName_Test is ProxyAdmin_TestInit { function test_setImplementationName_succeeds() external { - vm.prank(alice); + vm.prank(PROXY_ADMIN_OWNER); admin.setImplementationName(address(1), "foo"); assertEq(admin.implementationName(address(1)), "foo"); } @@ -122,7 +117,7 @@ contract ProxyAdmin_SetImplementationName_Test is ProxyAdmin_TestInit { contract ProxyAdmin_SetAddressManager_Test is ProxyAdmin_TestInit { function test_setAddressManager_notOwner_reverts() external { vm.expectRevert("Ownable: caller is not the owner"); - admin.setAddressManager(IAddressManager((address(0)))); + admin.setAddressManager(IAddressManager(address(0))); } } @@ -130,74 +125,67 @@ contract ProxyAdmin_SetAddressManager_Test is ProxyAdmin_TestInit { /// @notice Tests the `isUpgrading` function of the `ProxyAdmin` contract. contract ProxyAdmin_IsUpgrading_Test is ProxyAdmin_TestInit { function test_isUpgrading_succeeds() external { - assertEq(false, admin.isUpgrading()); + assertFalse(admin.isUpgrading()); - vm.prank(alice); + vm.prank(PROXY_ADMIN_OWNER); admin.setUpgrading(true); - assertEq(true, admin.isUpgrading()); + assertTrue(admin.isUpgrading()); } } /// @title ProxyAdmin_GetProxyImplementation_Test /// @notice Tests the `getProxyImplementation` function of the `ProxyAdmin` contract. contract ProxyAdmin_GetProxyImplementation_Test is ProxyAdmin_TestInit { - function getProxyImplementation(address payable _proxy) internal { - { - address impl = admin.getProxyImplementation(_proxy); - assertEq(impl, address(0)); - } + function _assertProxyImplementation(address payable _proxy) internal { + assertEq(admin.getProxyImplementation(_proxy), address(0)); - vm.prank(alice); + vm.prank(PROXY_ADMIN_OWNER); admin.upgrade(_proxy, address(implementation)); - { - address impl = admin.getProxyImplementation(_proxy); - assertEq(impl, address(implementation)); - } + assertEq(admin.getProxyImplementation(_proxy), address(implementation)); } function test_getProxyImplementation_erc1967_succeeds() external { - getProxyImplementation(payable(proxy)); + _assertProxyImplementation(payable(proxy)); } function test_getProxyImplementation_chugsplash_succeeds() external { - getProxyImplementation(payable(chugsplash)); + _assertProxyImplementation(payable(chugsplash)); } function test_getProxyImplementation_resolved_succeeds() external { - getProxyImplementation(payable(resolved)); + _assertProxyImplementation(payable(resolved)); } } /// @title ProxyAdmin_GetProxyAdmin_Test /// @notice Tests the `getProxyAdmin` function of the `ProxyAdmin` contract. contract ProxyAdmin_GetProxyAdmin_Test is ProxyAdmin_TestInit { - function getProxyAdmin(address payable _proxy) internal view { - address owner = admin.getProxyAdmin(_proxy); - assertEq(owner, address(admin)); + function _assertProxyAdmin(address payable _proxy) internal view { + assertEq(admin.getProxyAdmin(_proxy), address(admin)); } function test_getProxyAdmin_erc1967_succeeds() external view { - getProxyAdmin(payable(proxy)); + _assertProxyAdmin(payable(proxy)); } function test_getProxyAdmin_chugsplash_succeeds() external view { - getProxyAdmin(payable(chugsplash)); + _assertProxyAdmin(payable(chugsplash)); } function test_getProxyAdmin_resolved_succeeds() external view { - getProxyAdmin(payable(resolved)); + _assertProxyAdmin(payable(resolved)); } } /// @title ProxyAdmin_ChangeProxyAdmin_Test /// @notice Tests the `changeProxyAdmin` function of the `ProxyAdmin` contract. contract ProxyAdmin_ChangeProxyAdmin_Test is ProxyAdmin_TestInit { - function changeProxyAdmin(address payable _proxy) internal { + function _assertChangeProxyAdmin(address payable _proxy) internal { IProxyAdmin.ProxyType proxyType = admin.proxyType(address(_proxy)); - vm.prank(alice); - admin.changeProxyAdmin(_proxy, address(128)); + vm.prank(PROXY_ADMIN_OWNER); + admin.changeProxyAdmin(_proxy, NEW_PROXY_ADMIN); // The proxy is no longer the admin and can // no longer call the proxy interface except for @@ -210,86 +198,79 @@ contract ProxyAdmin_ChangeProxyAdmin_Test is ProxyAdmin_TestInit { vm.expectRevert("L1ChugSplashProxy: implementation is not set yet"); admin.getProxyAdmin(_proxy); } else if (proxyType == IProxyAdmin.ProxyType.RESOLVED) { - // Just an empty block to show that all cases are covered - } else { - vm.expectRevert("ProxyAdmin: unknown proxy type"); + assertEq(admin.getProxyAdmin(_proxy), NEW_PROXY_ADMIN); } // Call the proxy contract directly to get the admin. // Different proxy types have different interfaces. - vm.prank(address(128)); + vm.prank(NEW_PROXY_ADMIN); if (proxyType == IProxyAdmin.ProxyType.ERC1967) { - assertEq(IProxy(payable(_proxy)).admin(), address(128)); + assertEq(IProxy(payable(_proxy)).admin(), NEW_PROXY_ADMIN); } else if (proxyType == IProxyAdmin.ProxyType.CHUGSPLASH) { - assertEq(IL1ChugSplashProxy(payable(_proxy)).getOwner(), address(128)); + assertEq(IL1ChugSplashProxy(payable(_proxy)).getOwner(), NEW_PROXY_ADMIN); } else if (proxyType == IProxyAdmin.ProxyType.RESOLVED) { - assertEq(addressManager.owner(), address(128)); - } else { - assert(false); + assertEq(addressManager.owner(), NEW_PROXY_ADMIN); } } function test_changeProxyAdmin_erc1967_succeeds() external { - changeProxyAdmin(payable(proxy)); + _assertChangeProxyAdmin(payable(proxy)); } function test_changeProxyAdmin_chugsplash_succeeds() external { - changeProxyAdmin(payable(chugsplash)); + _assertChangeProxyAdmin(payable(chugsplash)); } function test_changeProxyAdmin_resolved_succeeds() external { - changeProxyAdmin(payable(resolved)); + _assertChangeProxyAdmin(payable(resolved)); } } /// @title ProxyAdmin_Upgrade_Test /// @notice Tests the `upgrade` function of the `ProxyAdmin` contract. contract ProxyAdmin_Upgrade_Test is ProxyAdmin_TestInit { - function upgrade(address payable _proxy) internal { - vm.prank(alice); + function _assertUpgrade(address payable _proxy) internal { + vm.prank(PROXY_ADMIN_OWNER); admin.upgrade(_proxy, address(implementation)); - address impl = admin.getProxyImplementation(_proxy); - assertEq(impl, address(implementation)); + assertEq(admin.getProxyImplementation(_proxy), address(implementation)); } function test_upgrade_erc1967_succeeds() external { - upgrade(payable(proxy)); + _assertUpgrade(payable(proxy)); } function test_upgrade_chugsplash_succeeds() external { - upgrade(payable(chugsplash)); + _assertUpgrade(payable(chugsplash)); } function test_upgrade_resolved_succeeds() external { - upgrade(payable(resolved)); + _assertUpgrade(payable(resolved)); } } /// @title ProxyAdmin_UpgradeAndCall_Test /// @notice Tests the `upgradeAndCall` function of the `ProxyAdmin` contract. contract ProxyAdmin_UpgradeAndCall_Test is ProxyAdmin_TestInit { - function upgradeAndCall(address payable _proxy) internal { - vm.prank(alice); + function _assertUpgradeAndCall(address payable _proxy) internal { + vm.prank(PROXY_ADMIN_OWNER); admin.upgradeAndCall(_proxy, address(implementation), abi.encodeCall(Proxy_SimpleStorage_Harness.set, (1, 1))); - address impl = admin.getProxyImplementation(_proxy); - assertEq(impl, address(implementation)); + assertEq(admin.getProxyImplementation(_proxy), address(implementation)); - uint256 got = Proxy_SimpleStorage_Harness(address(_proxy)).get(1); - assertEq(got, 1); + assertEq(Proxy_SimpleStorage_Harness(address(_proxy)).get(1), 1); } - function test_erc1967UpgradeAndCall_succeeds() external { - upgradeAndCall(payable(proxy)); + function test_upgradeAndCall_erc1967_succeeds() external { + _assertUpgradeAndCall(payable(proxy)); } - function test_chugsplashUpgradeAndCall_succeeds() external { - upgradeAndCall(payable(chugsplash)); + function test_upgradeAndCall_chugsplash_succeeds() external { + _assertUpgradeAndCall(payable(chugsplash)); } - function test_delegateResolvedUpgradeAndCall_succeeds() external { - upgradeAndCall(payable(resolved)); + function test_upgradeAndCall_resolved_succeeds() external { + _assertUpgradeAndCall(payable(resolved)); } } @@ -298,7 +279,7 @@ contract ProxyAdmin_UpgradeAndCall_Test is ProxyAdmin_TestInit { /// functions of the `ProxyAdmin` contract. contract ProxyAdmin_Uncategorized_Test is ProxyAdmin_TestInit { function test_owner_succeeds() external view { - assertEq(admin.owner(), alice); + assertEq(admin.owner(), PROXY_ADMIN_OWNER); } function test_proxyType_succeeds() external view { diff --git a/test/universal/ProxyAdminOwnedBase.t.sol b/test/universal/ProxyAdminOwnedBase.t.sol index 0872fe04..b8d6a466 100644 --- a/test/universal/ProxyAdminOwnedBase.t.sol +++ b/test/universal/ProxyAdminOwnedBase.t.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.15; // Testing import { CommonTest } from "test/setup/CommonTest.sol"; -import { Constants } from "src/libraries/Constants.sol"; +import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; // Contracts import { ProxyAdminOwnedBase } from "src/universal/ProxyAdminOwnedBase.sol"; @@ -12,10 +12,10 @@ import { ProxyAdminOwnedBase } from "src/universal/ProxyAdminOwnedBase.sol"; /// @notice Contract implementing the abstract `ProxyAdminOwnedBase` contract so we can write unit /// tests for the `ProxyAdminOwnedBase` contract. contract ProxyAdminOwnedBase_Harness is ProxyAdminOwnedBase { - /// @notice Slot 0, used to test ResolvedDelegateProxy behavior. + /// @notice Slot 0, matching the legacy ResolvedDelegateProxy storage layout. mapping(address => string) public slot0; - /// @notice Slot 1, used to test ResolvedDelegateProxy behavior. + /// @notice Slot 1, matching the legacy ResolvedDelegateProxy storage layout. mapping(address => address) public slot1; /// @notice Assert that the proxy admin owner of the current contract is the same as the proxy @@ -38,19 +38,15 @@ contract ProxyAdminOwnedBase_Harness is ProxyAdminOwnedBase { function assertOnlyProxyAdminOrProxyAdminOwner() public view { _assertOnlyProxyAdminOrProxyAdminOwner(); } - - /// @notice Set the value of slot 0 for the provided address. - function setSlot0(address _address, string memory _value) public { - slot0[_address] = _value; - } - - /// @notice Set the value of slot 1 for the provided address. - function setSlot1(address _address, address _value) public { - slot1[_address] = _value; - } } abstract contract ProxyAdminOwnedBase_TestInit is CommonTest { + /// @notice Stored name value that identifies the legacy ResolvedDelegateProxy. + bytes32 internal constant RESOLVED_DELEGATE_PROXY_NAME = bytes32("OVM_L1CrossDomainMessenger"); + + /// @notice Length of the stored ResolvedDelegateProxy name. + uint256 internal constant RESOLVED_DELEGATE_PROXY_NAME_LENGTH = 26; + /// @notice Harness for the `ProxyAdminOwnedBase` contract. ProxyAdminOwnedBase_Harness public harness; @@ -58,14 +54,39 @@ abstract contract ProxyAdminOwnedBase_TestInit is CommonTest { function setUp() public override { super.setUp(); - // Create a new harness harness = new ProxyAdminOwnedBase_Harness(); + EIP1967Helper.setAdmin(address(harness), address(proxyAdmin)); + } - // Set the owner of the harness to the ProxyAdmin contract. + /// @notice Clears the EIP-1967 admin slot and sets the legacy ResolvedDelegateProxy slots. + function setResolvedDelegateProxy(address _addressManager) internal { + EIP1967Helper.setAdmin(address(harness), address(0)); + vm.store(address(harness), resolvedDelegateProxyNameSlot(), resolvedDelegateProxyNameSlotValue()); vm.store( - address(harness), bytes32(Constants.PROXY_OWNER_ADDRESS), bytes32(uint256(uint160(address(proxyAdmin)))) + address(harness), resolvedDelegateProxyAddressManagerSlot(), bytes32(uint256(uint160(_addressManager))) ); } + + /// @notice Stores a raw value in the ResolvedDelegateProxy name slot. + function setResolvedDelegateProxyNameSlot(bytes32 _value) internal { + EIP1967Helper.setAdmin(address(harness), address(0)); + vm.store(address(harness), resolvedDelegateProxyNameSlot(), _value); + } + + /// @notice Returns the storage slot for `slot0[address(harness)]`. + function resolvedDelegateProxyNameSlot() internal view returns (bytes32) { + return keccak256(abi.encode(address(harness), uint256(0))); + } + + /// @notice Returns the storage value Solidity uses for the short string name. + function resolvedDelegateProxyNameSlotValue() internal pure returns (bytes32) { + return bytes32(uint256(RESOLVED_DELEGATE_PROXY_NAME) | uint256(RESOLVED_DELEGATE_PROXY_NAME_LENGTH * 2)); + } + + /// @notice Returns the storage slot for `slot1[address(harness)]`. + function resolvedDelegateProxyAddressManagerSlot() internal view returns (bytes32) { + return keccak256(abi.encode(address(harness), uint256(1))); + } } contract ProxyAdminOwnedBase_proxyAdminOwner_Test is ProxyAdminOwnedBase_TestInit { @@ -84,49 +105,25 @@ contract ProxyAdminOwnedBase_proxyAdmin_Test is ProxyAdminOwnedBase_TestInit { /// @notice Tests that the proxyAdmin function returns the correct proxy when the current /// contract is a full ResolvedDelegateProxy. function test_proxyAdmin_fullResolvedDelegateProxy_succeeds() public { - // Unset the standard proxy owner slot. - vm.store(address(harness), bytes32(Constants.PROXY_OWNER_ADDRESS), bytes32(0)); - - // Store the string "OVM_L1CrossDomainMessenger" in slot 0. - harness.setSlot0(address(harness), "OVM_L1CrossDomainMessenger"); - - // Store the address of the proxyAdmin in slot 1. - harness.setSlot1(address(harness), address(addressManager)); - - // Expect no revert. + setResolvedDelegateProxy(address(addressManager)); assertEq(address(harness.proxyAdmin()), address(proxyAdmin)); } /// @notice Tests that the proxyAdmin function reverts if the current contract is not a /// ResolvedDelegateProxy. - /// @param _slot0Value The value to store in slot 0. - function test_proxyAdmin_notResolvedDelegateProxy_reverts(string memory _slot0Value) public { - // Assume the slot 0 value is not "OVM_L1CrossDomainMessenger". - vm.assume(keccak256(abi.encode(_slot0Value)) != keccak256(abi.encode("OVM_L1CrossDomainMessenger"))); + /// @param _slot0Value The raw value to store in the ResolvedDelegateProxy name slot. + function test_proxyAdmin_notResolvedDelegateProxy_reverts(bytes32 _slot0Value) public { + vm.assume(_slot0Value != resolvedDelegateProxyNameSlotValue()); + setResolvedDelegateProxyNameSlot(_slot0Value); - // Unset the standard proxy owner slot. - vm.store(address(harness), bytes32(Constants.PROXY_OWNER_ADDRESS), bytes32(0)); - - // Store the slot 0 value. - harness.setSlot0(address(harness), _slot0Value); - - // Expect a revert. vm.expectRevert(ProxyAdminOwnedBase.ProxyAdminOwnedBase_NotResolvedDelegateProxy.selector); harness.proxyAdmin(); } /// @notice Tests that the proxyAdmin function reverts if the proxy admin is not found. function test_proxyAdmin_proxyAdminNotFound_reverts() public { - // Unset the standard proxy owner slot. - vm.store(address(harness), bytes32(Constants.PROXY_OWNER_ADDRESS), bytes32(0)); - - // Store the string "OVM_L1CrossDomainMessenger" in slot 0. - harness.setSlot0(address(harness), "OVM_L1CrossDomainMessenger"); + setResolvedDelegateProxy(address(0)); - // Store address(0) in slot 1. - harness.setSlot1(address(harness), address(0)); - - // Expect a revert. vm.expectRevert(ProxyAdminOwnedBase.ProxyAdminOwnedBase_ProxyAdminNotFound.selector); harness.proxyAdmin(); } @@ -136,13 +133,10 @@ contract ProxyAdminOwnedBase_assertSharedProxyAdminOwner_Test is ProxyAdminOwned /// @notice Tests that the assertSharedProxyAdminOwner function does not revert if the provided /// proxy has the same owner as the current contract. function test_assertSharedProxyAdminOwner_sameOwner_succeeds(address _proxy) public { - // Assume the provided proxy is not a forge address. assumeNotForgeAddress(_proxy); - // Mock the proxyAdminOwner function to return the same owner as the current contract. vm.mockCall(_proxy, abi.encodeCall(ProxyAdminOwnedBase.proxyAdminOwner, ()), abi.encode(proxyAdminOwner)); - // Expect no revert. harness.assertSharedProxyAdminOwner(_proxy); } @@ -154,17 +148,11 @@ contract ProxyAdminOwnedBase_assertSharedProxyAdminOwner_Test is ProxyAdminOwned ) public { - // Assume the provided proxy is not a forge address. assumeNotForgeAddress(_proxy); - assumeNotForgeAddress(_otherProxyOwner); - - // Assume the other proxy owner is not the same as the current owner. vm.assume(_otherProxyOwner != proxyAdminOwner); - // Mock the proxyAdminOwner function to return the other proxy owner. vm.mockCall(_proxy, abi.encodeCall(ProxyAdminOwnedBase.proxyAdminOwner, ()), abi.encode(_otherProxyOwner)); - // Expect a revert. vm.expectRevert(ProxyAdminOwnedBase.ProxyAdminOwnedBase_NotSharedProxyAdminOwner.selector); harness.assertSharedProxyAdminOwner(_proxy); } @@ -174,10 +162,7 @@ contract ProxyAdminOwnedBase_assertOnlyProxyAdmin_Test is ProxyAdminOwnedBase_Te /// @notice Tests that the assertOnlyProxyAdmin function does not revert if the caller is the /// ProxyAdmin. function test_assertOnlyProxyAdmin_proxyAdmin_succeeds() public { - // Prank as the ProxyAdmin. vm.prank(address(proxyAdmin)); - - // Expect no revert. harness.assertOnlyProxyAdmin(); } @@ -185,11 +170,9 @@ contract ProxyAdminOwnedBase_assertOnlyProxyAdmin_Test is ProxyAdminOwnedBase_Te /// ProxyAdmin. /// @param _sender The address of the sender to test. function test_assertOnlyProxyAdmin_notProxyAdmin_reverts(address _sender) public { - // Prank as the not ProxyAdmin. vm.assume(_sender != address(proxyAdmin)); vm.prank(_sender); - // Expect a revert. vm.expectRevert(ProxyAdminOwnedBase.ProxyAdminOwnedBase_NotProxyAdmin.selector); harness.assertOnlyProxyAdmin(); } @@ -199,10 +182,7 @@ contract ProxyAdminOwnedBase_assertOnlyProxyAdminOwner_Test is ProxyAdminOwnedBa /// @notice Tests that the assertOnlyProxyAdminOwner function does not revert if the caller is /// the ProxyAdmin owner. function test_assertOnlyProxyAdminOwner_proxyAdminOwner_succeeds() public { - // Prank as the ProxyAdmin owner. vm.prank(proxyAdminOwner); - - // Expect no revert. harness.assertOnlyProxyAdminOwner(); } @@ -210,11 +190,9 @@ contract ProxyAdminOwnedBase_assertOnlyProxyAdminOwner_Test is ProxyAdminOwnedBa /// ProxyAdmin owner. /// @param _sender The address of the sender to test. function test_assertOnlyProxyAdminOwner_notProxyAdminOwner_reverts(address _sender) public { - // Prank as the not ProxyAdmin owner. vm.assume(_sender != proxyAdminOwner); vm.prank(_sender); - // Expect a revert. vm.expectRevert(ProxyAdminOwnedBase.ProxyAdminOwnedBase_NotProxyAdminOwner.selector); harness.assertOnlyProxyAdminOwner(); } @@ -224,20 +202,14 @@ contract ProxyAdminOwnedBase_assertOnlyProxyAdminOrProxyAdminOwner_Test is Proxy /// @notice Tests that the assertOnlyProxyAdminOrProxyAdminOwner function does not revert if /// the caller is the ProxyAdmin or the ProxyAdmin owner. function test_assertOnlyProxyAdminOrProxyAdminOwner_proxyAdmin_succeeds() public { - // Prank as the ProxyAdmin. vm.prank(address(proxyAdmin)); - - // Expect no revert. harness.assertOnlyProxyAdminOrProxyAdminOwner(); } /// @notice Tests that the assertOnlyProxyAdminOrProxyAdminOwner function does not revert if /// the caller is the ProxyAdmin owner. function test_assertOnlyProxyAdminOrProxyAdminOwner_proxyAdminOwner_succeeds() public { - // Prank as the ProxyAdmin owner. vm.prank(proxyAdminOwner); - - // Expect no revert. harness.assertOnlyProxyAdminOrProxyAdminOwner(); } @@ -245,11 +217,9 @@ contract ProxyAdminOwnedBase_assertOnlyProxyAdminOrProxyAdminOwner_Test is Proxy /// is not the ProxyAdmin or the ProxyAdmin owner. /// @param _sender The address of the sender to test. function test_assertOnlyProxyAdminOrProxyAdminOwner_notProxyAdminOrProxyAdminOwner_reverts(address _sender) public { - // Prank as the not ProxyAdmin or ProxyAdmin owner. vm.assume(_sender != address(proxyAdmin) && _sender != proxyAdminOwner); vm.prank(_sender); - // Expect a revert. vm.expectRevert(ProxyAdminOwnedBase.ProxyAdminOwnedBase_NotProxyAdminOrProxyAdminOwner.selector); harness.assertOnlyProxyAdminOrProxyAdminOwner(); } diff --git a/test/universal/ReinitializableBase.t.sol b/test/universal/ReinitializableBase.t.sol index 5c7055cb..58f3445b 100644 --- a/test/universal/ReinitializableBase.t.sol +++ b/test/universal/ReinitializableBase.t.sol @@ -7,27 +7,18 @@ import { Test } from "lib/forge-std/src/Test.sol"; // Contracts import { ReinitializableBase } from "src/universal/ReinitializableBase.sol"; -/// @title ReinitializableBase_Harness -/// @notice Harness contract to allow direct instantiation and testing of `ReinitializableBase` -/// logic. contract ReinitializableBase_Harness is ReinitializableBase { constructor(uint8 _initVersion) ReinitializableBase(_initVersion) { } } -/// @title ReinitializableBase_Constructor_Test -/// @notice Tests the constructor of the `ReinitializableBase` contract. contract ReinitializableBase_Constructor_Test is Test { - /// @notice Tests that the contract creation reverts when init version is zero. - function test_constructor_zeroVersion_reverts() public { + function test_constructor_zeroVersion_reverts() external { vm.expectRevert(ReinitializableBase.ReinitializableBase_ZeroInitVersion.selector); new ReinitializableBase_Harness(0); } - /// @notice Tests that constructor succeeds with valid non-zero init versions. - /// @param _initVersion Init version to use when creating the contract. - function testFuzz_constructor_validVersion_succeeds(uint8 _initVersion) public { + function testFuzz_constructor_validVersion_succeeds(uint8 _initVersion) external { _initVersion = uint8(bound(_initVersion, 1, type(uint8).max)); - ReinitializableBase_Harness harness = new ReinitializableBase_Harness(_initVersion); - assertEq(harness.initVersion(), _initVersion); + assertEq(new ReinitializableBase_Harness(_initVersion).initVersion(), _initVersion); } } diff --git a/test/universal/StandardBridge.t.sol b/test/universal/StandardBridge.t.sol index 9e9ce971..dcf46190 100644 --- a/test/universal/StandardBridge.t.sol +++ b/test/universal/StandardBridge.t.sol @@ -3,15 +3,15 @@ pragma solidity 0.8.15; import { StandardBridge } from "src/universal/StandardBridge.sol"; import { CommonTest } from "test/setup/CommonTest.sol"; -import { OptimismMintableERC20, ILegacyMintableERC20 } from "src/universal/OptimismMintableERC20.sol"; +import { OptimismMintableERC20 } from "src/universal/OptimismMintableERC20.sol"; +import { ILegacyMintableERC20 } from "interfaces/legacy/ILegacyMintableERC20.sol"; import { ERC20 } from "lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; +import { IERC165 } from "lib/openzeppelin-contracts/contracts/utils/introspection/IERC165.sol"; /// @title StandardBridgeTester /// @notice Simple wrapper around the StandardBridge contract that exposes /// internal functions so they can be more easily tested directly. contract StandardBridgeTester is StandardBridge { - constructor() StandardBridge() { } - function isOptimismMintableERC20(address _token) external view returns (bool) { return _isOptimismMintableERC20(_token); } @@ -36,15 +36,9 @@ contract LegacyMintable is ERC20 { function burn(address _from, uint256 _amount) external pure { } - /// @notice Implements ERC165. This implementation should not be changed as - /// it is how the actual legacy optimism mintable token does the - /// check. Allows for testing against code that is has been deployed, - /// assuming different compiler version is no problem. + /// @notice Matches the deployed legacy mintable token's ERC165 support. function supportsInterface(bytes4 _interfaceId) external pure returns (bool) { - bytes4 firstSupportedInterface = bytes4(keccak256("supportsInterface(bytes4)")); // ERC165 - bytes4 secondSupportedInterface = ILegacyMintableERC20.l1Token.selector ^ ILegacyMintableERC20.mint.selector - ^ ILegacyMintableERC20.burn.selector; - return _interfaceId == firstSupportedInterface || _interfaceId == secondSupportedInterface; + return _interfaceId == type(IERC165).interfaceId || _interfaceId == type(ILegacyMintableERC20).interfaceId; } } @@ -53,9 +47,10 @@ contract LegacyMintable is ERC20 { /// @dev This setup is primarily for tests focusing on internal stateless logic or default states /// of the `StandardBridge` contract. abstract contract StandardBridge_TestInit is CommonTest { + address internal constant OTHER_TOKEN = address(0x20); + StandardBridgeTester internal bridge; OptimismMintableERC20 internal mintable; - ERC20 internal erc20; LegacyMintable internal legacy; function setUp() public override { @@ -67,7 +62,6 @@ abstract contract StandardBridge_TestInit is CommonTest { _bridge: address(0), _remoteToken: address(0), _name: "Stonks", _symbol: "STONK", _decimals: 18 }); - erc20 = new ERC20("Altcoin", "ALT"); legacy = new LegacyMintable("Legacy", "LEG"); } } @@ -84,40 +78,28 @@ contract StandardBridge_Paused_Test is StandardBridge_TestInit { /// @title StandardBridge_IsOptimismMintableERC20_Test /// @notice Tests the `_isOptimismMintableERC20` internal function of `StandardBridge`. contract StandardBridge_IsOptimismMintableERC20_Test is StandardBridge_TestInit { - /// @notice Test coverage for identifying OptimismMintableERC20 tokens. This function should - /// return true for both modern and legacy OptimismMintableERC20 tokens and false for - /// any accounts that do not implement the interface. function test_isOptimismMintableERC20_succeeds() external view { - // Both the modern and legacy mintable tokens should return true assertTrue(bridge.isOptimismMintableERC20(address(mintable))); assertTrue(bridge.isOptimismMintableERC20(address(legacy))); - // A regular ERC20 should return false - assertFalse(bridge.isOptimismMintableERC20(address(erc20))); - // Non existent contracts should return false and not revert - assertEq(address(0x20).code.length, 0); - assertFalse(bridge.isOptimismMintableERC20(address(0x20))); + assertFalse(bridge.isOptimismMintableERC20(address(L1Token))); + + assertEq(OTHER_TOKEN.code.length, 0); + assertFalse(bridge.isOptimismMintableERC20(OTHER_TOKEN)); } } /// @title StandardBridge_IsCorrectTokenPair_Test /// @notice Tests the `_isCorrectTokenPair` internal function of `StandardBridge`. contract StandardBridge_IsCorrectTokenPair_Test is StandardBridge_TestInit { - /// @notice Test coverage of `isCorrectTokenPair` under different types of tokens. function test_isCorrectTokenPair_succeeds() external { - // Modern + known to be correct remote token assertTrue(bridge.isCorrectTokenPair(address(mintable), mintable.remoteToken())); - // Modern + known to be correct l1Token (legacy interface) assertTrue(bridge.isCorrectTokenPair(address(mintable), mintable.l1Token())); - // Modern + known to be incorrect remote token - assertTrue(mintable.remoteToken() != address(0x20)); - assertFalse(bridge.isCorrectTokenPair(address(mintable), address(0x20))); - // Legacy + known to be correct l1Token + assertFalse(bridge.isCorrectTokenPair(address(mintable), OTHER_TOKEN)); + assertTrue(bridge.isCorrectTokenPair(address(legacy), legacy.l1Token())); - // Legacy + known to be incorrect l1Token - assertTrue(legacy.l1Token() != address(0x20)); - assertFalse(bridge.isCorrectTokenPair(address(legacy), address(0x20))); - // A token that doesn't support either modern or legacy interface will revert + assertFalse(bridge.isCorrectTokenPair(address(legacy), OTHER_TOKEN)); + vm.expectRevert(); // nosemgrep: sol-safety-expectrevert-no-args - bridge.isCorrectTokenPair(address(erc20), address(1)); + bridge.isCorrectTokenPair(address(L1Token), address(1)); } } diff --git a/test/universal/WETH98.t.sol b/test/universal/WETH98.t.sol index 323a4ddb..17316686 100644 --- a/test/universal/WETH98.t.sol +++ b/test/universal/WETH98.t.sol @@ -5,8 +5,7 @@ pragma solidity 0.8.15; import { Test } from "lib/forge-std/src/Test.sol"; // Contracts -import { IWETH98 } from "interfaces/universal/IWETH98.sol"; -import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; +import { WETH98 } from "src/universal/WETH98.sol"; /// @title WETH98_TestInit /// @notice Reusable test initialization for `WETH98` tests. @@ -16,19 +15,20 @@ abstract contract WETH98_TestInit is Test { event Deposit(address indexed dst, uint256 wad); event Withdrawal(address indexed src, uint256 wad); - IWETH98 public weth; + WETH98 public weth; address alice; address bob; function setUp() public { - weth = IWETH98( - DeployUtils.create1({ - _name: "WETH98", _args: DeployUtils.encodeConstructor(abi.encodeCall(IWETH98.__constructor__, ())) - }) - ); + weth = new WETH98(); alice = makeAddr("alice"); bob = makeAddr("bob"); - deal(alice, 1 ether); + } + + function _depositAlice(uint256 _amount) internal { + deal(alice, _amount); + vm.prank(alice); + weth.deposit{ value: _amount }(); } } @@ -36,6 +36,7 @@ abstract contract WETH98_TestInit is Test { /// @notice Tests the `receive` function of the `WETH98` contract. contract WETH98_Receive_Test is WETH98_TestInit { function test_receive_succeeds() public { + deal(alice, 1 ether); vm.expectEmit(address(weth)); emit Deposit(alice, 1 ether); vm.prank(alice); @@ -49,6 +50,7 @@ contract WETH98_Receive_Test is WETH98_TestInit { /// @notice Tests the `fallback` function of the `WETH98` contract. contract WETH98_Fallback_Test is WETH98_TestInit { function test_fallback_succeeds() public { + deal(alice, 1 ether); vm.expectEmit(address(weth)); emit Deposit(alice, 1 ether); vm.prank(alice); @@ -62,6 +64,7 @@ contract WETH98_Fallback_Test is WETH98_TestInit { /// @notice Tests the `deposit` function of the `WETH98` contract. contract WETH98_Deposit_Test is WETH98_TestInit { function test_deposit_succeeds() public { + deal(alice, 1 ether); vm.expectEmit(address(weth)); emit Deposit(alice, 1 ether); vm.prank(alice); @@ -74,8 +77,7 @@ contract WETH98_Deposit_Test is WETH98_TestInit { /// @notice Tests the `withdraw` function of the `WETH98` contract. contract WETH98_Withdraw_Test is WETH98_TestInit { function test_withdraw_succeeds() public { - vm.prank(alice); - weth.deposit{ value: 1 ether }(); + _depositAlice(1 ether); vm.expectEmit(address(weth)); emit Withdrawal(alice, 1 ether); vm.prank(alice); @@ -84,8 +86,7 @@ contract WETH98_Withdraw_Test is WETH98_TestInit { } function test_withdraw_partialWithdrawal_succeeds() public { - vm.prank(alice); - weth.deposit{ value: 1 ether }(); + _depositAlice(1 ether); vm.expectEmit(address(weth)); emit Withdrawal(alice, 1 ether / 2); vm.prank(alice); @@ -94,9 +95,8 @@ contract WETH98_Withdraw_Test is WETH98_TestInit { } function test_withdraw_tooLargeWithdrawal_fails() public { - vm.prank(alice); - weth.deposit{ value: 1 ether }(); - vm.expectRevert(); + _depositAlice(1 ether); + vm.expectRevert(bytes("")); vm.prank(alice); weth.withdraw(1 ether + 1); } @@ -118,8 +118,7 @@ contract WETH98_Approve_Test is WETH98_TestInit { /// @notice Tests the `transfer` function of the `WETH98` contract. contract WETH98_Transfer_Test is WETH98_TestInit { function test_transfer_succeeds() public { - vm.prank(alice); - weth.deposit{ value: 1 ether }(); + _depositAlice(1 ether); vm.expectEmit(address(weth)); emit Transfer(alice, bob, 1 ether); vm.prank(alice); @@ -129,9 +128,8 @@ contract WETH98_Transfer_Test is WETH98_TestInit { } function test_transfer_tooLarge_fails() public { - vm.prank(alice); - weth.deposit{ value: 1 ether }(); - vm.expectRevert(); + _depositAlice(1 ether); + vm.expectRevert(bytes("")); vm.prank(alice); weth.transfer(bob, 1 ether + 1); } @@ -141,8 +139,7 @@ contract WETH98_Transfer_Test is WETH98_TestInit { /// @notice Tests the `transferFrom` function of the `WETH98` contract. contract WETH98_TransferFrom_Test is WETH98_TestInit { function test_transferFrom_succeeds() public { - vm.prank(alice); - weth.deposit{ value: 1 ether }(); + _depositAlice(1 ether); vm.prank(alice); weth.approve(bob, 1 ether); vm.expectEmit(address(weth)); @@ -154,21 +151,19 @@ contract WETH98_TransferFrom_Test is WETH98_TestInit { } function test_transferFrom_tooLittleApproval_fails() public { - vm.prank(alice); - weth.deposit{ value: 1 ether }(); + _depositAlice(1 ether); vm.prank(alice); weth.approve(bob, 1 ether); - vm.expectRevert(); + vm.expectRevert(bytes("")); vm.prank(bob); weth.transferFrom(alice, bob, 1 ether + 1); } function test_transferFrom_tooLittleBalance_fails() public { - vm.prank(alice); - weth.deposit{ value: 1 ether }(); + _depositAlice(1 ether); vm.prank(alice); weth.approve(bob, 2 ether); - vm.expectRevert(); + vm.expectRevert(bytes("")); vm.prank(bob); weth.transferFrom(alice, bob, 1 ether + 1); } diff --git a/test/utils/CBMulticall.t.sol b/test/utils/CBMulticall.t.sol index 28136e2b..19b0078d 100644 --- a/test/utils/CBMulticall.t.sol +++ b/test/utils/CBMulticall.t.sol @@ -4,9 +4,7 @@ pragma solidity 0.8.25; import { CBMulticall, Call, Call3, Call3Value, Result } from "src/universal/CBMulticall.sol"; import { Test } from "lib/forge-std/src/Test.sol"; -/// @dev Helper contract used to invoke `aggregateDelegateCalls` via `delegatecall`. -/// This simulates the intended multisig usage pattern where the multicall -/// logic is executed in the context of another contract. +/// @dev Invokes `aggregateDelegateCalls` through `delegatecall` to simulate multisig execution. contract CBMulticallDelegateCaller { CBMulticall public mc; @@ -15,8 +13,13 @@ contract CBMulticallDelegateCaller { } function aggregateDelegateCalls(Call3[] calldata calls) external returns (Result[] memory) { - (, bytes memory data) = + (bool success, bytes memory data) = address(mc).delegatecall(abi.encodeWithSelector(CBMulticall.aggregateDelegateCalls.selector, calls)); + if (!success) { + assembly ("memory-safe") { + revert(add(data, 0x20), mload(data)) + } + } return abi.decode(data, (Result[])); } } @@ -36,20 +39,40 @@ contract MockReceiver { } contract CBMulticallTest is Test { + string internal constant MULTICALL_CALL_FAILED = "Multicall3: call failed"; + CBMulticall mc; MockReceiver target; - CBMulticallDelegateCaller delegateCaller; function setUp() public { mc = new CBMulticall(); target = new MockReceiver(); - delegateCaller = new CBMulticallDelegateCaller(mc); + } + + function _bumpCall(uint256 x) internal view returns (Call memory) { + return Call({ target: address(target), callData: abi.encodeCall(MockReceiver.bump, (x)) }); + } + + function _revertingCall() internal view returns (Call memory) { + return Call({ target: address(target), callData: abi.encodeCall(MockReceiver.willRevert, ()) }); + } + + function _bumpCall3(bool allowFailure, uint256 x) internal view returns (Call3 memory) { + return Call3({ + target: address(target), allowFailure: allowFailure, callData: abi.encodeCall(MockReceiver.bump, (x)) + }); + } + + function _revertingCall3(bool allowFailure) internal view returns (Call3 memory) { + return Call3({ + target: address(target), allowFailure: allowFailure, callData: abi.encodeCall(MockReceiver.willRevert, ()) + }); } function test_aggregate_returnsBlockNumberAndData() external { Call[] memory calls = new Call[](2); - calls[0] = Call({ target: address(target), callData: abi.encodeWithSelector(MockReceiver.bump.selector, 41) }); - calls[1] = Call({ target: address(target), callData: abi.encodeWithSelector(MockReceiver.bump.selector, 1) }); + calls[0] = _bumpCall(41); + calls[1] = _bumpCall(1); (uint256 bn, bytes[] memory rdata) = mc.aggregate(calls); assertEq(bn, block.number); @@ -59,16 +82,16 @@ contract CBMulticallTest is Test { function test_aggregate_revertsOnFailedCall() external { Call[] memory calls = new Call[](2); - calls[0] = Call({ target: address(target), callData: abi.encodeWithSelector(MockReceiver.bump.selector, 0) }); - calls[1] = Call({ target: address(target), callData: abi.encodeWithSelector(MockReceiver.willRevert.selector) }); - vm.expectRevert(bytes("Multicall3: call failed")); + calls[0] = _bumpCall(0); + calls[1] = _revertingCall(); + vm.expectRevert(bytes(MULTICALL_CALL_FAILED)); mc.aggregate(calls); } function test_tryAggregate_noRequire_returnsResults() external { Call[] memory calls = new Call[](2); - calls[0] = Call({ target: address(target), callData: abi.encodeWithSelector(MockReceiver.bump.selector, 1) }); - calls[1] = Call({ target: address(target), callData: abi.encodeWithSelector(MockReceiver.willRevert.selector) }); + calls[0] = _bumpCall(1); + calls[1] = _revertingCall(); Result[] memory results = mc.tryAggregate(false, calls); assertEq(results.length, 2); @@ -79,16 +102,16 @@ contract CBMulticallTest is Test { function test_tryAggregate_requireSuccess_revertsOnFailure() external { Call[] memory calls = new Call[](2); - calls[0] = Call({ target: address(target), callData: abi.encodeWithSelector(MockReceiver.bump.selector, 0) }); - calls[1] = Call({ target: address(target), callData: abi.encodeWithSelector(MockReceiver.willRevert.selector) }); - vm.expectRevert(bytes("Multicall3: call failed")); + calls[0] = _bumpCall(0); + calls[1] = _revertingCall(); + vm.expectRevert(bytes(MULTICALL_CALL_FAILED)); mc.tryAggregate(true, calls); } function test_tryBlockAndAggregate_noRequire_returnsBlockInfoAndResults() external { Call[] memory calls = new Call[](2); - calls[0] = Call({ target: address(target), callData: abi.encodeWithSelector(MockReceiver.bump.selector, 0) }); - calls[1] = Call({ target: address(target), callData: abi.encodeWithSelector(MockReceiver.willRevert.selector) }); + calls[0] = _bumpCall(0); + calls[1] = _revertingCall(); (uint256 bn, bytes32 bh, Result[] memory res) = mc.tryBlockAndAggregate(false, calls); assertEq(bn, block.number); @@ -100,8 +123,8 @@ contract CBMulticallTest is Test { function test_blockAndAggregate_allSuccess_returnsResults() external { Call[] memory calls = new Call[](2); - calls[0] = Call({ target: address(target), callData: abi.encodeWithSelector(MockReceiver.bump.selector, 1) }); - calls[1] = Call({ target: address(target), callData: abi.encodeWithSelector(MockReceiver.bump.selector, 2) }); + calls[0] = _bumpCall(1); + calls[1] = _bumpCall(2); (uint256 bn, bytes32 bh, Result[] memory res) = mc.blockAndAggregate(calls); assertEq(bn, block.number); assertEq(bh, blockhash(block.number)); @@ -111,126 +134,101 @@ contract CBMulticallTest is Test { function test_blockAndAggregate_revertsOnFailure() external { Call[] memory calls = new Call[](2); - calls[0] = Call({ target: address(target), callData: abi.encodeWithSelector(MockReceiver.bump.selector, 0) }); - calls[1] = Call({ target: address(target), callData: abi.encodeWithSelector(MockReceiver.willRevert.selector) }); - vm.expectRevert(bytes("Multicall3: call failed")); + calls[0] = _bumpCall(0); + calls[1] = _revertingCall(); + vm.expectRevert(bytes(MULTICALL_CALL_FAILED)); mc.blockAndAggregate(calls); } function test_tryBlockAndAggregate_requireSuccess_revertsOnFailure() external { Call[] memory calls = new Call[](2); - calls[0] = Call({ target: address(target), callData: abi.encodeWithSelector(MockReceiver.bump.selector, 0) }); - calls[1] = Call({ target: address(target), callData: abi.encodeWithSelector(MockReceiver.willRevert.selector) }); - vm.expectRevert(bytes("Multicall3: call failed")); + calls[0] = _bumpCall(0); + calls[1] = _revertingCall(); + vm.expectRevert(bytes(MULTICALL_CALL_FAILED)); mc.tryBlockAndAggregate(true, calls); } function test_aggregate3_success() external { - Call3[] memory calls3 = new Call3[](1); - calls3[0] = Call3({ - target: address(target), - allowFailure: false, - callData: abi.encodeWithSelector(MockReceiver.bump.selector, 4) - }); - Result[] memory ret3 = mc.aggregate3(calls3); - assertTrue(ret3[0].success); - assertEq(abi.decode(ret3[0].returnData, (uint256)), 5); + Call3[] memory calls = new Call3[](1); + calls[0] = _bumpCall3(false, 4); + Result[] memory results = mc.aggregate3(calls); + assertTrue(results[0].success); + assertEq(abi.decode(results[0].returnData, (uint256)), 5); } function test_aggregate3_allowedFailure_returnsFalse() external { - Call3[] memory calls3 = new Call3[](1); - calls3[0] = Call3({ - target: address(target), - allowFailure: true, - callData: abi.encodeWithSelector(MockReceiver.willRevert.selector) - }); - Result[] memory ret3 = mc.aggregate3(calls3); - assertFalse(ret3[0].success); + Call3[] memory calls = new Call3[](1); + calls[0] = _revertingCall3(true); + Result[] memory results = mc.aggregate3(calls); + assertFalse(results[0].success); } function test_aggregate3_revertsOnNonAllowedFailure() external { - Call3[] memory calls3 = new Call3[](1); - calls3[0] = Call3({ - target: address(target), - allowFailure: false, - callData: abi.encodeWithSelector(MockReceiver.willRevert.selector) - }); - vm.expectRevert(bytes("Multicall3: call failed")); - mc.aggregate3(calls3); + Call3[] memory calls = new Call3[](1); + calls[0] = _revertingCall3(false); + vm.expectRevert(bytes(MULTICALL_CALL_FAILED)); + mc.aggregate3(calls); } function test_aggregateDelegateCalls_success() external { - Call3[] memory calls3 = new Call3[](1); - calls3[0] = Call3({ - target: address(target), - allowFailure: false, - callData: abi.encodeWithSelector(MockReceiver.bump.selector, 4) - }); - Result[] memory ret3 = delegateCaller.aggregateDelegateCalls(calls3); - assertTrue(ret3[0].success); - assertEq(abi.decode(ret3[0].returnData, (uint256)), 5); + CBMulticallDelegateCaller caller = new CBMulticallDelegateCaller(mc); + Call3[] memory calls = new Call3[](1); + calls[0] = _bumpCall3(false, 4); + Result[] memory results = caller.aggregateDelegateCalls(calls); + assertTrue(results[0].success); + assertEq(abi.decode(results[0].returnData, (uint256)), 5); } function test_aggregateDelegateCalls_allowedFailure_returnsFalse() external { - Call3[] memory calls3 = new Call3[](1); - calls3[0] = Call3({ - target: address(target), - allowFailure: true, - callData: abi.encodeWithSelector(MockReceiver.willRevert.selector) - }); - Result[] memory ret3 = delegateCaller.aggregateDelegateCalls(calls3); - assertFalse(ret3[0].success); + CBMulticallDelegateCaller caller = new CBMulticallDelegateCaller(mc); + Call3[] memory calls = new Call3[](1); + calls[0] = _revertingCall3(true); + Result[] memory results = caller.aggregateDelegateCalls(calls); + assertFalse(results[0].success); } function test_aggregateDelegateCalls_revertsOnNonAllowedFailure() external { - Call3[] memory calls3 = new Call3[](1); - calls3[0] = Call3({ - target: address(target), - allowFailure: false, - callData: abi.encodeWithSelector(MockReceiver.willRevert.selector) - }); - vm.expectRevert(); - delegateCaller.aggregateDelegateCalls(calls3); + CBMulticallDelegateCaller caller = new CBMulticallDelegateCaller(mc); + Call3[] memory calls = new Call3[](1); + calls[0] = _revertingCall3(false); + vm.expectRevert(bytes(MULTICALL_CALL_FAILED)); + caller.aggregateDelegateCalls(calls); } function test_aggregateDelegateCalls_directCall_revertsWithMustDelegateCall() external { - Call3[] memory calls3 = new Call3[](1); - calls3[0] = Call3({ - target: address(target), - allowFailure: false, - callData: abi.encodeWithSelector(MockReceiver.bump.selector, 1) - }); + Call3[] memory calls = new Call3[](1); + calls[0] = _bumpCall3(false, 1); vm.expectRevert(CBMulticall.MustDelegateCall.selector); - mc.aggregateDelegateCalls(calls3); + mc.aggregateDelegateCalls(calls); } function test_aggregate3Value_success_usesContractBalance() external { vm.deal(address(mc), 1 ether); - Call3Value[] memory callsV = new Call3Value[](1); - callsV[0] = Call3Value({ + Call3Value[] memory calls = new Call3Value[](1); + calls[0] = Call3Value({ target: address(target), allowFailure: false, value: 0.5 ether, - callData: abi.encodeWithSelector(MockReceiver.payAndEcho.selector, 7) + callData: abi.encodeCall(MockReceiver.payAndEcho, (7)) }); - Result[] memory retV = mc.aggregate3Value(callsV); - (uint256 x, uint256 v) = abi.decode(retV[0].returnData, (uint256, uint256)); + Result[] memory results = mc.aggregate3Value(calls); + (uint256 x, uint256 v) = abi.decode(results[0].returnData, (uint256, uint256)); assertEq(x, 7); assertEq(v, 0.5 ether); assertEq(address(target).balance, 0.5 ether); } function test_aggregate3Value_revertsOnNonAllowedFailure() external { - Call3Value[] memory callsV = new Call3Value[](1); - callsV[0] = Call3Value({ + Call3Value[] memory calls = new Call3Value[](1); + calls[0] = Call3Value({ target: address(target), allowFailure: false, value: 0, - callData: abi.encodeWithSelector(MockReceiver.willRevert.selector) + callData: abi.encodeCall(MockReceiver.willRevert, ()) }); - vm.expectRevert(bytes("Multicall3: call failed")); - mc.aggregate3Value(callsV); + vm.expectRevert(bytes(MULTICALL_CALL_FAILED)); + mc.aggregate3Value(calls); } function test_getBlockNumber() external view { diff --git a/test/vendor/AddressAliasHelper.t.sol b/test/vendor/AddressAliasHelper.t.sol index 76747cec..bdeaeb76 100644 --- a/test/vendor/AddressAliasHelper.t.sol +++ b/test/vendor/AddressAliasHelper.t.sol @@ -8,7 +8,6 @@ import { AddressAliasHelper } from "src/vendor/AddressAliasHelper.sol"; /// @notice General tests that are not testing any function directly of the `AddressAliasHelper` /// contract or are testing multiple functions at once. contract AddressAliasHelper_Uncategorized_Test is Test { - /// @notice Tests that applying and then undoing an alias results in the original address. function testFuzz_applyAndUndo_succeeds(address _address) external pure { address aliased = AddressAliasHelper.applyL1ToL2Alias(_address); address unaliased = AddressAliasHelper.undoL1ToL2Alias(aliased); diff --git a/test/vendor/Initializable.t.sol b/test/vendor/Initializable.t.sol index e36f9fd3..9bdd9df9 100644 --- a/test/vendor/Initializable.t.sol +++ b/test/vendor/Initializable.t.sol @@ -22,9 +22,7 @@ import { TEEProverRegistry } from "src/L1/proofs/tee/TEEProverRegistry.sol"; /// @title Initializer_Test /// @dev Ensures that the `initialize()` function on contracts cannot be called more than -/// once. This contract inherits from `ERC721Bridge_Initializer` because it is the -/// deepest contract in the inheritance chain for setting up the system contracts. -/// For each L1 contract both the implementation and the proxy are tested. +/// once. For each L1 contract both the implementation and the proxy are tested. contract Initializer_Test is CommonTest { /// @notice Contains the address of an `Initializable` contract and the calldata /// used to initialize it. @@ -37,263 +35,195 @@ contract Initializer_Test is CommonTest { /// @notice Array of contracts to test. InitializeableContract[] contracts; - /// @notice Mapping of nickname to actual contract name. - /// @dev Nicknames are only used when one proxy contract has multiple potential implementations - /// as can happen when a new implementation is being developed. - mapping(string => string) nicknames; - function setUp() public override { super.setUp(); - // Initialize the `contracts` array with the addresses of the contracts to test, the - // calldata used to initialize them, and the storage slot of their `_initialized` flag. + // Initialize the `contracts` array with the addresses of the contracts to test and the + // calldata used to initialize them. // This array should contain all initializable L1 contracts. L2 contract initialization is // tested in Predeploys.t.sol. - // The 'name' field should be the name of the contract as it saved in the deployment + // The 'name' field should be the name of the contract as it is saved in the deployment // script. - // L1CrossDomainMessengerImpl + bytes memory initCalldata = abi.encodeCall(l1CrossDomainMessenger.initialize, (systemConfig, optimismPortal2)); contracts.push( InitializeableContract({ name: "L1CrossDomainMessengerImpl", target: addressManager.getAddress("OVM_L1CrossDomainMessenger"), - initCalldata: abi.encodeCall(l1CrossDomainMessenger.initialize, (systemConfig, optimismPortal2)) + initCalldata: initCalldata }) ); - // L1CrossDomainMessengerProxy contracts.push( InitializeableContract({ - name: "L1CrossDomainMessengerProxy", - target: address(l1CrossDomainMessenger), - initCalldata: abi.encodeCall(l1CrossDomainMessenger.initialize, (systemConfig, optimismPortal2)) + name: "L1CrossDomainMessengerProxy", target: address(l1CrossDomainMessenger), initCalldata: initCalldata }) ); - // DisputeGameFactoryImpl + + initCalldata = abi.encodeCall(disputeGameFactory.initialize, (address(0))); contracts.push( InitializeableContract({ name: "DisputeGameFactoryImpl", target: EIP1967Helper.getImplementation(address(disputeGameFactory)), - initCalldata: abi.encodeCall(disputeGameFactory.initialize, (address(0))) + initCalldata: initCalldata }) ); - // DisputeGameFactoryProxy contracts.push( InitializeableContract({ - name: "DisputeGameFactoryProxy", - target: address(disputeGameFactory), - initCalldata: abi.encodeCall(disputeGameFactory.initialize, (address(0))) + name: "DisputeGameFactoryProxy", target: address(disputeGameFactory), initCalldata: initCalldata }) ); - // DelayedWETHImpl + + initCalldata = abi.encodeCall(delayedWeth.initialize, (ISystemConfig(address(0)))); contracts.push( InitializeableContract({ name: "DelayedWETHImpl", target: EIP1967Helper.getImplementation(address(delayedWeth)), - initCalldata: abi.encodeCall(delayedWeth.initialize, (ISystemConfig(address(0)))) + initCalldata: initCalldata }) ); - // DelayedWETHProxy contracts.push( InitializeableContract({ - name: "DelayedWETHProxy", - target: address(delayedWeth), - initCalldata: abi.encodeCall(delayedWeth.initialize, (ISystemConfig(address(0)))) + name: "DelayedWETHProxy", target: address(delayedWeth), initCalldata: initCalldata }) ); - // OptimismPortal2Impl + initCalldata = abi.encodeCall(optimismPortal2.initialize, (systemConfig, anchorStateRegistry)); contracts.push( InitializeableContract({ name: "OptimismPortal2Impl", target: EIP1967Helper.getImplementation(address(optimismPortal2)), - initCalldata: abi.encodeCall(optimismPortal2.initialize, (systemConfig, anchorStateRegistry)) + initCalldata: initCalldata }) ); - // OptimismPortal2Proxy contracts.push( InitializeableContract({ - name: "OptimismPortal2Proxy", - target: address(optimismPortal2), - initCalldata: abi.encodeCall(optimismPortal2.initialize, (systemConfig, anchorStateRegistry)) + name: "OptimismPortal2Proxy", target: address(optimismPortal2), initCalldata: initCalldata }) ); - // SystemConfigImpl + initCalldata = abi.encodeCall( + systemConfig.initialize, + ( + address(0xdead), + 0, + 0, + bytes32(0), + 1, + address(0), + IResourceMetering.ResourceConfig({ + maxResourceLimit: 1, + elasticityMultiplier: 1, + baseFeeMaxChangeDenominator: 2, + minimumBaseFee: 0, + systemTxMaxGas: 0, + maximumBaseFee: 0 + }), + address(0), + ISystemConfig.Addresses({ + l1CrossDomainMessenger: address(0), + l1ERC721Bridge: address(0), + l1StandardBridge: address(0), + optimismPortal: address(0), + optimismMintableERC20Factory: address(0), + delayedWETH: address(0) + }), + 0, + ISuperchainConfig(address(0)) + ) + ); contracts.push( InitializeableContract({ name: "SystemConfigImpl", target: EIP1967Helper.getImplementation(address(systemConfig)), - initCalldata: abi.encodeCall( - systemConfig.initialize, - ( - address(0xdead), - 0, - 0, - bytes32(0), - 1, - address(0), - IResourceMetering.ResourceConfig({ - maxResourceLimit: 1, - elasticityMultiplier: 1, - baseFeeMaxChangeDenominator: 2, - minimumBaseFee: 0, - systemTxMaxGas: 0, - maximumBaseFee: 0 - }), - address(0), - ISystemConfig.Addresses({ - l1CrossDomainMessenger: address(0), - l1ERC721Bridge: address(0), - l1StandardBridge: address(0), - optimismPortal: address(0), - optimismMintableERC20Factory: address(0), - delayedWETH: address(0) - }), - 0, - ISuperchainConfig(address(0)) - ) - ) + initCalldata: initCalldata }) ); - // SystemConfigProxy contracts.push( InitializeableContract({ - name: "SystemConfigProxy", - target: address(systemConfig), - initCalldata: abi.encodeCall( - systemConfig.initialize, - ( - address(0xdead), - 0, - 0, - bytes32(0), - 1, - address(0), - IResourceMetering.ResourceConfig({ - maxResourceLimit: 1, - elasticityMultiplier: 1, - baseFeeMaxChangeDenominator: 2, - minimumBaseFee: 0, - systemTxMaxGas: 0, - maximumBaseFee: 0 - }), - address(0), - ISystemConfig.Addresses({ - l1CrossDomainMessenger: address(0), - l1ERC721Bridge: address(0), - l1StandardBridge: address(0), - optimismPortal: address(0), - optimismMintableERC20Factory: address(0), - delayedWETH: address(0) - }), - 0, - ISuperchainConfig(address(0)) - ) - ) + name: "SystemConfigProxy", target: address(systemConfig), initCalldata: initCalldata }) ); - // L1StandardBridgeImpl + + initCalldata = abi.encodeCall(l1StandardBridge.initialize, (l1CrossDomainMessenger, systemConfig)); contracts.push( InitializeableContract({ name: "L1StandardBridgeImpl", target: EIP1967Helper.getImplementation(address(l1StandardBridge)), - initCalldata: abi.encodeCall(l1StandardBridge.initialize, (l1CrossDomainMessenger, systemConfig)) + initCalldata: initCalldata }) ); - // L1StandardBridgeProxy contracts.push( InitializeableContract({ - name: "L1StandardBridgeProxy", - target: address(l1StandardBridge), - initCalldata: abi.encodeCall(l1StandardBridge.initialize, (l1CrossDomainMessenger, systemConfig)) + name: "L1StandardBridgeProxy", target: address(l1StandardBridge), initCalldata: initCalldata }) ); - // L1ERC721BridgeImpl + + initCalldata = abi.encodeCall(l1ERC721Bridge.initialize, (l1CrossDomainMessenger, systemConfig)); contracts.push( InitializeableContract({ name: "L1ERC721BridgeImpl", target: EIP1967Helper.getImplementation(address(l1ERC721Bridge)), - initCalldata: abi.encodeCall(l1ERC721Bridge.initialize, (l1CrossDomainMessenger, systemConfig)) + initCalldata: initCalldata }) ); - // L1ERC721BridgeProxy contracts.push( InitializeableContract({ - name: "L1ERC721BridgeProxy", - target: address(l1ERC721Bridge), - initCalldata: abi.encodeCall(l1ERC721Bridge.initialize, (l1CrossDomainMessenger, systemConfig)) + name: "L1ERC721BridgeProxy", target: address(l1ERC721Bridge), initCalldata: initCalldata }) ); - // OptimismMintableERC20FactoryImpl + + initCalldata = abi.encodeCall(l1OptimismMintableERC20Factory.initialize, (address(l1StandardBridge))); contracts.push( InitializeableContract({ name: "OptimismMintableERC20FactoryImpl", target: EIP1967Helper.getImplementation(address(l1OptimismMintableERC20Factory)), - initCalldata: abi.encodeCall(l1OptimismMintableERC20Factory.initialize, (address(l1StandardBridge))) + initCalldata: initCalldata }) ); - // OptimismMintableERC20FactoryProxy contracts.push( InitializeableContract({ name: "OptimismMintableERC20FactoryProxy", target: address(l1OptimismMintableERC20Factory), - initCalldata: abi.encodeCall(l1OptimismMintableERC20Factory.initialize, (address(l1StandardBridge))) + initCalldata: initCalldata }) ); - // AnchorStateRegistry + + initCalldata = abi.encodeCall( + anchorStateRegistry.initialize, + ( + ISystemConfig(address(0)), + IDisputeGameFactory(address(0)), + Proposal({ root: Hash.wrap(bytes32(0)), l2SequenceNumber: 0 }), + GameType.wrap(uint32(deploy.cfg().respectedGameType())) + ) + ); contracts.push( InitializeableContract({ name: "AnchorStateRegistryImpl", target: EIP1967Helper.getImplementation(address(anchorStateRegistry)), - initCalldata: abi.encodeCall( - anchorStateRegistry.initialize, - ( - ISystemConfig(address(0)), - IDisputeGameFactory(address(0)), - Proposal({ root: Hash.wrap(bytes32(0)), l2SequenceNumber: 0 }), - GameType.wrap(uint32(deploy.cfg().respectedGameType())) - ) - ) + initCalldata: initCalldata }) ); - // AnchorStateRegistryProxy contracts.push( InitializeableContract({ - name: "AnchorStateRegistryProxy", - target: address(anchorStateRegistry), - initCalldata: abi.encodeCall( - anchorStateRegistry.initialize, - ( - ISystemConfig(address(0)), - IDisputeGameFactory(address(0)), - Proposal({ root: Hash.wrap(bytes32(0)), l2SequenceNumber: 0 }), - GameType.wrap(uint32(deploy.cfg().respectedGameType())) - ) - ) + name: "AnchorStateRegistryProxy", target: address(anchorStateRegistry), initCalldata: initCalldata }) ); // ETHLockbox is only deployed when interop is enabled if (address(ethLockbox) != address(0)) { - // ETHLockboxImpl + initCalldata = abi.encodeCall(ethLockbox.initialize, (ISystemConfig(address(0)), new IOptimismPortal2[](0))); contracts.push( InitializeableContract({ name: "ETHLockboxImpl", target: EIP1967Helper.getImplementation(address(ethLockbox)), - initCalldata: abi.encodeCall( - ethLockbox.initialize, (ISystemConfig(address(0)), new IOptimismPortal2[](0)) - ) + initCalldata: initCalldata }) ); - // ETHLockboxProxy contracts.push( InitializeableContract({ - name: "ETHLockboxProxy", - target: address(ethLockbox), - initCalldata: abi.encodeCall( - ethLockbox.initialize, (ISystemConfig(address(0)), new IOptimismPortal2[](0)) - ) + name: "ETHLockboxProxy", target: address(ethLockbox), initCalldata: initCalldata }) ); } @@ -302,7 +232,6 @@ contract Initializer_Test is CommonTest { // uint8, so it cannot be tested by this framework. It is excluded below. if (address(teeProverRegistry) != address(0)) { - // TEEProverRegistryImpl contracts.push( InitializeableContract({ name: "TEEProverRegistryImpl", @@ -395,11 +324,10 @@ contract Initializer_Test is CommonTest { // Attempt to re-initialize all contracts within the `contracts` array. for (uint256 i; i < contracts.length; i++) { InitializeableContract memory _contract = contracts[i]; - string memory deploymentName = _getRealContractName(_contract.name); // Assert that the contract is already initialized. assertTrue( - ForgeArtifacts.isInitialized({ _name: _removeSuffix(deploymentName), _address: _contract.target }), + ForgeArtifacts.isInitialized({ _name: _removeSuffix(_contract.name), _address: _contract.target }), "Initializable: contract is not initialized" ); @@ -415,20 +343,12 @@ contract Initializer_Test is CommonTest { /// @return matching_ True if the contract is in the `contracts` array, false otherwise. function _hasMatchingContract(string memory _name) internal view returns (bool matching_) { for (uint256 i; i < contracts.length; i++) { - if (LibString.eq(contracts[i].name, _getRealContractName(_name))) { - // return early + if (LibString.eq(contracts[i].name, _name)) { return true; } } } - /// @dev Returns the real name of the contract, including any nicknames. - /// @param _name The name of the contract. - /// @return real_ The real name of the contract. - function _getRealContractName(string memory _name) internal view returns (string memory real_) { - real_ = bytes(nicknames[_name]).length > 0 ? nicknames[_name] : _name; - } - /// @dev Extracts the revert string from returndata encoded in the form of `Error(string)`. function _extractErrorString(bytes memory _returnData) internal pure returns (string memory error_) { // The first 4 bytes of the return data should be the selector for `Error(string)`. If not, revert. diff --git a/test/vendor/InitializableOZv5.t.sol b/test/vendor/InitializableOZv5.t.sol index 7a4705e2..aac051e1 100644 --- a/test/vendor/InitializableOZv5.t.sol +++ b/test/vendor/InitializableOZv5.t.sol @@ -16,95 +16,55 @@ contract InitializerOZv5_Test is Test { /// keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.Initializable")) - 1)) & ~bytes32(uint256(0xff)) bytes32 private constant INITIALIZABLE_STORAGE = 0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00; - /// @notice Contains the address of an `Initializable` contract and the calldata - /// used to initialize it. - struct InitializeableContract { + struct InitializableContract { address target; bytes initCalldata; } - /// @notice Contains the addresses of the contracts to test as well as the calldata - /// used to initialize them. - InitializeableContract[] contracts; + InitializableContract[] private contracts; function setUp() public { - // Initialize the `contracts` array with the addresses of the contracts to test and the - // calldata used to initialize them + bytes memory constructorArgs = DeployUtils.encodeConstructor(abi.encodeCall(IFeeVault.__constructor__, ())); + bytes memory initCalldata = abi.encodeCall(IFeeVault.initialize, (address(0), 0, Types.WithdrawalNetwork.L1)); - // BaseFeeVault contracts.push( - InitializeableContract({ - target: address( - DeployUtils.create1({ - _name: "BaseFeeVault", - _args: DeployUtils.encodeConstructor(abi.encodeCall(IFeeVault.__constructor__, ())) - }) - ), - initCalldata: abi.encodeCall(IFeeVault.initialize, (address(0), 0, Types.WithdrawalNetwork.L1)) + InitializableContract({ + target: address(DeployUtils.create1({ _name: "BaseFeeVault", _args: constructorArgs })), + initCalldata: initCalldata }) ); - // OperatorFeeVault contracts.push( - InitializeableContract({ - target: address( - DeployUtils.create1({ - _name: "OperatorFeeVault", - _args: DeployUtils.encodeConstructor(abi.encodeCall(IFeeVault.__constructor__, ())) - }) - ), - initCalldata: abi.encodeCall(IFeeVault.initialize, (address(0), 0, Types.WithdrawalNetwork.L1)) + InitializableContract({ + target: address(DeployUtils.create1({ _name: "OperatorFeeVault", _args: constructorArgs })), + initCalldata: initCalldata }) ); - // SequencerFeeVault contracts.push( - InitializeableContract({ - target: address( - DeployUtils.create1({ - _name: "SequencerFeeVault", - _args: DeployUtils.encodeConstructor(abi.encodeCall(IFeeVault.__constructor__, ())) - }) - ), - initCalldata: abi.encodeCall(IFeeVault.initialize, (address(0), 0, Types.WithdrawalNetwork.L1)) + InitializableContract({ + target: address(DeployUtils.create1({ _name: "SequencerFeeVault", _args: constructorArgs })), + initCalldata: initCalldata }) ); - // L1FeeVault contracts.push( - InitializeableContract({ - target: address( - DeployUtils.create1({ - _name: "L1FeeVault", - _args: DeployUtils.encodeConstructor(abi.encodeCall(IFeeVault.__constructor__, ())) - }) - ), - initCalldata: abi.encodeCall(IFeeVault.initialize, (address(0), 0, Types.WithdrawalNetwork.L1)) + InitializableContract({ + target: address(DeployUtils.create1({ _name: "L1FeeVault", _args: constructorArgs })), + initCalldata: initCalldata }) ); } - /// @notice Tests that: - /// 1. The `initialized` flag of each contract is properly set to `type(uint64).max`, - /// signifying that the contracts are initialized. - /// 2. The `initialize()` function of each contract cannot be called more than once. - /// 3. Returns the correct error when attempting to re-initialize a contract. + /// @notice Ensures OZ v5 initializers are disabled on deployed FeeVault contracts. function test_cannotReinitialize_succeeds() public { - // Attempt to re-initialize all contracts within the `contracts` array. for (uint256 i; i < contracts.length; i++) { - InitializeableContract memory _contract = contracts[i]; - uint256 size; - address target = _contract.target; - assembly { - size := extcodesize(target) - } + InitializableContract memory _contract = contracts[i]; - // Assert that the contract is already initialized. bytes32 slotVal = vm.load(_contract.target, INITIALIZABLE_STORAGE); uint64 initialized = uint64(uint256(slotVal)); assertEq(initialized, type(uint64).max); - // Then, attempt to re-initialize the contract. This should fail. (bool success, bytes memory returnData) = _contract.target.call(_contract.initCalldata); assertFalse(success); assertEq(bytes4(returnData), Initializable.InvalidInitialization.selector);