From 5a1c421f434072fb16beff018ab2d33053c98cea Mon Sep 17 00:00:00 2001 From: saucepoint Date: Wed, 31 Jul 2024 20:22:50 -0400 Subject: [PATCH 01/11] wip bubble up revert --- src/base/Multicall.sol | 8 ++++- src/interfaces/IMulticall.sol | 2 ++ .../PositionManager.multicall.t.sol | 31 +++++++++++++++++++ 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/base/Multicall.sol b/src/base/Multicall.sol index bd926766..9543b09a 100644 --- a/src/base/Multicall.sol +++ b/src/base/Multicall.sol @@ -1,11 +1,17 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.8.19; +import {CustomRevert} from "@uniswap/v4-core/src/libraries/CustomRevert.sol"; + import {IMulticall} from "../interfaces/IMulticall.sol"; +import "forge-std/console2.sol"; + /// @title Multicall /// @notice Enables calling multiple methods in a single call to the contract abstract contract Multicall is IMulticall { + using CustomRevert for bytes4; + /// @inheritdoc IMulticall function multicall(bytes[] calldata data) public payable override returns (bytes[] memory results) { results = new bytes[](data.length); @@ -20,7 +26,7 @@ abstract contract Multicall is IMulticall { } } // Next 5 lines from https://ethereum.stackexchange.com/a/83577 - if (result.length < 68) revert(); + if (result.length < 68) CallFailed.selector.bubbleUpAndRevertWith(); assembly { result := add(result, 0x04) } diff --git a/src/interfaces/IMulticall.sol b/src/interfaces/IMulticall.sol index dfa9db24..92c26ba5 100644 --- a/src/interfaces/IMulticall.sol +++ b/src/interfaces/IMulticall.sol @@ -4,9 +4,11 @@ pragma solidity ^0.8.19; /// @title Multicall interface /// @notice Enables calling multiple methods in a single call to the contract interface IMulticall { + error CallFailed(bytes revertReason); /// @notice Call multiple functions in the current contract and return the data from all of them if they all succeed /// @dev The `msg.value` should not be trusted for any method callable from multicall. /// @param data The encoded function data for each of the calls to make to this contract /// @return results The results from each of the calls passed in via data + function multicall(bytes[] calldata data) external payable returns (bytes[] memory results); } diff --git a/test/position-managers/PositionManager.multicall.t.sol b/test/position-managers/PositionManager.multicall.t.sol index 5dde4e17..5ff751c9 100644 --- a/test/position-managers/PositionManager.multicall.t.sol +++ b/test/position-managers/PositionManager.multicall.t.sol @@ -68,4 +68,35 @@ contract PositionManagerMulticallTest is Test, PosmTestSetup, LiquidityFuzzers { assertEq(result.amount0(), amountSpecified); assertGt(result.amount1(), 0); } + + function test_multicall_bubbleRevert() public { + // charlie will attempt to decrease liquidity without approval + // posm's NotApproved(charlie) should bubble up through Multicall + + PositionConfig memory config = PositionConfig({ + poolKey: key, + tickLower: TickMath.minUsableTick(key.tickSpacing), + tickUpper: TickMath.maxUsableTick(key.tickSpacing) + }); + uint256 tokenId = lpm.nextTokenId(); + mint(config, 100e18, address(this), ZERO_BYTES); + + Plan memory planner = Planner.init(); + planner.add(Actions.DECREASE_LIQUIDITY, abi.encode(tokenId, config, 100e18, ZERO_BYTES)); + bytes memory actions = planner.finalizeModifyLiquidity(config.poolKey); + + // Use multicall to decrease liquidity + bytes[] memory calls = new bytes[](1); + calls[0] = abi.encodeWithSelector(IPositionManager.modifyLiquidities.selector, actions, _deadline); + + address charlie = makeAddr("CHARLIE"); + vm.startPrank(charlie); + vm.expectRevert( + abi.encodeWithSelector( + IMulticall.CallFailed.selector, abi.encodeWithSelector(IPositionManager.NotApproved.selector, charlie) + ) + ); + lpm.multicall(calls); + vm.stopPrank(); + } } From 58d80e646618724518cf340f895fe38fea065642 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Wed, 31 Jul 2024 20:25:02 -0400 Subject: [PATCH 02/11] fix formatting --- src/interfaces/IMulticall.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/interfaces/IMulticall.sol b/src/interfaces/IMulticall.sol index 92c26ba5..1278c148 100644 --- a/src/interfaces/IMulticall.sol +++ b/src/interfaces/IMulticall.sol @@ -5,10 +5,10 @@ pragma solidity ^0.8.19; /// @notice Enables calling multiple methods in a single call to the contract interface IMulticall { error CallFailed(bytes revertReason); + /// @notice Call multiple functions in the current contract and return the data from all of them if they all succeed /// @dev The `msg.value` should not be trusted for any method callable from multicall. /// @param data The encoded function data for each of the calls to make to this contract /// @return results The results from each of the calls passed in via data - function multicall(bytes[] calldata data) external payable returns (bytes[] memory results); } From 80ae166b98031d44200157736a45ba196fa2726e Mon Sep 17 00:00:00 2001 From: saucepoint Date: Wed, 31 Jul 2024 20:34:55 -0400 Subject: [PATCH 03/11] simple bubble --- .forge-snapshots/PositionManager_burn_empty.snap | 2 +- .../PositionManager_burn_empty_native.snap | 2 +- .forge-snapshots/PositionManager_burn_nonEmpty.snap | 2 +- .../PositionManager_burn_nonEmpty_native.snap | 2 +- .forge-snapshots/PositionManager_collect.snap | 2 +- .forge-snapshots/PositionManager_collect_native.snap | 2 +- .../PositionManager_collect_sameRange.snap | 2 +- .../PositionManager_decreaseLiquidity.snap | 2 +- .../PositionManager_decreaseLiquidity_native.snap | 2 +- .../PositionManager_decrease_burnEmpty.snap | 2 +- .../PositionManager_decrease_burnEmpty_native.snap | 2 +- ...itionManager_decrease_sameRange_allLiquidity.snap | 2 +- .../PositionManager_increaseLiquidity_erc20.snap | 2 +- .../PositionManager_increaseLiquidity_native.snap | 2 +- ...ager_increase_autocompoundExactUnclaimedFees.snap | 2 +- ...anager_increase_autocompoundExcessFeesCredit.snap | 2 +- .forge-snapshots/PositionManager_mint.snap | 2 +- .forge-snapshots/PositionManager_mint_native.snap | 2 +- .../PositionManager_mint_nativeWithSweep.snap | 2 +- .../PositionManager_mint_onSameTickLower.snap | 2 +- .../PositionManager_mint_onSameTickUpper.snap | 2 +- .forge-snapshots/PositionManager_mint_sameRange.snap | 2 +- ...PositionManager_mint_settleWithBalance_sweep.snap | 2 +- ...sitionManager_mint_warmedPool_differentRange.snap | 2 +- .../PositionManager_multicall_initialize_mint.snap | 2 +- src/base/Multicall.sol | 12 ++---------- .../PositionManager.multicall.t.sol | 6 +----- 27 files changed, 28 insertions(+), 40 deletions(-) diff --git a/.forge-snapshots/PositionManager_burn_empty.snap b/.forge-snapshots/PositionManager_burn_empty.snap index eae5c224..5ce0ec84 100644 --- a/.forge-snapshots/PositionManager_burn_empty.snap +++ b/.forge-snapshots/PositionManager_burn_empty.snap @@ -1 +1 @@ -46761 \ No newline at end of file +46759 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_burn_empty_native.snap b/.forge-snapshots/PositionManager_burn_empty_native.snap index f4f42539..cfe43987 100644 --- a/.forge-snapshots/PositionManager_burn_empty_native.snap +++ b/.forge-snapshots/PositionManager_burn_empty_native.snap @@ -1 +1 @@ -46579 \ No newline at end of file +46576 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_burn_nonEmpty.snap b/.forge-snapshots/PositionManager_burn_nonEmpty.snap index 774f434a..5f818936 100644 --- a/.forge-snapshots/PositionManager_burn_nonEmpty.snap +++ b/.forge-snapshots/PositionManager_burn_nonEmpty.snap @@ -1 +1 @@ -129621 \ No newline at end of file +129619 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_burn_nonEmpty_native.snap b/.forge-snapshots/PositionManager_burn_nonEmpty_native.snap index ae82b06d..466150e8 100644 --- a/.forge-snapshots/PositionManager_burn_nonEmpty_native.snap +++ b/.forge-snapshots/PositionManager_burn_nonEmpty_native.snap @@ -1 +1 @@ -122543 \ No newline at end of file +122540 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_collect.snap b/.forge-snapshots/PositionManager_collect.snap index 5f10760f..67a4339f 100644 --- a/.forge-snapshots/PositionManager_collect.snap +++ b/.forge-snapshots/PositionManager_collect.snap @@ -1 +1 @@ -149718 \ No newline at end of file +149715 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_collect_native.snap b/.forge-snapshots/PositionManager_collect_native.snap index 7ca113a9..b4c32e05 100644 --- a/.forge-snapshots/PositionManager_collect_native.snap +++ b/.forge-snapshots/PositionManager_collect_native.snap @@ -1 +1 @@ -140870 \ No newline at end of file +140867 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_collect_sameRange.snap b/.forge-snapshots/PositionManager_collect_sameRange.snap index 5f10760f..67a4339f 100644 --- a/.forge-snapshots/PositionManager_collect_sameRange.snap +++ b/.forge-snapshots/PositionManager_collect_sameRange.snap @@ -1 +1 @@ -149718 \ No newline at end of file +149715 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_decreaseLiquidity.snap b/.forge-snapshots/PositionManager_decreaseLiquidity.snap index ad2cf446..0ab6c496 100644 --- a/.forge-snapshots/PositionManager_decreaseLiquidity.snap +++ b/.forge-snapshots/PositionManager_decreaseLiquidity.snap @@ -1 +1 @@ -115261 \ No newline at end of file +115258 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_decreaseLiquidity_native.snap b/.forge-snapshots/PositionManager_decreaseLiquidity_native.snap index 1671a2c9..5ff8c0be 100644 --- a/.forge-snapshots/PositionManager_decreaseLiquidity_native.snap +++ b/.forge-snapshots/PositionManager_decreaseLiquidity_native.snap @@ -1 +1 @@ -108171 \ No newline at end of file +108168 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_decrease_burnEmpty.snap b/.forge-snapshots/PositionManager_decrease_burnEmpty.snap index 0dd409e3..a9455135 100644 --- a/.forge-snapshots/PositionManager_decrease_burnEmpty.snap +++ b/.forge-snapshots/PositionManager_decrease_burnEmpty.snap @@ -1 +1 @@ -133487 \ No newline at end of file +133484 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_decrease_burnEmpty_native.snap b/.forge-snapshots/PositionManager_decrease_burnEmpty_native.snap index 432fc190..6bf82583 100644 --- a/.forge-snapshots/PositionManager_decrease_burnEmpty_native.snap +++ b/.forge-snapshots/PositionManager_decrease_burnEmpty_native.snap @@ -1 +1 @@ -126226 \ No newline at end of file +126224 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_decrease_sameRange_allLiquidity.snap b/.forge-snapshots/PositionManager_decrease_sameRange_allLiquidity.snap index 2546c8e2..a909bc49 100644 --- a/.forge-snapshots/PositionManager_decrease_sameRange_allLiquidity.snap +++ b/.forge-snapshots/PositionManager_decrease_sameRange_allLiquidity.snap @@ -1 +1 @@ -127977 \ No newline at end of file +127974 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_increaseLiquidity_erc20.snap b/.forge-snapshots/PositionManager_increaseLiquidity_erc20.snap index 9062ddd1..87211bf4 100644 --- a/.forge-snapshots/PositionManager_increaseLiquidity_erc20.snap +++ b/.forge-snapshots/PositionManager_increaseLiquidity_erc20.snap @@ -1 +1 @@ -151197 \ No newline at end of file +151194 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_increaseLiquidity_native.snap b/.forge-snapshots/PositionManager_increaseLiquidity_native.snap index 770caaf9..68742c96 100644 --- a/.forge-snapshots/PositionManager_increaseLiquidity_native.snap +++ b/.forge-snapshots/PositionManager_increaseLiquidity_native.snap @@ -1 +1 @@ -132997 \ No newline at end of file +132994 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_increase_autocompoundExactUnclaimedFees.snap b/.forge-snapshots/PositionManager_increase_autocompoundExactUnclaimedFees.snap index 25672fe1..d5c63238 100644 --- a/.forge-snapshots/PositionManager_increase_autocompoundExactUnclaimedFees.snap +++ b/.forge-snapshots/PositionManager_increase_autocompoundExactUnclaimedFees.snap @@ -1 +1 @@ -133924 \ No newline at end of file +133921 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_increase_autocompoundExcessFeesCredit.snap b/.forge-snapshots/PositionManager_increase_autocompoundExcessFeesCredit.snap index 5c8985e2..cbeec491 100644 --- a/.forge-snapshots/PositionManager_increase_autocompoundExcessFeesCredit.snap +++ b/.forge-snapshots/PositionManager_increase_autocompoundExcessFeesCredit.snap @@ -1 +1 @@ -170080 \ No newline at end of file +170077 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint.snap b/.forge-snapshots/PositionManager_mint.snap index cb4f1c61..dd46c8ff 100644 --- a/.forge-snapshots/PositionManager_mint.snap +++ b/.forge-snapshots/PositionManager_mint.snap @@ -1 +1 @@ -371232 \ No newline at end of file +371229 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_native.snap b/.forge-snapshots/PositionManager_mint_native.snap index 0beb1d33..4a8f735f 100644 --- a/.forge-snapshots/PositionManager_mint_native.snap +++ b/.forge-snapshots/PositionManager_mint_native.snap @@ -1 +1 @@ -335932 \ No newline at end of file +335929 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_nativeWithSweep.snap b/.forge-snapshots/PositionManager_mint_nativeWithSweep.snap index 54bd5bcf..b36f07dd 100644 --- a/.forge-snapshots/PositionManager_mint_nativeWithSweep.snap +++ b/.forge-snapshots/PositionManager_mint_nativeWithSweep.snap @@ -1 +1 @@ -344562 \ No newline at end of file +344559 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_onSameTickLower.snap b/.forge-snapshots/PositionManager_mint_onSameTickLower.snap index 9900e39e..9d75e3ff 100644 --- a/.forge-snapshots/PositionManager_mint_onSameTickLower.snap +++ b/.forge-snapshots/PositionManager_mint_onSameTickLower.snap @@ -1 +1 @@ -313914 \ No newline at end of file +313911 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_onSameTickUpper.snap b/.forge-snapshots/PositionManager_mint_onSameTickUpper.snap index 4738722e..ce78fdbd 100644 --- a/.forge-snapshots/PositionManager_mint_onSameTickUpper.snap +++ b/.forge-snapshots/PositionManager_mint_onSameTickUpper.snap @@ -1 +1 @@ -314556 \ No newline at end of file +314553 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_sameRange.snap b/.forge-snapshots/PositionManager_mint_sameRange.snap index 9742db4a..236f64dd 100644 --- a/.forge-snapshots/PositionManager_mint_sameRange.snap +++ b/.forge-snapshots/PositionManager_mint_sameRange.snap @@ -1 +1 @@ -240138 \ No newline at end of file +240135 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_settleWithBalance_sweep.snap b/.forge-snapshots/PositionManager_mint_settleWithBalance_sweep.snap index dc2c7bb5..b889b0b1 100644 --- a/.forge-snapshots/PositionManager_mint_settleWithBalance_sweep.snap +++ b/.forge-snapshots/PositionManager_mint_settleWithBalance_sweep.snap @@ -1 +1 @@ -369400 \ No newline at end of file +369397 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_warmedPool_differentRange.snap b/.forge-snapshots/PositionManager_mint_warmedPool_differentRange.snap index cbe27444..baf3a658 100644 --- a/.forge-snapshots/PositionManager_mint_warmedPool_differentRange.snap +++ b/.forge-snapshots/PositionManager_mint_warmedPool_differentRange.snap @@ -1 +1 @@ -319932 \ No newline at end of file +319929 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_multicall_initialize_mint.snap b/.forge-snapshots/PositionManager_multicall_initialize_mint.snap index dce5eb0f..aa95ab32 100644 --- a/.forge-snapshots/PositionManager_multicall_initialize_mint.snap +++ b/.forge-snapshots/PositionManager_multicall_initialize_mint.snap @@ -1 +1 @@ -415608 \ No newline at end of file +415605 \ No newline at end of file diff --git a/src/base/Multicall.sol b/src/base/Multicall.sol index 9543b09a..fd26b5de 100644 --- a/src/base/Multicall.sol +++ b/src/base/Multicall.sol @@ -19,18 +19,10 @@ abstract contract Multicall is IMulticall { (bool success, bytes memory result) = address(this).delegatecall(data[i]); if (!success) { - // handle custom errors - if (result.length == 4) { - assembly { - revert(add(result, 0x20), mload(result)) - } - } - // Next 5 lines from https://ethereum.stackexchange.com/a/83577 - if (result.length < 68) CallFailed.selector.bubbleUpAndRevertWith(); + // bubble up the revert reason assembly { - result := add(result, 0x04) + revert(add(result, 0x20), mload(result)) } - revert(abi.decode(result, (string))); } results[i] = result; diff --git a/test/position-managers/PositionManager.multicall.t.sol b/test/position-managers/PositionManager.multicall.t.sol index 5ff751c9..8db75968 100644 --- a/test/position-managers/PositionManager.multicall.t.sol +++ b/test/position-managers/PositionManager.multicall.t.sol @@ -91,11 +91,7 @@ contract PositionManagerMulticallTest is Test, PosmTestSetup, LiquidityFuzzers { address charlie = makeAddr("CHARLIE"); vm.startPrank(charlie); - vm.expectRevert( - abi.encodeWithSelector( - IMulticall.CallFailed.selector, abi.encodeWithSelector(IPositionManager.NotApproved.selector, charlie) - ) - ); + vm.expectRevert(abi.encodeWithSelector(IPositionManager.NotApproved.selector, charlie)); lpm.multicall(calls); vm.stopPrank(); } From 55ce624d8682da0e6459f92801c1cec6974578a5 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Wed, 31 Jul 2024 22:11:09 -0400 Subject: [PATCH 04/11] test different error types on multicall --- test/Multicall.t.sol | 34 +++++++++++++++++++++++++++++++--- test/mocks/MockMulticall.sol | 13 ++++++++++++- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/test/Multicall.t.sol b/test/Multicall.t.sol index 18cdd2a3..849b11e9 100644 --- a/test/Multicall.t.sol +++ b/test/Multicall.t.sol @@ -30,7 +30,7 @@ contract MulticallTest is Test { function test_multicall_firstRevert() public { bytes[] memory calls = new bytes[](2); calls[0] = - abi.encodeWithSelector(MockMulticall(multicall).functionThatRevertsWithError.selector, "First call failed"); + abi.encodeWithSelector(MockMulticall(multicall).functionThatRevertsWithString.selector, "First call failed"); calls[1] = abi.encodeWithSelector(MockMulticall(multicall).functionThatReturnsTuple.selector, 1, 2); vm.expectRevert("First call failed"); @@ -40,8 +40,9 @@ contract MulticallTest is Test { function test_multicall_secondRevert() public { bytes[] memory calls = new bytes[](2); calls[0] = abi.encodeWithSelector(MockMulticall(multicall).functionThatReturnsTuple.selector, 1, 2); - calls[1] = - abi.encodeWithSelector(MockMulticall(multicall).functionThatRevertsWithError.selector, "Second call failed"); + calls[1] = abi.encodeWithSelector( + MockMulticall(multicall).functionThatRevertsWithString.selector, "Second call failed" + ); vm.expectRevert("Second call failed"); multicall.multicall(calls); @@ -106,4 +107,31 @@ contract MulticallTest is Test { assertEq(multicall.msgValue(), 100); assertEq(multicall.msgValueDouble(), 200); } + + // revert bubbling + function test_multicall_bubbleRevert_string() public { + bytes[] memory calls = new bytes[](1); + calls[0] = + abi.encodeWithSelector(MockMulticall(multicall).functionThatRevertsWithString.selector, "errorString"); + + vm.expectRevert("errorString"); + multicall.multicall(calls); + } + + function test_multicall_bubbleRevert_simpleError() public { + bytes[] memory calls = new bytes[](1); + calls[0] = abi.encodeWithSelector(MockMulticall(multicall).functionThatRevertsWithSimpleError.selector); + + vm.expectRevert(MockMulticall.SimpleError.selector); + multicall.multicall(calls); + } + + function test_multicall_bubbleRevert_errorWithParams(uint256 a, uint256 b) public { + bytes[] memory calls = new bytes[](1); + calls[0] = + abi.encodeWithSelector(MockMulticall(multicall).functionThatRevertsWithErrorWithParams.selector, a, b); + + vm.expectRevert(abi.encodeWithSelector(MockMulticall.ErrorWithParams.selector, a, b)); + multicall.multicall(calls); + } } diff --git a/test/mocks/MockMulticall.sol b/test/mocks/MockMulticall.sol index 4b96d915..25cf0cad 100644 --- a/test/mocks/MockMulticall.sol +++ b/test/mocks/MockMulticall.sol @@ -4,6 +4,9 @@ pragma solidity ^0.8.20; import "../../src/base/Multicall.sol"; contract MockMulticall is Multicall { + error SimpleError(); + error ErrorWithParams(uint256 a, uint256 b); + struct Tuple { uint256 a; uint256 b; @@ -12,10 +15,18 @@ contract MockMulticall is Multicall { uint256 public msgValue; uint256 public msgValueDouble; - function functionThatRevertsWithError(string memory error) external pure { + function functionThatRevertsWithString(string memory error) external pure { revert(error); } + function functionThatRevertsWithSimpleError() external pure { + revert SimpleError(); + } + + function functionThatRevertsWithErrorWithParams(uint256 a, uint256 b) external pure { + revert ErrorWithParams(a, b); + } + function functionThatReturnsTuple(uint256 a, uint256 b) external pure returns (Tuple memory tuple) { tuple = Tuple({a: a, b: b}); } From 50390fa49c1f61b3244e72b7bfe3ee912c395c0f Mon Sep 17 00:00:00 2001 From: saucepoint Date: Thu, 1 Aug 2024 10:32:41 -0400 Subject: [PATCH 05/11] additional testing for external contract reverts --- test/Multicall.t.sol | 26 +++++++++++++++++++++++++- test/mocks/MockMulticall.sol | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/test/Multicall.t.sol b/test/Multicall.t.sol index 849b11e9..880dbf9c 100644 --- a/test/Multicall.t.sol +++ b/test/Multicall.t.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.20; import "forge-std/Test.sol"; -import {MockMulticall} from "./mocks/MockMulticall.sol"; +import {MockMulticall, RevertContract} from "./mocks/MockMulticall.sol"; contract MulticallTest is Test { MockMulticall multicall; @@ -134,4 +134,28 @@ contract MulticallTest is Test { vm.expectRevert(abi.encodeWithSelector(MockMulticall.ErrorWithParams.selector, a, b)); multicall.multicall(calls); } + + function test_multicall_bubbleRevert_externalRevertString() public { + bytes[] memory calls = new bytes[](2); + calls[0] = abi.encodeWithSelector(MockMulticall(multicall).externalRevertString.selector, "errorString"); + + vm.expectRevert("errorString"); + multicall.multicall(calls); + } + + function test_multicall_bubbleRevert_externalRevertSimple() public { + bytes[] memory calls = new bytes[](1); + calls[0] = abi.encodeWithSelector(MockMulticall(multicall).externalRevertError1.selector); + + vm.expectRevert(RevertContract.Error1.selector); + multicall.multicall(calls); + } + + function test_multicall_bubbleRevert_externalRevertWithParams(uint256 a, uint256 b) public { + bytes[] memory calls = new bytes[](1); + calls[0] = abi.encodeWithSelector(MockMulticall(multicall).externalRevertError2.selector, a, b); + + vm.expectRevert(abi.encodeWithSelector(RevertContract.Error2.selector, a, b)); + multicall.multicall(calls); + } } diff --git a/test/mocks/MockMulticall.sol b/test/mocks/MockMulticall.sol index 25cf0cad..d8d108d8 100644 --- a/test/mocks/MockMulticall.sol +++ b/test/mocks/MockMulticall.sol @@ -3,6 +3,24 @@ pragma solidity ^0.8.20; import "../../src/base/Multicall.sol"; +/// @dev If MockMulticall is to PositionManager, then RevertContract is to PoolManager +contract RevertContract { + error Error1(); + error Error2(uint256 a, uint256 b); + + function revertWithString(string memory error) external pure { + revert(error); + } + + function revertWithError1() external pure { + revert Error1(); + } + + function revertWithError2(uint256 a, uint256 b) external pure { + revert Error2(a, b); + } +} + contract MockMulticall is Multicall { error SimpleError(); error ErrorWithParams(uint256 a, uint256 b); @@ -15,6 +33,8 @@ contract MockMulticall is Multicall { uint256 public msgValue; uint256 public msgValueDouble; + RevertContract public revertContract = new RevertContract(); + function functionThatRevertsWithString(string memory error) external pure { revert(error); } @@ -42,4 +62,16 @@ contract MockMulticall is Multicall { function returnSender() external view returns (address) { return msg.sender; } + + function externalRevertString(string memory error) external view { + revertContract.revertWithString(error); + } + + function externalRevertError1() external view { + revertContract.revertWithError1(); + } + + function externalRevertError2(uint256 a, uint256 b) external view { + revertContract.revertWithError2(a, b); + } } From eb808644096367531e10674e778a3726c1f7300c Mon Sep 17 00:00:00 2001 From: saucepoint Date: Fri, 2 Aug 2024 11:07:57 -0400 Subject: [PATCH 06/11] example core revert bubbling --- .../PositionManager.multicall.t.sol | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/test/position-managers/PositionManager.multicall.t.sol b/test/position-managers/PositionManager.multicall.t.sol index aa2e4793..30ef186d 100644 --- a/test/position-managers/PositionManager.multicall.t.sol +++ b/test/position-managers/PositionManager.multicall.t.sol @@ -15,6 +15,7 @@ import {FixedPointMathLib} from "solmate/src/utils/FixedPointMathLib.sol"; import {IERC20} from "forge-std/interfaces/IERC20.sol"; import {IPositionManager} from "../../src/interfaces/IPositionManager.sol"; +import {PoolInitializer} from "../../src/base/PoolInitializer.sol"; import {Actions} from "../../src/libraries/Actions.sol"; import {PositionManager} from "../../src/PositionManager.sol"; import {PositionConfig} from "../../src/libraries/PositionConfig.sol"; @@ -101,4 +102,52 @@ contract PositionManagerMulticallTest is Test, PosmTestSetup, LiquidityFuzzers { lpm.multicall(calls); vm.stopPrank(); } + + function test_multicall_bubbleRevert_core() public { + // decrease liquidity but forget to close + // core's CurrencyNotSettled should bubble up through Multicall + + PositionConfig memory config = PositionConfig({ + poolKey: key, + tickLower: TickMath.minUsableTick(key.tickSpacing), + tickUpper: TickMath.maxUsableTick(key.tickSpacing) + }); + uint256 tokenId = lpm.nextTokenId(); + mint(config, 100e18, address(this), ZERO_BYTES); + + // do not close deltas to throw CurrencyNotSettled in core + Plan memory planner = Planner.init(); + planner.add( + Actions.DECREASE_LIQUIDITY, + abi.encode(tokenId, config, 100e18, MIN_SLIPPAGE_DECREASE, MIN_SLIPPAGE_DECREASE, ZERO_BYTES) + ); + bytes memory actions = planner.encode(); + + // Use multicall to decrease liquidity + bytes[] memory calls = new bytes[](1); + calls[0] = abi.encodeWithSelector(IPositionManager.modifyLiquidities.selector, actions, _deadline); + + vm.expectRevert(IPoolManager.CurrencyNotSettled.selector); + lpm.multicall(calls); + } + + function test_multicall_bubbleRevert_core_args() public { + // create a pool where tickSpacing is negative + // core's TickSpacingTooSmall(int24) should bubble up through Multicall + int24 tickSpacing = -10; + key = PoolKey({ + currency0: currency0, + currency1: currency1, + fee: 0, + tickSpacing: tickSpacing, + hooks: IHooks(address(0)) + }); + + // Use multicall to initialize a pool + bytes[] memory calls = new bytes[](1); + calls[0] = abi.encodeWithSelector(PoolInitializer.initializePool.selector, key, SQRT_PRICE_1_1, ZERO_BYTES); + + vm.expectRevert(abi.encodeWithSelector(IPoolManager.TickSpacingTooSmall.selector, tickSpacing)); + lpm.multicall(calls); + } } From 77268b90969934976f74d1b53af804c1769c24a0 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Fri, 2 Aug 2024 12:24:43 -0400 Subject: [PATCH 07/11] testing for different lengths --- test/Multicall.t.sol | 67 ++++++++++++++++++++++++++++++++++++ test/mocks/MockMulticall.sol | 21 +++++++++++ 2 files changed, 88 insertions(+) diff --git a/test/Multicall.t.sol b/test/Multicall.t.sol index 880dbf9c..c78efb0a 100644 --- a/test/Multicall.t.sol +++ b/test/Multicall.t.sol @@ -143,6 +143,73 @@ contract MulticallTest is Test { multicall.multicall(calls); } + function test_multicall_bubbleRevert_4bytes() public { + bytes[] memory calls = new bytes[](1); + calls[0] = abi.encodeWithSelector(MockMulticall(multicall).revertWith4Bytes.selector); + + vm.expectRevert(MockMulticall.Error4Bytes.selector); + multicall.multicall(calls); + + try multicall.revertWith4Bytes() {} catch (bytes memory reason) { + assertEq(reason.length, 4); + } + } + + function test_multicall_bubbleRevert_36bytes() public { + uint8 num = 10; + bytes[] memory calls = new bytes[](1); + calls[0] = abi.encodeWithSelector(MockMulticall(multicall).revertWith36Bytes.selector, num); + + vm.expectRevert(abi.encodeWithSelector(MockMulticall.Error36Bytes.selector, num)); + multicall.multicall(calls); + + try multicall.revertWith36Bytes(num) {} catch (bytes memory reason) { + assertEq(reason.length, 36); + } + } + + function test_multicall_bubbleRevert_68bytes() public { + uint256 a = 10; + uint256 b = 20; + bytes[] memory calls = new bytes[](1); + calls[0] = abi.encodeWithSelector(MockMulticall(multicall).revertWith68Bytes.selector, a, b); + + vm.expectRevert(abi.encodeWithSelector(MockMulticall.Error68Bytes.selector, a, b)); + multicall.multicall(calls); + + try multicall.revertWith68Bytes(a, b) {} catch (bytes memory reason) { + assertEq(reason.length, 68); + } + } + + function test_fuzz_multicall_bubbleRevert_arbitraryBytes(uint16 length) public { + length = 1; + // length = uint16(bound(length, 0, 4096)); + bytes memory data = new bytes(length); + for (uint256 i = 0; i < data.length; i++) { + data[i] = bytes1(uint8(i)); + } + + bytes[] memory calls = new bytes[](1); + calls[0] = abi.encodeWithSelector(MockMulticall(multicall).revertWithBytes.selector, data); + + vm.expectRevert(abi.encodeWithSelector(MockMulticall.ErrorBytes.selector, data)); + multicall.multicall(calls); + + try multicall.revertWithBytes(data) {} catch (bytes memory reason) { + // errors with 0 bytes are by default 64 bytes of data (length & pointer?) + 4 bytes of selector + if (length == 0) { + assertEq(reason.length, 68); + } + else { + uint256 expectedLength = 64 + 4; // default length + selector + // for every 32 bytes of data, + expectedLength += (((data.length - 1) / 32) + 1) * 32; + assertEq(reason.length, expectedLength); + } + } + } + function test_multicall_bubbleRevert_externalRevertSimple() public { bytes[] memory calls = new bytes[](1); calls[0] = abi.encodeWithSelector(MockMulticall(multicall).externalRevertError1.selector); diff --git a/test/mocks/MockMulticall.sol b/test/mocks/MockMulticall.sol index d8d108d8..cb17336b 100644 --- a/test/mocks/MockMulticall.sol +++ b/test/mocks/MockMulticall.sol @@ -24,6 +24,11 @@ contract RevertContract { contract MockMulticall is Multicall { error SimpleError(); error ErrorWithParams(uint256 a, uint256 b); + + error Error4Bytes(); // 4 bytes of selector + error Error36Bytes(uint8 a); // 32 bytes + 4 bytes of selector + error Error68Bytes(uint256 a, uint256 b); // 64 bytes + 4 bytes of selector + error ErrorBytes(bytes data); // arbitrary byte length struct Tuple { uint256 a; @@ -74,4 +79,20 @@ contract MockMulticall is Multicall { function externalRevertError2(uint256 a, uint256 b) external view { revertContract.revertWithError2(a, b); } + + function revertWith4Bytes() external pure { + revert Error4Bytes(); + } + + function revertWith36Bytes(uint8 a) external pure { + revert Error36Bytes(a); + } + + function revertWith68Bytes(uint256 a, uint256 b) external pure { + revert Error68Bytes(a, b); + } + + function revertWithBytes(bytes memory data) external pure { + revert ErrorBytes(data); + } } From c678194db7feb93fada64405aa4d08238526a688 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Fri, 2 Aug 2024 12:29:35 -0400 Subject: [PATCH 08/11] cleanup --- test/Multicall.t.sol | 70 ++++++++++++++++-------------------- test/mocks/MockMulticall.sol | 11 ------ 2 files changed, 30 insertions(+), 51 deletions(-) diff --git a/test/Multicall.t.sol b/test/Multicall.t.sol index c78efb0a..86ed8d3d 100644 --- a/test/Multicall.t.sol +++ b/test/Multicall.t.sol @@ -118,73 +118,53 @@ contract MulticallTest is Test { multicall.multicall(calls); } - function test_multicall_bubbleRevert_simpleError() public { - bytes[] memory calls = new bytes[](1); - calls[0] = abi.encodeWithSelector(MockMulticall(multicall).functionThatRevertsWithSimpleError.selector); - - vm.expectRevert(MockMulticall.SimpleError.selector); - multicall.multicall(calls); - } - - function test_multicall_bubbleRevert_errorWithParams(uint256 a, uint256 b) public { - bytes[] memory calls = new bytes[](1); - calls[0] = - abi.encodeWithSelector(MockMulticall(multicall).functionThatRevertsWithErrorWithParams.selector, a, b); - - vm.expectRevert(abi.encodeWithSelector(MockMulticall.ErrorWithParams.selector, a, b)); - multicall.multicall(calls); - } - - function test_multicall_bubbleRevert_externalRevertString() public { - bytes[] memory calls = new bytes[](2); - calls[0] = abi.encodeWithSelector(MockMulticall(multicall).externalRevertString.selector, "errorString"); - - vm.expectRevert("errorString"); - multicall.multicall(calls); - } - function test_multicall_bubbleRevert_4bytes() public { bytes[] memory calls = new bytes[](1); calls[0] = abi.encodeWithSelector(MockMulticall(multicall).revertWith4Bytes.selector); + // revert is caught vm.expectRevert(MockMulticall.Error4Bytes.selector); multicall.multicall(calls); - try multicall.revertWith4Bytes() {} catch (bytes memory reason) { + // confirm expected length of the revert + try multicall.revertWith4Bytes() {} + catch (bytes memory reason) { assertEq(reason.length, 4); } } - function test_multicall_bubbleRevert_36bytes() public { - uint8 num = 10; + function test_fuzz_multicall_bubbleRevert_36bytes(uint8 num) public { bytes[] memory calls = new bytes[](1); calls[0] = abi.encodeWithSelector(MockMulticall(multicall).revertWith36Bytes.selector, num); + // revert is caught vm.expectRevert(abi.encodeWithSelector(MockMulticall.Error36Bytes.selector, num)); multicall.multicall(calls); - try multicall.revertWith36Bytes(num) {} catch (bytes memory reason) { + // confirm expected length of the revert + try multicall.revertWith36Bytes(num) {} + catch (bytes memory reason) { assertEq(reason.length, 36); } } - function test_multicall_bubbleRevert_68bytes() public { - uint256 a = 10; - uint256 b = 20; + function test_fuzz_multicall_bubbleRevert_68bytes(uint256 a, uint256 b) public { bytes[] memory calls = new bytes[](1); calls[0] = abi.encodeWithSelector(MockMulticall(multicall).revertWith68Bytes.selector, a, b); + // revert is caught vm.expectRevert(abi.encodeWithSelector(MockMulticall.Error68Bytes.selector, a, b)); multicall.multicall(calls); - try multicall.revertWith68Bytes(a, b) {} catch (bytes memory reason) { + // confirm expected length of the revert + try multicall.revertWith68Bytes(a, b) {} + catch (bytes memory reason) { assertEq(reason.length, 68); } } function test_fuzz_multicall_bubbleRevert_arbitraryBytes(uint16 length) public { - length = 1; - // length = uint16(bound(length, 0, 4096)); + length = uint16(bound(length, 0, 4096)); bytes memory data = new bytes(length); for (uint256 i = 0; i < data.length; i++) { data[i] = bytes1(uint8(i)); @@ -193,23 +173,33 @@ contract MulticallTest is Test { bytes[] memory calls = new bytes[](1); calls[0] = abi.encodeWithSelector(MockMulticall(multicall).revertWithBytes.selector, data); + // revert is caught vm.expectRevert(abi.encodeWithSelector(MockMulticall.ErrorBytes.selector, data)); multicall.multicall(calls); - try multicall.revertWithBytes(data) {} catch (bytes memory reason) { + // confirm expected length of the revert + try multicall.revertWithBytes(data) {} + catch (bytes memory reason) { // errors with 0 bytes are by default 64 bytes of data (length & pointer?) + 4 bytes of selector - if (length == 0) { + if (length == 0) { assertEq(reason.length, 68); - } - else { + } else { uint256 expectedLength = 64 + 4; // default length + selector - // for every 32 bytes of data, + // for every 32 bytes of data, expectedLength += (((data.length - 1) / 32) + 1) * 32; assertEq(reason.length, expectedLength); } } } + function test_multicall_bubbleRevert_externalRevertString() public { + bytes[] memory calls = new bytes[](2); + calls[0] = abi.encodeWithSelector(MockMulticall(multicall).externalRevertString.selector, "errorString"); + + vm.expectRevert("errorString"); + multicall.multicall(calls); + } + function test_multicall_bubbleRevert_externalRevertSimple() public { bytes[] memory calls = new bytes[](1); calls[0] = abi.encodeWithSelector(MockMulticall(multicall).externalRevertError1.selector); diff --git a/test/mocks/MockMulticall.sol b/test/mocks/MockMulticall.sol index cb17336b..1a936deb 100644 --- a/test/mocks/MockMulticall.sol +++ b/test/mocks/MockMulticall.sol @@ -22,9 +22,6 @@ contract RevertContract { } contract MockMulticall is Multicall { - error SimpleError(); - error ErrorWithParams(uint256 a, uint256 b); - error Error4Bytes(); // 4 bytes of selector error Error36Bytes(uint8 a); // 32 bytes + 4 bytes of selector error Error68Bytes(uint256 a, uint256 b); // 64 bytes + 4 bytes of selector @@ -44,14 +41,6 @@ contract MockMulticall is Multicall { revert(error); } - function functionThatRevertsWithSimpleError() external pure { - revert SimpleError(); - } - - function functionThatRevertsWithErrorWithParams(uint256 a, uint256 b) external pure { - revert ErrorWithParams(a, b); - } - function functionThatReturnsTuple(uint256 a, uint256 b) external pure returns (Tuple memory tuple) { tuple = Tuple({a: a, b: b}); } From 7e886da64e118bde6b4a2d4fa5e211244a3aafca Mon Sep 17 00:00:00 2001 From: saucepoint Date: Fri, 2 Aug 2024 16:58:43 -0400 Subject: [PATCH 09/11] cleanup unused imports --- src/base/Multicall.sol | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/base/Multicall.sol b/src/base/Multicall.sol index fd26b5de..e48fa69f 100644 --- a/src/base/Multicall.sol +++ b/src/base/Multicall.sol @@ -1,16 +1,11 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.8.19; -import {CustomRevert} from "@uniswap/v4-core/src/libraries/CustomRevert.sol"; - import {IMulticall} from "../interfaces/IMulticall.sol"; -import "forge-std/console2.sol"; - /// @title Multicall /// @notice Enables calling multiple methods in a single call to the contract abstract contract Multicall is IMulticall { - using CustomRevert for bytes4; /// @inheritdoc IMulticall function multicall(bytes[] calldata data) public payable override returns (bytes[] memory results) { From 1933500cf0939e7a77bfd4e2e6a53cbde1c637d7 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Fri, 2 Aug 2024 21:26:49 -0400 Subject: [PATCH 10/11] delete stale gas --- .forge-snapshots/PositionManager_burn_nonEmpty.snap | 1 - .forge-snapshots/PositionManager_burn_nonEmpty_native.snap | 1 - .forge-snapshots/PositionManager_collect.snap | 1 - .forge-snapshots/PositionManager_decreaseLiquidity.snap | 1 - .../PositionManager_increaseLiquidity_erc20.snap | 5 ----- .forge-snapshots/PositionManager_mint.snap | 5 ----- .forge-snapshots/PositionManager_mint_nativeWithSweep.snap | 5 ----- 7 files changed, 19 deletions(-) delete mode 100644 .forge-snapshots/PositionManager_burn_nonEmpty.snap delete mode 100644 .forge-snapshots/PositionManager_burn_nonEmpty_native.snap delete mode 100644 .forge-snapshots/PositionManager_collect.snap delete mode 100644 .forge-snapshots/PositionManager_decreaseLiquidity.snap delete mode 100644 .forge-snapshots/PositionManager_increaseLiquidity_erc20.snap delete mode 100644 .forge-snapshots/PositionManager_mint.snap delete mode 100644 .forge-snapshots/PositionManager_mint_nativeWithSweep.snap diff --git a/.forge-snapshots/PositionManager_burn_nonEmpty.snap b/.forge-snapshots/PositionManager_burn_nonEmpty.snap deleted file mode 100644 index a04a302d..00000000 --- a/.forge-snapshots/PositionManager_burn_nonEmpty.snap +++ /dev/null @@ -1 +0,0 @@ -129849 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_burn_nonEmpty_native.snap b/.forge-snapshots/PositionManager_burn_nonEmpty_native.snap deleted file mode 100644 index 63bc6a64..00000000 --- a/.forge-snapshots/PositionManager_burn_nonEmpty_native.snap +++ /dev/null @@ -1 +0,0 @@ -122771 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_collect.snap b/.forge-snapshots/PositionManager_collect.snap deleted file mode 100644 index ad973caf..00000000 --- a/.forge-snapshots/PositionManager_collect.snap +++ /dev/null @@ -1 +0,0 @@ -149981 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_decreaseLiquidity.snap b/.forge-snapshots/PositionManager_decreaseLiquidity.snap deleted file mode 100644 index 887039a0..00000000 --- a/.forge-snapshots/PositionManager_decreaseLiquidity.snap +++ /dev/null @@ -1 +0,0 @@ -115524 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_increaseLiquidity_erc20.snap b/.forge-snapshots/PositionManager_increaseLiquidity_erc20.snap deleted file mode 100644 index 2d22e2cc..00000000 --- a/.forge-snapshots/PositionManager_increaseLiquidity_erc20.snap +++ /dev/null @@ -1,5 +0,0 @@ -<<<<<<< HEAD -152097 -======= -152144 ->>>>>>> main diff --git a/.forge-snapshots/PositionManager_mint.snap b/.forge-snapshots/PositionManager_mint.snap deleted file mode 100644 index c5612b59..00000000 --- a/.forge-snapshots/PositionManager_mint.snap +++ /dev/null @@ -1,5 +0,0 @@ -<<<<<<< HEAD -371960 -======= -372007 ->>>>>>> main diff --git a/.forge-snapshots/PositionManager_mint_nativeWithSweep.snap b/.forge-snapshots/PositionManager_mint_nativeWithSweep.snap deleted file mode 100644 index 04ac3576..00000000 --- a/.forge-snapshots/PositionManager_mint_nativeWithSweep.snap +++ /dev/null @@ -1,5 +0,0 @@ -<<<<<<< HEAD -345143 -======= -345190 ->>>>>>> main From 2141221c19741dec84db047b0bd05f842362bbd5 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Fri, 2 Aug 2024 21:32:13 -0400 Subject: [PATCH 11/11] minor nits --- src/interfaces/IMulticall.sol | 2 -- test/Multicall.t.sol | 2 +- test/position-managers/PositionManager.multicall.t.sol | 4 ++-- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/interfaces/IMulticall.sol b/src/interfaces/IMulticall.sol index 1278c148..dfa9db24 100644 --- a/src/interfaces/IMulticall.sol +++ b/src/interfaces/IMulticall.sol @@ -4,8 +4,6 @@ pragma solidity ^0.8.19; /// @title Multicall interface /// @notice Enables calling multiple methods in a single call to the contract interface IMulticall { - error CallFailed(bytes revertReason); - /// @notice Call multiple functions in the current contract and return the data from all of them if they all succeed /// @dev The `msg.value` should not be trusted for any method callable from multicall. /// @param data The encoded function data for each of the calls to make to this contract diff --git a/test/Multicall.t.sol b/test/Multicall.t.sol index 86ed8d3d..8591c271 100644 --- a/test/Multicall.t.sol +++ b/test/Multicall.t.sol @@ -185,7 +185,7 @@ contract MulticallTest is Test { assertEq(reason.length, 68); } else { uint256 expectedLength = 64 + 4; // default length + selector - // for every 32 bytes of data, + // 32 bytes added to the reason for each 32 bytes of data expectedLength += (((data.length - 1) / 32) + 1) * 32; assertEq(reason.length, expectedLength); } diff --git a/test/position-managers/PositionManager.multicall.t.sol b/test/position-managers/PositionManager.multicall.t.sol index 6cc0204e..8c030164 100644 --- a/test/position-managers/PositionManager.multicall.t.sol +++ b/test/position-managers/PositionManager.multicall.t.sol @@ -167,9 +167,9 @@ contract PositionManagerMulticallTest is Test, Permit2SignatureHelpers, PosmTest lpm.multicall(calls); } + // create a pool where tickSpacing is negative + // core's TickSpacingTooSmall(int24) should bubble up through Multicall function test_multicall_bubbleRevert_core_args() public { - // create a pool where tickSpacing is negative - // core's TickSpacingTooSmall(int24) should bubble up through Multicall int24 tickSpacing = -10; key = PoolKey({ currency0: currency0,