diff --git a/.forge-snapshots/Payments_swap_settleFromCaller_takeAllToMsgSender.snap b/.forge-snapshots/Payments_swap_settleFromCaller_takeAllToMsgSender.snap index 2cd533ee..2fb484c4 100644 --- a/.forge-snapshots/Payments_swap_settleFromCaller_takeAllToMsgSender.snap +++ b/.forge-snapshots/Payments_swap_settleFromCaller_takeAllToMsgSender.snap @@ -1 +1 @@ -129854 \ No newline at end of file +129642 \ No newline at end of file diff --git a/.forge-snapshots/Payments_swap_settleFromCaller_takeAllToSpecifiedAddress.snap b/.forge-snapshots/Payments_swap_settleFromCaller_takeAllToSpecifiedAddress.snap index 89faf94c..ff128171 100644 --- a/.forge-snapshots/Payments_swap_settleFromCaller_takeAllToSpecifiedAddress.snap +++ b/.forge-snapshots/Payments_swap_settleFromCaller_takeAllToSpecifiedAddress.snap @@ -1 +1 @@ -131905 \ No newline at end of file +131705 \ No newline at end of file diff --git a/.forge-snapshots/Payments_swap_settleWithBalance_takeAllToMsgSender.snap b/.forge-snapshots/Payments_swap_settleWithBalance_takeAllToMsgSender.snap index 55ac6b3a..7703a32a 100644 --- a/.forge-snapshots/Payments_swap_settleWithBalance_takeAllToMsgSender.snap +++ b/.forge-snapshots/Payments_swap_settleWithBalance_takeAllToMsgSender.snap @@ -1 +1 @@ -124110 \ No newline at end of file +123910 \ No newline at end of file diff --git a/.forge-snapshots/Payments_swap_settleWithBalance_takeAllToSpecifiedAddress.snap b/.forge-snapshots/Payments_swap_settleWithBalance_takeAllToSpecifiedAddress.snap index 00e673a8..0830d929 100644 --- a/.forge-snapshots/Payments_swap_settleWithBalance_takeAllToSpecifiedAddress.snap +++ b/.forge-snapshots/Payments_swap_settleWithBalance_takeAllToSpecifiedAddress.snap @@ -1 +1 @@ -124252 \ No newline at end of file +124052 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_burn_empty.snap b/.forge-snapshots/PositionManager_burn_empty.snap index 0e85ca4a..949dd08a 100644 --- a/.forge-snapshots/PositionManager_burn_empty.snap +++ b/.forge-snapshots/PositionManager_burn_empty.snap @@ -1 +1 @@ -50446 \ No newline at end of file +50481 \ 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 0e85ca4a..949dd08a 100644 --- a/.forge-snapshots/PositionManager_burn_empty_native.snap +++ b/.forge-snapshots/PositionManager_burn_empty_native.snap @@ -1 +1 @@ -50446 \ No newline at end of file +50481 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_burn_nonEmpty_native_withClose.snap b/.forge-snapshots/PositionManager_burn_nonEmpty_native_withClose.snap index 9e62ab4d..c3bc574e 100644 --- a/.forge-snapshots/PositionManager_burn_nonEmpty_native_withClose.snap +++ b/.forge-snapshots/PositionManager_burn_nonEmpty_native_withClose.snap @@ -1 +1 @@ -125584 \ No newline at end of file +125659 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_burn_nonEmpty_native_withTakePair.snap b/.forge-snapshots/PositionManager_burn_nonEmpty_native_withTakePair.snap index 33aa7739..e3359234 100644 --- a/.forge-snapshots/PositionManager_burn_nonEmpty_native_withTakePair.snap +++ b/.forge-snapshots/PositionManager_burn_nonEmpty_native_withTakePair.snap @@ -1 +1 @@ -125031 \ No newline at end of file +125141 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_burn_nonEmpty_withClose.snap b/.forge-snapshots/PositionManager_burn_nonEmpty_withClose.snap index 6a3bb39f..54b474eb 100644 --- a/.forge-snapshots/PositionManager_burn_nonEmpty_withClose.snap +++ b/.forge-snapshots/PositionManager_burn_nonEmpty_withClose.snap @@ -1 +1 @@ -132446 \ No newline at end of file +132521 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_burn_nonEmpty_withTakePair.snap b/.forge-snapshots/PositionManager_burn_nonEmpty_withTakePair.snap index 62abe7a1..8bbf4b07 100644 --- a/.forge-snapshots/PositionManager_burn_nonEmpty_withTakePair.snap +++ b/.forge-snapshots/PositionManager_burn_nonEmpty_withTakePair.snap @@ -1 +1 @@ -131893 \ No newline at end of file +132004 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_collect_native.snap b/.forge-snapshots/PositionManager_collect_native.snap index 83e0e3d6..85355e35 100644 --- a/.forge-snapshots/PositionManager_collect_native.snap +++ b/.forge-snapshots/PositionManager_collect_native.snap @@ -1 +1 @@ -146294 \ No newline at end of file +146388 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_collect_sameRange.snap b/.forge-snapshots/PositionManager_collect_sameRange.snap index ab27c01b..1a7e51e0 100644 --- a/.forge-snapshots/PositionManager_collect_sameRange.snap +++ b/.forge-snapshots/PositionManager_collect_sameRange.snap @@ -1 +1 @@ -154872 \ No newline at end of file +154966 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_collect_withClose.snap b/.forge-snapshots/PositionManager_collect_withClose.snap index ab27c01b..1a7e51e0 100644 --- a/.forge-snapshots/PositionManager_collect_withClose.snap +++ b/.forge-snapshots/PositionManager_collect_withClose.snap @@ -1 +1 @@ -154872 \ No newline at end of file +154966 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_collect_withTakePair.snap b/.forge-snapshots/PositionManager_collect_withTakePair.snap index 47b66e44..e1639e55 100644 --- a/.forge-snapshots/PositionManager_collect_withTakePair.snap +++ b/.forge-snapshots/PositionManager_collect_withTakePair.snap @@ -1 +1 @@ -154193 \ No newline at end of file +154331 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_decreaseLiquidity_native.snap b/.forge-snapshots/PositionManager_decreaseLiquidity_native.snap index e13e87a3..4db04993 100644 --- a/.forge-snapshots/PositionManager_decreaseLiquidity_native.snap +++ b/.forge-snapshots/PositionManager_decreaseLiquidity_native.snap @@ -1 +1 @@ -111980 \ No newline at end of file +112056 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_decreaseLiquidity_withClose.snap b/.forge-snapshots/PositionManager_decreaseLiquidity_withClose.snap index f5f1f8b2..0d683313 100644 --- a/.forge-snapshots/PositionManager_decreaseLiquidity_withClose.snap +++ b/.forge-snapshots/PositionManager_decreaseLiquidity_withClose.snap @@ -1 +1 @@ -119753 \ No newline at end of file +119847 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_decreaseLiquidity_withTakePair.snap b/.forge-snapshots/PositionManager_decreaseLiquidity_withTakePair.snap index e4b3cfde..8e8a2c64 100644 --- a/.forge-snapshots/PositionManager_decreaseLiquidity_withTakePair.snap +++ b/.forge-snapshots/PositionManager_decreaseLiquidity_withTakePair.snap @@ -1 +1 @@ -119074 \ No newline at end of file +119212 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_decrease_burnEmpty.snap b/.forge-snapshots/PositionManager_decrease_burnEmpty.snap index 31c3a99c..99fb7939 100644 --- a/.forge-snapshots/PositionManager_decrease_burnEmpty.snap +++ b/.forge-snapshots/PositionManager_decrease_burnEmpty.snap @@ -1 +1 @@ -135243 \ No newline at end of file +135318 \ 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 5cbd7ccc..ef0e4aaf 100644 --- a/.forge-snapshots/PositionManager_decrease_burnEmpty_native.snap +++ b/.forge-snapshots/PositionManager_decrease_burnEmpty_native.snap @@ -1 +1 @@ -128380 \ No newline at end of file +128456 \ 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 af29b36e..651fb440 100644 --- a/.forge-snapshots/PositionManager_decrease_sameRange_allLiquidity.snap +++ b/.forge-snapshots/PositionManager_decrease_sameRange_allLiquidity.snap @@ -1 +1 @@ -132440 \ No newline at end of file +132534 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_decrease_take_take.snap b/.forge-snapshots/PositionManager_decrease_take_take.snap index 9497d424..84049ea5 100644 --- a/.forge-snapshots/PositionManager_decrease_take_take.snap +++ b/.forge-snapshots/PositionManager_decrease_take_take.snap @@ -1 +1 @@ -120329 \ No newline at end of file +120467 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_increaseLiquidity_erc20_withClose.snap b/.forge-snapshots/PositionManager_increaseLiquidity_erc20_withClose.snap index df7539ba..f237a3ea 100644 --- a/.forge-snapshots/PositionManager_increaseLiquidity_erc20_withClose.snap +++ b/.forge-snapshots/PositionManager_increaseLiquidity_erc20_withClose.snap @@ -1 +1 @@ -159057 \ No newline at end of file +159127 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_increaseLiquidity_erc20_withSettlePair.snap b/.forge-snapshots/PositionManager_increaseLiquidity_erc20_withSettlePair.snap index d62921d3..7055ac3b 100644 --- a/.forge-snapshots/PositionManager_increaseLiquidity_erc20_withSettlePair.snap +++ b/.forge-snapshots/PositionManager_increaseLiquidity_erc20_withSettlePair.snap @@ -1 +1 @@ -157997 \ No newline at end of file +158079 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_increaseLiquidity_native.snap b/.forge-snapshots/PositionManager_increaseLiquidity_native.snap index 5224273f..b4db755e 100644 --- a/.forge-snapshots/PositionManager_increaseLiquidity_native.snap +++ b/.forge-snapshots/PositionManager_increaseLiquidity_native.snap @@ -1 +1 @@ -140872 \ No newline at end of file +140942 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_increase_autocompoundExactUnclaimedFees.snap b/.forge-snapshots/PositionManager_increase_autocompoundExactUnclaimedFees.snap index 770899b4..b6d9ed62 100644 --- a/.forge-snapshots/PositionManager_increase_autocompoundExactUnclaimedFees.snap +++ b/.forge-snapshots/PositionManager_increase_autocompoundExactUnclaimedFees.snap @@ -1 +1 @@ -136359 \ No newline at end of file +136403 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_increase_autocompoundExcessFeesCredit.snap b/.forge-snapshots/PositionManager_increase_autocompoundExcessFeesCredit.snap index d9726ba2..40fa4162 100644 --- a/.forge-snapshots/PositionManager_increase_autocompoundExcessFeesCredit.snap +++ b/.forge-snapshots/PositionManager_increase_autocompoundExcessFeesCredit.snap @@ -1 +1 @@ -177364 \ No newline at end of file +177458 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_increase_autocompound_clearExcess.snap b/.forge-snapshots/PositionManager_increase_autocompound_clearExcess.snap index 21e55c88..7becdebb 100644 --- a/.forge-snapshots/PositionManager_increase_autocompound_clearExcess.snap +++ b/.forge-snapshots/PositionManager_increase_autocompound_clearExcess.snap @@ -1 +1 @@ -148040 \ No newline at end of file +148084 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_native.snap b/.forge-snapshots/PositionManager_mint_native.snap index 742cc68e..0c393410 100644 --- a/.forge-snapshots/PositionManager_mint_native.snap +++ b/.forge-snapshots/PositionManager_mint_native.snap @@ -1 +1 @@ -364745 \ No newline at end of file +364815 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_nativeWithSweep_withClose.snap b/.forge-snapshots/PositionManager_mint_nativeWithSweep_withClose.snap index 7c56236a..1ab4ab47 100644 --- a/.forge-snapshots/PositionManager_mint_nativeWithSweep_withClose.snap +++ b/.forge-snapshots/PositionManager_mint_nativeWithSweep_withClose.snap @@ -1 +1 @@ -373268 \ No newline at end of file +373334 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_nativeWithSweep_withSettlePair.snap b/.forge-snapshots/PositionManager_mint_nativeWithSweep_withSettlePair.snap index 822d688b..64d0360e 100644 --- a/.forge-snapshots/PositionManager_mint_nativeWithSweep_withSettlePair.snap +++ b/.forge-snapshots/PositionManager_mint_nativeWithSweep_withSettlePair.snap @@ -1 +1 @@ -372491 \ No newline at end of file +372569 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_onSameTickLower.snap b/.forge-snapshots/PositionManager_mint_onSameTickLower.snap index ccfdb51f..b941b6e0 100644 --- a/.forge-snapshots/PositionManager_mint_onSameTickLower.snap +++ b/.forge-snapshots/PositionManager_mint_onSameTickLower.snap @@ -1 +1 @@ -317617 \ No newline at end of file +317687 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_onSameTickUpper.snap b/.forge-snapshots/PositionManager_mint_onSameTickUpper.snap index 9f9a6b77..d9ef5ca0 100644 --- a/.forge-snapshots/PositionManager_mint_onSameTickUpper.snap +++ b/.forge-snapshots/PositionManager_mint_onSameTickUpper.snap @@ -1 +1 @@ -318287 \ No newline at end of file +318357 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_sameRange.snap b/.forge-snapshots/PositionManager_mint_sameRange.snap index ccc454dd..91240b8c 100644 --- a/.forge-snapshots/PositionManager_mint_sameRange.snap +++ b/.forge-snapshots/PositionManager_mint_sameRange.snap @@ -1 +1 @@ -243856 \ No newline at end of file +243926 \ 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 23648cc9..79e218a8 100644 --- a/.forge-snapshots/PositionManager_mint_settleWithBalance_sweep.snap +++ b/.forge-snapshots/PositionManager_mint_settleWithBalance_sweep.snap @@ -1 +1 @@ -419060 \ No newline at end of file +419134 \ 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 cedd0614..f8eb1300 100644 --- a/.forge-snapshots/PositionManager_mint_warmedPool_differentRange.snap +++ b/.forge-snapshots/PositionManager_mint_warmedPool_differentRange.snap @@ -1 +1 @@ -323648 \ No newline at end of file +323718 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_withClose.snap b/.forge-snapshots/PositionManager_mint_withClose.snap index cf0292c9..b7e3ad99 100644 --- a/.forge-snapshots/PositionManager_mint_withClose.snap +++ b/.forge-snapshots/PositionManager_mint_withClose.snap @@ -1 +1 @@ -420170 \ No newline at end of file +420240 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_withSettlePair.snap b/.forge-snapshots/PositionManager_mint_withSettlePair.snap index 549b6841..eedfce3e 100644 --- a/.forge-snapshots/PositionManager_mint_withSettlePair.snap +++ b/.forge-snapshots/PositionManager_mint_withSettlePair.snap @@ -1 +1 @@ -419228 \ No newline at end of file +419310 \ 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 b51b3f66..66493802 100644 --- a/.forge-snapshots/PositionManager_multicall_initialize_mint.snap +++ b/.forge-snapshots/PositionManager_multicall_initialize_mint.snap @@ -1 +1 @@ -455975 \ No newline at end of file +456088 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_permit_twice.snap b/.forge-snapshots/PositionManager_permit_twice.snap index 379f9611..d650ccbd 100644 --- a/.forge-snapshots/PositionManager_permit_twice.snap +++ b/.forge-snapshots/PositionManager_permit_twice.snap @@ -1 +1 @@ -44852 \ No newline at end of file +44876 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_subscribe.snap b/.forge-snapshots/PositionManager_subscribe.snap index e4eada75..b0cbf20a 100644 --- a/.forge-snapshots/PositionManager_subscribe.snap +++ b/.forge-snapshots/PositionManager_subscribe.snap @@ -1 +1 @@ -84348 \ No newline at end of file +88168 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_unsubscribe.snap b/.forge-snapshots/PositionManager_unsubscribe.snap index 0151c604..9bf5d646 100644 --- a/.forge-snapshots/PositionManager_unsubscribe.snap +++ b/.forge-snapshots/PositionManager_unsubscribe.snap @@ -1 +1 @@ -59238 \ No newline at end of file +63080 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_Bytecode.snap b/.forge-snapshots/V4Router_Bytecode.snap index fb87573d..9c58b982 100644 --- a/.forge-snapshots/V4Router_Bytecode.snap +++ b/.forge-snapshots/V4Router_Bytecode.snap @@ -1 +1 @@ -7148 \ No newline at end of file +7063 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactIn1Hop_nativeIn.snap b/.forge-snapshots/V4Router_ExactIn1Hop_nativeIn.snap index bc9d9989..a3d9bfc1 100644 --- a/.forge-snapshots/V4Router_ExactIn1Hop_nativeIn.snap +++ b/.forge-snapshots/V4Router_ExactIn1Hop_nativeIn.snap @@ -1 +1 @@ -115722 \ No newline at end of file +115753 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactIn1Hop_nativeOut.snap b/.forge-snapshots/V4Router_ExactIn1Hop_nativeOut.snap index 764f3bbb..f637e5fe 100644 --- a/.forge-snapshots/V4Router_ExactIn1Hop_nativeOut.snap +++ b/.forge-snapshots/V4Router_ExactIn1Hop_nativeOut.snap @@ -1 +1 @@ -116043 \ No newline at end of file +116070 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactIn1Hop_oneForZero.snap b/.forge-snapshots/V4Router_ExactIn1Hop_oneForZero.snap index 40990e3c..f2476e2f 100644 --- a/.forge-snapshots/V4Router_ExactIn1Hop_oneForZero.snap +++ b/.forge-snapshots/V4Router_ExactIn1Hop_oneForZero.snap @@ -1 +1 @@ -124861 \ No newline at end of file +124888 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactIn1Hop_zeroForOne.snap b/.forge-snapshots/V4Router_ExactIn1Hop_zeroForOne.snap index a38c964a..690eed05 100644 --- a/.forge-snapshots/V4Router_ExactIn1Hop_zeroForOne.snap +++ b/.forge-snapshots/V4Router_ExactIn1Hop_zeroForOne.snap @@ -1 +1 @@ -130584 \ No newline at end of file +130611 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactIn2Hops.snap b/.forge-snapshots/V4Router_ExactIn2Hops.snap index 208b1023..beee482b 100644 --- a/.forge-snapshots/V4Router_ExactIn2Hops.snap +++ b/.forge-snapshots/V4Router_ExactIn2Hops.snap @@ -1 +1 @@ -185439 \ No newline at end of file +185452 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactIn2Hops_nativeIn.snap b/.forge-snapshots/V4Router_ExactIn2Hops_nativeIn.snap index 2862d64c..36622c52 100644 --- a/.forge-snapshots/V4Router_ExactIn2Hops_nativeIn.snap +++ b/.forge-snapshots/V4Router_ExactIn2Hops_nativeIn.snap @@ -1 +1 @@ -170577 \ No newline at end of file +170594 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactIn3Hops.snap b/.forge-snapshots/V4Router_ExactIn3Hops.snap index c44d7bb0..c55ea7d8 100644 --- a/.forge-snapshots/V4Router_ExactIn3Hops.snap +++ b/.forge-snapshots/V4Router_ExactIn3Hops.snap @@ -1 +1 @@ -240297 \ No newline at end of file +240296 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactIn3Hops_nativeIn.snap b/.forge-snapshots/V4Router_ExactIn3Hops_nativeIn.snap index e98fcba7..0758ae8f 100644 --- a/.forge-snapshots/V4Router_ExactIn3Hops_nativeIn.snap +++ b/.forge-snapshots/V4Router_ExactIn3Hops_nativeIn.snap @@ -1 +1 @@ -225435 \ No newline at end of file +225438 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactInputSingle.snap b/.forge-snapshots/V4Router_ExactInputSingle.snap index 2cd533ee..2fb484c4 100644 --- a/.forge-snapshots/V4Router_ExactInputSingle.snap +++ b/.forge-snapshots/V4Router_ExactInputSingle.snap @@ -1 +1 @@ -129854 \ No newline at end of file +129642 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactInputSingle_nativeIn.snap b/.forge-snapshots/V4Router_ExactInputSingle_nativeIn.snap index 5e5c5b3b..a8669680 100644 --- a/.forge-snapshots/V4Router_ExactInputSingle_nativeIn.snap +++ b/.forge-snapshots/V4Router_ExactInputSingle_nativeIn.snap @@ -1 +1 @@ -114992 \ No newline at end of file +114784 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactInputSingle_nativeOut.snap b/.forge-snapshots/V4Router_ExactInputSingle_nativeOut.snap index f36fa450..d8315aec 100644 --- a/.forge-snapshots/V4Router_ExactInputSingle_nativeOut.snap +++ b/.forge-snapshots/V4Router_ExactInputSingle_nativeOut.snap @@ -1 +1 @@ -115282 \ No newline at end of file +115069 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactOut1Hop_nativeIn_sweepETH.snap b/.forge-snapshots/V4Router_ExactOut1Hop_nativeIn_sweepETH.snap index 9c6beb91..0b08fa2f 100644 --- a/.forge-snapshots/V4Router_ExactOut1Hop_nativeIn_sweepETH.snap +++ b/.forge-snapshots/V4Router_ExactOut1Hop_nativeIn_sweepETH.snap @@ -1 +1 @@ -121985 \ No newline at end of file +122016 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactOut1Hop_nativeOut.snap b/.forge-snapshots/V4Router_ExactOut1Hop_nativeOut.snap index adfd51ab..b1d78c0e 100644 --- a/.forge-snapshots/V4Router_ExactOut1Hop_nativeOut.snap +++ b/.forge-snapshots/V4Router_ExactOut1Hop_nativeOut.snap @@ -1 +1 @@ -117107 \ No newline at end of file +117134 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactOut1Hop_oneForZero.snap b/.forge-snapshots/V4Router_ExactOut1Hop_oneForZero.snap index 7692da73..6202f067 100644 --- a/.forge-snapshots/V4Router_ExactOut1Hop_oneForZero.snap +++ b/.forge-snapshots/V4Router_ExactOut1Hop_oneForZero.snap @@ -1 +1 @@ -125925 \ No newline at end of file +125952 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactOut1Hop_zeroForOne.snap b/.forge-snapshots/V4Router_ExactOut1Hop_zeroForOne.snap index 93703d01..1c782b16 100644 --- a/.forge-snapshots/V4Router_ExactOut1Hop_zeroForOne.snap +++ b/.forge-snapshots/V4Router_ExactOut1Hop_zeroForOne.snap @@ -1 +1 @@ -129870 \ No newline at end of file +129897 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactOut2Hops.snap b/.forge-snapshots/V4Router_ExactOut2Hops.snap index abfba6e5..ff527d26 100644 --- a/.forge-snapshots/V4Router_ExactOut2Hops.snap +++ b/.forge-snapshots/V4Router_ExactOut2Hops.snap @@ -1 +1 @@ -183787 \ No newline at end of file +183800 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactOut2Hops_nativeIn.snap b/.forge-snapshots/V4Router_ExactOut2Hops_nativeIn.snap index f236e20d..75b2b47c 100644 --- a/.forge-snapshots/V4Router_ExactOut2Hops_nativeIn.snap +++ b/.forge-snapshots/V4Router_ExactOut2Hops_nativeIn.snap @@ -1 +1 @@ -175902 \ No newline at end of file +175919 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactOut3Hops.snap b/.forge-snapshots/V4Router_ExactOut3Hops.snap index cfb3e827..1ed797a7 100644 --- a/.forge-snapshots/V4Router_ExactOut3Hops.snap +++ b/.forge-snapshots/V4Router_ExactOut3Hops.snap @@ -1 +1 @@ -237735 \ No newline at end of file +237734 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactOut3Hops_nativeIn.snap b/.forge-snapshots/V4Router_ExactOut3Hops_nativeIn.snap index c3091101..209ae505 100644 --- a/.forge-snapshots/V4Router_ExactOut3Hops_nativeIn.snap +++ b/.forge-snapshots/V4Router_ExactOut3Hops_nativeIn.snap @@ -1 +1 @@ -229850 \ No newline at end of file +229853 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactOut3Hops_nativeOut.snap b/.forge-snapshots/V4Router_ExactOut3Hops_nativeOut.snap index 575b9ea9..50495b29 100644 --- a/.forge-snapshots/V4Router_ExactOut3Hops_nativeOut.snap +++ b/.forge-snapshots/V4Router_ExactOut3Hops_nativeOut.snap @@ -1 +1 @@ -217090 \ No newline at end of file +217089 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactOutputSingle.snap b/.forge-snapshots/V4Router_ExactOutputSingle.snap index 5de03712..178defb9 100644 --- a/.forge-snapshots/V4Router_ExactOutputSingle.snap +++ b/.forge-snapshots/V4Router_ExactOutputSingle.snap @@ -1 +1 @@ -129140 \ No newline at end of file +128925 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactOutputSingle_nativeIn_sweepETH.snap b/.forge-snapshots/V4Router_ExactOutputSingle_nativeIn_sweepETH.snap index 6120543a..c7076c46 100644 --- a/.forge-snapshots/V4Router_ExactOutputSingle_nativeIn_sweepETH.snap +++ b/.forge-snapshots/V4Router_ExactOutputSingle_nativeIn_sweepETH.snap @@ -1 +1 @@ -121255 \ No newline at end of file +121044 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactOutputSingle_nativeOut.snap b/.forge-snapshots/V4Router_ExactOutputSingle_nativeOut.snap index b7b122f5..a21b83f7 100644 --- a/.forge-snapshots/V4Router_ExactOutputSingle_nativeOut.snap +++ b/.forge-snapshots/V4Router_ExactOutputSingle_nativeOut.snap @@ -1 +1 @@ -116452 \ No newline at end of file +116236 \ No newline at end of file diff --git a/.forge-snapshots/positionDescriptor bytecode size.snap b/.forge-snapshots/positionDescriptor bytecode size.snap index fcd5df5e..9985dac9 100644 --- a/.forge-snapshots/positionDescriptor bytecode size.snap +++ b/.forge-snapshots/positionDescriptor bytecode size.snap @@ -1 +1 @@ -31144 \ No newline at end of file +31514 \ No newline at end of file diff --git a/script/DeployPosm.s.sol b/script/DeployPosm.s.sol index 5bbb6184..e3a20758 100644 --- a/script/DeployPosm.s.sol +++ b/script/DeployPosm.s.sol @@ -10,6 +10,7 @@ import {PositionManager} from "../src/PositionManager.sol"; import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol"; import {IPositionDescriptor} from "../src/interfaces/IPositionDescriptor.sol"; import {PositionDescriptor} from "../src/PositionDescriptor.sol"; +import {IWETH9} from "../src/interfaces/external/IWETH9.sol"; contract DeployPosmTest is Script { function setUp() public {} @@ -30,7 +31,8 @@ contract DeployPosmTest is Script { IPoolManager(poolManager), IAllowanceTransfer(permit2), unsubscribeGasLimit, - IPositionDescriptor(address(positionDescriptor)) + IPositionDescriptor(address(positionDescriptor)), + IWETH9(wrappedNative) ); console2.log("PositionManager", address(posm)); diff --git a/src/PositionDescriptor.sol b/src/PositionDescriptor.sol index 716f7497..1ae83856 100644 --- a/src/PositionDescriptor.sol +++ b/src/PositionDescriptor.sol @@ -11,7 +11,7 @@ import {IPositionDescriptor} from "./interfaces/IPositionDescriptor.sol"; import {PositionInfo, PositionInfoLibrary} from "./libraries/PositionInfoLibrary.sol"; import {Descriptor} from "./libraries/Descriptor.sol"; import {AddressRatioSortOrder} from "./libraries/AddressRatioSortOrder.sol"; -import {SafeERC20Metadata} from "./libraries/SafeERC20Metadata.sol"; +import {SafeAddressMetadata} from "./libraries/SafeAddressMetadata.sol"; /// @title Describes NFT token positions /// @notice Produces a string containing the data URI for a JSON metadata string @@ -71,10 +71,10 @@ contract PositionDescriptor is IPositionDescriptor { tokenId: tokenId, quoteAddress: quoteAddress, baseAddress: baseAddress, - quoteAddressSymbol: SafeERC20Metadata.addressSymbol(quoteAddress, nativeAddressLabel), - baseAddressSymbol: SafeERC20Metadata.addressSymbol(baseAddress, nativeAddressLabel), - quoteAddressDecimals: SafeERC20Metadata.addressDecimals(quoteAddress), - baseAddressDecimals: SafeERC20Metadata.addressDecimals(baseAddress), + quoteAddressSymbol: SafeAddressMetadata.addressSymbol(quoteAddress, nativeAddressLabel), + baseAddressSymbol: SafeAddressMetadata.addressSymbol(baseAddress, nativeAddressLabel), + quoteAddressDecimals: SafeAddressMetadata.addressDecimals(quoteAddress), + baseAddressDecimals: SafeAddressMetadata.addressDecimals(baseAddress), flipRatio: _flipRatio, tickLower: positionInfo.tickLower(), tickUpper: positionInfo.tickUpper(), diff --git a/src/PositionManager.sol b/src/PositionManager.sol index d5721e34..428fb4b7 100644 --- a/src/PositionManager.sol +++ b/src/PositionManager.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.26; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; -import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; import {Position} from "@uniswap/v4-core/src/libraries/Position.sol"; @@ -26,6 +26,8 @@ import {CalldataDecoder} from "./libraries/CalldataDecoder.sol"; import {Permit2Forwarder} from "./base/Permit2Forwarder.sol"; import {SlippageCheck} from "./libraries/SlippageCheck.sol"; import {PositionInfo, PositionInfoLibrary} from "./libraries/PositionInfoLibrary.sol"; +import {NativeWrapper} from "./base/NativeWrapper.sol"; +import {IWETH9} from "./interfaces/external/IWETH9.sol"; // 444444444 // 444444444444 444444 @@ -102,7 +104,8 @@ contract PositionManager is ReentrancyLock, BaseActionsRouter, Notifier, - Permit2Forwarder + Permit2Forwarder, + NativeWrapper { using PoolIdLibrary for PoolKey; using StateLibrary for IPoolManager; @@ -126,12 +129,14 @@ contract PositionManager is IPoolManager _poolManager, IAllowanceTransfer _permit2, uint256 _unsubscribeGasLimit, - IPositionDescriptor _tokenDescriptor + IPositionDescriptor _tokenDescriptor, + IWETH9 _weth9 ) BaseActionsRouter(_poolManager) Permit2Forwarder(_permit2) ERC721Permit_v4("Uniswap v4 Positions NFT", "UNI-V4-POSM") Notifier(_unsubscribeGasLimit) + NativeWrapper(_weth9) { tokenDescriptor = _tokenDescriptor; } @@ -153,6 +158,12 @@ contract PositionManager is _; } + /// @notice Enforces that the PoolManager is locked. + modifier onlyIfPoolManagerLocked() override { + if (poolManager.isUnlocked()) revert PoolManagerMustBeLocked(); + _; + } + function tokenURI(uint256 tokenId) public view override returns (string memory) { return IPositionDescriptor(tokenDescriptor).tokenURI(this, tokenId); } @@ -242,6 +253,14 @@ contract PositionManager is (Currency currency, address to) = params.decodeCurrencyAndAddress(); _sweep(currency, _mapRecipient(to)); return; + } else if (action == Actions.WRAP) { + uint256 amount = params.decodeUint256(); + _wrap(_mapWrapUnwrapAmount(CurrencyLibrary.ADDRESS_ZERO, amount, Currency.wrap(address(WETH9)))); + return; + } else if (action == Actions.UNWRAP) { + uint256 amount = params.decodeUint256(); + _unwrap(_mapWrapUnwrapAmount(Currency.wrap(address(WETH9)), amount, CurrencyLibrary.ADDRESS_ZERO)); + return; } } revert UnsupportedAction(action); @@ -363,7 +382,7 @@ contract PositionManager is if (currencyDelta < 0) { // Casting is safe due to limits on the total supply of a pool _settle(currency, caller, uint256(-currencyDelta)); - } else if (currencyDelta > 0) { + } else { _take(currency, caller, uint256(currencyDelta)); } } @@ -431,7 +450,8 @@ contract PositionManager is } /// @dev overrides solmate transferFrom in case a notification to subscribers is needed - function transferFrom(address from, address to, uint256 id) public virtual override { + /// @dev will revert if pool manager is locked + function transferFrom(address from, address to, uint256 id) public virtual override onlyIfPoolManagerLocked { super.transferFrom(from, to, id); if (positionInfo[id].hasSubscriber()) _notifyTransfer(id, from, to); } diff --git a/src/V4Router.sol b/src/V4Router.sol index 282ab77c..4f69a702 100644 --- a/src/V4Router.sol +++ b/src/V4Router.sol @@ -90,9 +90,8 @@ abstract contract V4Router is IV4Router, BaseActionsRouter, DeltaResolver { amountIn = _getFullCredit(params.zeroForOne ? params.poolKey.currency0 : params.poolKey.currency1).toUint128(); } - uint128 amountOut = _swap( - params.poolKey, params.zeroForOne, -int256(uint256(amountIn)), params.sqrtPriceLimitX96, params.hookData - ).toUint128(); + uint128 amountOut = + _swap(params.poolKey, params.zeroForOne, -int256(uint256(amountIn)), params.hookData).toUint128(); if (amountOut < params.amountOutMinimum) revert V4TooLittleReceived(params.amountOutMinimum, amountOut); } @@ -110,7 +109,7 @@ abstract contract V4Router is IV4Router, BaseActionsRouter, DeltaResolver { pathKey = params.path[i]; (PoolKey memory poolKey, bool zeroForOne) = pathKey.getPoolAndSwapDirection(currencyIn); // The output delta will always be positive, except for when interacting with certain hook pools - amountOut = _swap(poolKey, zeroForOne, -int256(uint256(amountIn)), 0, pathKey.hookData).toUint128(); + amountOut = _swap(poolKey, zeroForOne, -int256(uint256(amountIn)), pathKey.hookData).toUint128(); amountIn = amountOut; currencyIn = pathKey.intermediateCurrency; @@ -127,17 +126,7 @@ abstract contract V4Router is IV4Router, BaseActionsRouter, DeltaResolver { _getFullDebt(params.zeroForOne ? params.poolKey.currency1 : params.poolKey.currency0).toUint128(); } uint128 amountIn = ( - uint256( - -int256( - _swap( - params.poolKey, - params.zeroForOne, - int256(uint256(amountOut)), - params.sqrtPriceLimitX96, - params.hookData - ) - ) - ) + uint256(-int256(_swap(params.poolKey, params.zeroForOne, int256(uint256(amountOut)), params.hookData))) ).toUint128(); if (amountIn > params.amountInMaximum) revert V4TooMuchRequested(params.amountInMaximum, amountIn); } @@ -159,9 +148,8 @@ abstract contract V4Router is IV4Router, BaseActionsRouter, DeltaResolver { pathKey = params.path[i - 1]; (PoolKey memory poolKey, bool oneForZero) = pathKey.getPoolAndSwapDirection(currencyOut); // The output delta will always be negative, except for when interacting with certain hook pools - amountIn = ( - uint256(-int256(_swap(poolKey, !oneForZero, int256(uint256(amountOut)), 0, pathKey.hookData))) - ).toUint128(); + amountIn = (uint256(-int256(_swap(poolKey, !oneForZero, int256(uint256(amountOut)), pathKey.hookData)))) + .toUint128(); amountOut = amountIn; currencyOut = pathKey.intermediateCurrency; @@ -170,22 +158,16 @@ abstract contract V4Router is IV4Router, BaseActionsRouter, DeltaResolver { } } - function _swap( - PoolKey memory poolKey, - bool zeroForOne, - int256 amountSpecified, - uint160 sqrtPriceLimitX96, - bytes calldata hookData - ) private returns (int128 reciprocalAmount) { + function _swap(PoolKey memory poolKey, bool zeroForOne, int256 amountSpecified, bytes calldata hookData) + private + returns (int128 reciprocalAmount) + { + // for protection of exactOut swaps, sqrtPriceLimit is not exposed as a feature in this contract unchecked { BalanceDelta delta = poolManager.swap( poolKey, IPoolManager.SwapParams( - zeroForOne, - amountSpecified, - sqrtPriceLimitX96 == 0 - ? (zeroForOne ? TickMath.MIN_SQRT_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1) - : sqrtPriceLimitX96 + zeroForOne, amountSpecified, zeroForOne ? TickMath.MIN_SQRT_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1 ), hookData ); diff --git a/src/base/DeltaResolver.sol b/src/base/DeltaResolver.sol index ccde3d4d..85fd1212 100644 --- a/src/base/DeltaResolver.sol +++ b/src/base/DeltaResolver.sol @@ -16,12 +16,16 @@ abstract contract DeltaResolver is ImmutableState { error DeltaNotPositive(Currency currency); /// @notice Emitted trying to take a negative delta. error DeltaNotNegative(Currency currency); + /// @notice Emitted when the contract does not have enough balance to wrap or unwrap. + error InsufficientBalance(); /// @notice Take an amount of currency out of the PoolManager /// @param currency Currency to take /// @param recipient Address to receive the currency /// @param amount Amount to take + /// @dev Returns early if the amount is 0 function _take(Currency currency, address recipient, uint256 amount) internal { + if (amount == 0) return; poolManager.take(currency, recipient, amount); } @@ -30,7 +34,9 @@ abstract contract DeltaResolver is ImmutableState { /// @param currency Currency to settle /// @param payer Address of the payer /// @param amount Amount to send + /// @dev Returns early if the amount is 0 function _settle(Currency currency, address payer, uint256 amount) internal { + if (amount == 0) return; if (currency.isAddressZero()) { poolManager.settle{value: amount}(); } else { @@ -87,4 +93,30 @@ abstract contract DeltaResolver is ImmutableState { return amount; } } + + /// @notice Calculates the sanitized amount before wrapping/unwrapping. + /// @param inputCurrency The currency, either native or wrapped native, that this contract holds + /// @param amount The amount to wrap or unwrap. Can be CONTRACT_BALANCE, OPEN_DELTA or a specific amount + /// @param outputCurrency The currency after the wrap/unwrap that the user may owe a balance in on the poolManager + function _mapWrapUnwrapAmount(Currency inputCurrency, uint256 amount, Currency outputCurrency) + internal + view + returns (uint256) + { + // if wrapping, the balance in this contract is in ETH + // if unwrapping, the balance in this contract is in WETH + uint256 balance = inputCurrency.balanceOf(address(this)); + if (amount == ActionConstants.CONTRACT_BALANCE) { + // return early to avoid unnecessary balance check + return balance; + } + if (amount == ActionConstants.OPEN_DELTA) { + // if wrapping, the open currency on the PoolManager is WETH. + // if unwrapping, the open currency on the PoolManager is ETH. + // note that we use the DEBT amount. Positive deltas can be taken and then wrapped. + amount = _getFullDebt(outputCurrency); + } + if (amount > balance) revert InsufficientBalance(); + return amount; + } } diff --git a/src/base/NativeWrapper.sol b/src/base/NativeWrapper.sol new file mode 100644 index 00000000..ef6a3f2a --- /dev/null +++ b/src/base/NativeWrapper.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import {IWETH9} from "../interfaces/external/IWETH9.sol"; +import {ActionConstants} from "../libraries/ActionConstants.sol"; +import {ImmutableState} from "./ImmutableState.sol"; + +/// @title Native Wrapper +/// @notice Used for wrapping and unwrapping native +abstract contract NativeWrapper is ImmutableState { + /// @notice The address for WETH9 + IWETH9 public immutable WETH9; + + /// @notice Thrown when an unexpected address sends ETH to this contract + error InvalidEthSender(); + + constructor(IWETH9 _weth9) { + WETH9 = _weth9; + } + + /// @dev The amount should already be <= the current balance in this contract. + function _wrap(uint256 amount) internal { + if (amount > 0) WETH9.deposit{value: amount}(); + } + + /// @dev The amount should already be <= the current balance in this contract. + function _unwrap(uint256 amount) internal { + if (amount > 0) WETH9.withdraw(amount); + } + + receive() external payable { + if (msg.sender != address(WETH9) && msg.sender != address(poolManager)) revert InvalidEthSender(); + } +} diff --git a/src/base/Notifier.sol b/src/base/Notifier.sol index 558e85b7..2965e574 100644 --- a/src/base/Notifier.sol +++ b/src/base/Notifier.sol @@ -29,6 +29,9 @@ abstract contract Notifier is INotifier { /// @param tokenId the tokenId of the position modifier onlyIfApproved(address caller, uint256 tokenId) virtual; + /// @notice Enforces that the PoolManager is locked. + modifier onlyIfPoolManagerLocked() virtual; + function _setUnsubscribed(uint256 tokenId) internal virtual; function _setSubscribed(uint256 tokenId) internal virtual; @@ -37,6 +40,7 @@ abstract contract Notifier is INotifier { function subscribe(uint256 tokenId, address newSubscriber, bytes calldata data) external payable + onlyIfPoolManagerLocked onlyIfApproved(msg.sender, tokenId) { ISubscriber _subscriber = subscriber[tokenId]; @@ -56,7 +60,12 @@ abstract contract Notifier is INotifier { } /// @inheritdoc INotifier - function unsubscribe(uint256 tokenId) external payable onlyIfApproved(msg.sender, tokenId) { + function unsubscribe(uint256 tokenId) + external + payable + onlyIfPoolManagerLocked + onlyIfApproved(msg.sender, tokenId) + { _unsubscribe(tokenId); } diff --git a/src/interfaces/INotifier.sol b/src/interfaces/INotifier.sol index d6cca908..3eefba3c 100644 --- a/src/interfaces/INotifier.sol +++ b/src/interfaces/INotifier.sol @@ -36,6 +36,7 @@ interface INotifier { /// @param data caller-provided data that's forwarded to the subscriber contract /// @dev Calling subscribe when a position is already subscribed will revert /// @dev payable so it can be multicalled with NATIVE related actions + /// @dev will revert if pool manager is locked function subscribe(uint256 tokenId, address newSubscriber, bytes calldata data) external payable; /// @notice Removes the subscriber from receiving notifications for a respective position @@ -43,6 +44,7 @@ interface INotifier { /// @dev Callers must specify a high gas limit (remaining gas should be higher than unsubscriberGasLimit) such that the subscriber can be notified /// @dev payable so it can be multicalled with NATIVE related actions /// @dev Must always allow a user to unsubscribe. In the case of a malicious subscriber, a user can always unsubscribe safely, ensuring liquidity is always modifiable. + /// @dev will revert if pool manager is locked function unsubscribe(uint256 tokenId) external payable; /// @notice Returns and determines the maximum allowable gas-used for notifying unsubscribe diff --git a/src/interfaces/IPositionManager.sol b/src/interfaces/IPositionManager.sol index fbdfdd5c..53a2efbe 100644 --- a/src/interfaces/IPositionManager.sol +++ b/src/interfaces/IPositionManager.sol @@ -14,6 +14,9 @@ interface IPositionManager is INotifier, IImmutableState { error NotApproved(address caller); /// @notice Thrown when the block.timestamp exceeds the user-provided deadline error DeadlinePassed(uint256 deadline); + /// @notice Thrown when calling transfer, subscribe, or unsubscribe when the PoolManager is unlocked. + /// @dev This is to prevent hooks from being able to trigger notifications at the same time the position is being modified. + error PoolManagerMustBeLocked(); /// @notice Unlocks Uniswap v4 PoolManager and batches actions for modifying liquidity /// @dev This is the standard entrypoint for the PositionManager diff --git a/src/interfaces/IV4Router.sol b/src/interfaces/IV4Router.sol index 9c081244..a24ae9a9 100644 --- a/src/interfaces/IV4Router.sol +++ b/src/interfaces/IV4Router.sol @@ -20,7 +20,6 @@ interface IV4Router is IImmutableState { bool zeroForOne; uint128 amountIn; uint128 amountOutMinimum; - uint160 sqrtPriceLimitX96; bytes hookData; } @@ -38,7 +37,6 @@ interface IV4Router is IImmutableState { bool zeroForOne; uint128 amountOut; uint128 amountInMaximum; - uint160 sqrtPriceLimitX96; bytes hookData; } diff --git a/src/interfaces/external/IWETH9.sol b/src/interfaces/external/IWETH9.sol new file mode 100644 index 00000000..37c3be8e --- /dev/null +++ b/src/interfaces/external/IWETH9.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/// @title Interface for WETH9 +interface IWETH9 is IERC20 { + /// @notice Deposit ether to get wrapped ether + function deposit() external payable; + + /// @notice Withdraw wrapped ether to get ether + function withdraw(uint256) external; +} diff --git a/src/libraries/Actions.sol b/src/libraries/Actions.sol index 49d3e04f..16a8ce8c 100644 --- a/src/libraries/Actions.sol +++ b/src/libraries/Actions.sol @@ -33,8 +33,10 @@ library Actions { uint256 constant CLOSE_CURRENCY = 0x17; uint256 constant CLEAR_OR_TAKE = 0x18; uint256 constant SWEEP = 0x19; + uint256 constant WRAP = 0x20; + uint256 constant UNWRAP = 0x21; // minting/burning 6909s to close deltas - uint256 constant MINT_6909 = 0x20; - uint256 constant BURN_6909 = 0x21; + uint256 constant MINT_6909 = 0x22; + uint256 constant BURN_6909 = 0x23; } diff --git a/src/libraries/CalldataDecoder.sol b/src/libraries/CalldataDecoder.sol index 00cf6e33..a14f3a34 100644 --- a/src/libraries/CalldataDecoder.sol +++ b/src/libraries/CalldataDecoder.sol @@ -241,6 +241,13 @@ library CalldataDecoder { } } + /// @dev equivalent to: abi.decode(params, (uint256)) in calldata + function decodeUint256(bytes calldata params) internal pure returns (uint256 amount) { + assembly ("memory-safe") { + amount := calldataload(params.offset) + } + } + /// @dev equivalent to: abi.decode(params, (Currency, uint256, bool)) in calldata function decodeCurrencyUint256AndBool(bytes calldata params) internal diff --git a/src/libraries/Descriptor.sol b/src/libraries/Descriptor.sol index 56f7cb39..01057eb0 100644 --- a/src/libraries/Descriptor.sol +++ b/src/libraries/Descriptor.sol @@ -44,16 +44,16 @@ library Descriptor { function constructTokenURI(ConstructTokenURIParams memory params) internal pure returns (string memory) { string memory name = generateName(params, feeToPercentString(params.fee)); string memory descriptionPartOne = generateDescriptionPartOne( - escapeQuotes(params.quoteAddressSymbol), - escapeQuotes(params.baseAddressSymbol), + escapeSpecialCharacters(params.quoteAddressSymbol), + escapeSpecialCharacters(params.baseAddressSymbol), addressToString(params.poolManager) ); string memory descriptionPartTwo = generateDescriptionPartTwo( params.tokenId.toString(), - escapeQuotes(params.baseAddressSymbol), + escapeSpecialCharacters(params.baseAddressSymbol), params.quoteAddress == address(0) ? "Native" : addressToString(params.quoteAddress), params.baseAddress == address(0) ? "Native" : addressToString(params.baseAddress), - addressToString(params.hooks), + params.hooks == address(0) ? "No Hook" : addressToString(params.hooks), feeToPercentString(params.fee) ); string memory image = Base64.encode(bytes(generateSVGImage(params))); @@ -80,23 +80,23 @@ library Descriptor { ); } - /// @notice Escapes double quotes in a string if they are present - function escapeQuotes(string memory symbol) internal pure returns (string memory) { + /// @notice Escapes special characters in a string if they are present + function escapeSpecialCharacters(string memory symbol) internal pure returns (string memory) { bytes memory symbolBytes = bytes(symbol); - uint8 quotesCount = 0; - // count the amount of double quotes (") in the symbol + uint8 specialCharCount = 0; + // count the amount of double quotes, form feeds, new lines, carriage returns, or tabs in the symbol for (uint8 i = 0; i < symbolBytes.length; i++) { - if (symbolBytes[i] == '"') { - quotesCount++; + if (isSpecialCharacter(symbolBytes[i])) { + specialCharCount++; } } - if (quotesCount > 0) { - // create a new bytes array with enough space to hold the original bytes plus space for the backslashes to escape the quotes - bytes memory escapedBytes = new bytes(symbolBytes.length + quotesCount); + if (specialCharCount > 0) { + // create a new bytes array with enough space to hold the original bytes plus space for the backslashes to escape the special characters + bytes memory escapedBytes = new bytes(symbolBytes.length + specialCharCount); uint256 index; for (uint8 i = 0; i < symbolBytes.length; i++) { - // add a '\' before any double quotes - if (symbolBytes[i] == '"') { + // add a '\' before any double quotes, form feeds, new lines, carriage returns, or tabs + if (isSpecialCharacter(symbolBytes[i])) { escapedBytes[index++] = "\\"; } // copy each byte from original string to the new array @@ -185,9 +185,9 @@ library Descriptor { "Uniswap - ", feeTier, " - ", - escapeQuotes(params.quoteAddressSymbol), + escapeSpecialCharacters(params.quoteAddressSymbol), "/", - escapeQuotes(params.baseAddressSymbol), + escapeSpecialCharacters(params.baseAddressSymbol), " - ", tickToDecimalString( !params.flipRatio ? params.tickLower : params.tickUpper, @@ -502,6 +502,10 @@ library Descriptor { } } + function isSpecialCharacter(bytes1 b) private pure returns (bool) { + return b == '"' || b == "\u000c" || b == "\n" || b == "\r" || b == "\t"; + } + function scale(uint256 n, uint256 inMn, uint256 inMx, uint256 outMn, uint256 outMx) private pure diff --git a/src/libraries/SVG.sol b/src/libraries/SVG.sol index 0da00a50..6bbaf90a 100644 --- a/src/libraries/SVG.sol +++ b/src/libraries/SVG.sol @@ -345,7 +345,9 @@ library SVG { string memory tickLowerStr = tickToString(tickLower); string memory tickUpperStr = tickToString(tickUpper); uint256 str1length = bytes(tokenId).length + 4; - string memory hookSlice = string(abi.encodePacked(substring(hookStr, 0, 5), "...", substring(hookStr, 37, 40))); + string memory hookSlice = hook == address(0) + ? "No Hook" + : string(abi.encodePacked(substring(hookStr, 0, 5), "...", substring(hookStr, 39, 42))); uint256 str2length = bytes(hookSlice).length + 5; uint256 str3length = bytes(tickLowerStr).length + 10; uint256 str4length = bytes(tickUpperStr).length + 10; diff --git a/src/libraries/SafeAddressMetadata.sol b/src/libraries/SafeAddressMetadata.sol index 24e2b653..6184c691 100644 --- a/src/libraries/SafeAddressMetadata.sol +++ b/src/libraries/SafeAddressMetadata.sol @@ -36,7 +36,10 @@ library SafeAddressMetadata { return 0; } if (data.length == 32) { - return abi.decode(data, (uint8)); + uint256 decimals = abi.decode(data, (uint256)); + if (decimals <= type(uint8).max) { + return uint8(decimals); + } } return 0; } diff --git a/test/PositionDescriptor.t.sol b/test/PositionDescriptor.t.sol index e56bfae0..3084b1ad 100644 --- a/test/PositionDescriptor.t.sol +++ b/test/PositionDescriptor.t.sol @@ -12,10 +12,14 @@ import {PosmTestSetup} from "./shared/PosmTestSetup.sol"; import {ActionConstants} from "../src/libraries/ActionConstants.sol"; import {Base64} from "./base64.sol"; import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; -import {CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import {SafeAddressMetadata} from "../src/libraries/SafeAddressMetadata.sol"; +import {AddressStringUtil} from "../src/libraries/AddressStringUtil.sol"; +import {Descriptor} from "../src/libraries/Descriptor.sol"; contract PositionDescriptorTest is Test, PosmTestSetup, GasSnapshot { using Base64 for string; + using CurrencyLibrary for Currency; address public WETH9 = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; address public DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F; @@ -120,11 +124,23 @@ contract PositionDescriptorTest is Test, PosmTestSetup, GasSnapshot { bytes memory data = vm.parseJson(json); Token memory token = abi.decode(data, (Token)); - assertEq(token.name, "Uniswap - 0.3% - TEST/TEST - 1.0060<>1.0121"); + // quote is currency1, base is currency0 + string memory symbol0 = SafeAddressMetadata.addressSymbol(Currency.unwrap(currency0), nativeCurrencyLabel); + string memory symbol1 = SafeAddressMetadata.addressSymbol(Currency.unwrap(currency1), nativeCurrencyLabel); + string memory managerAddress = toHexString(address(manager)); + string memory currency0Address = toHexString(Currency.unwrap(currency0)); + string memory currency1Address = toHexString(Currency.unwrap(currency1)); + string memory id = uintToString(tokenId); + string memory hookAddress = address(key.hooks) == address(0) ? "No Hook" : string(abi.encodePacked("0x", toHexString(address(key.hooks)))); + string memory fee = Descriptor.feeToPercentString(key.fee); + string memory tickToDecimal0 = Descriptor.tickToDecimalString(tickLower, key.tickSpacing, SafeAddressMetadata.addressDecimals(Currency.unwrap(currency0)), SafeAddressMetadata.addressDecimals(Currency.unwrap(currency1)), false); + string memory tickToDecimal1 = Descriptor.tickToDecimalString(tickUpper, key.tickSpacing, SafeAddressMetadata.addressDecimals(Currency.unwrap(currency0)), SafeAddressMetadata.addressDecimals(Currency.unwrap(currency1)), false); + + assertEq(token.name, string(abi.encodePacked("Uniswap - ", fee, " - ", symbol1, "/", symbol0, " - ", tickToDecimal0, "<>", tickToDecimal1))); assertEq( token.description, - unicode"This NFT represents a liquidity position in a Uniswap v4 TEST-TEST pool. The owner of this NFT can modify or redeem the position.\n\nPool Manager Address: 0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f\nTEST Address: 0x5991a2df15a8f6a256d3ec51e99254cd3fb576a9\nTEST Address: 0x2e234dae75c793f67a35089c9d99245e1c58470b\nHook Address: 0x0000000000000000000000000000000000000000\nFee Tier: 0.3%\nToken ID: 1\n\n⚠️ DISCLAIMER: Due diligence is imperative when assessing this NFT. Make sure addresses match the expected addresses, as symbols may be imitated." - ); + string(abi.encodePacked(unicode"This NFT represents a liquidity position in a Uniswap v4 ", symbol1, "-", symbol0, " pool. The owner of this NFT can modify or redeem the position.\n\nPool Manager Address: ", managerAddress, "\n", symbol1, " Address: ", currency1Address, "\n", symbol0, " Address: ", currency0Address, "\nHook Address: ", hookAddress, "\nFee Tier: ", fee, "\nToken ID: ", id, "\n\n", unicode"⚠️ DISCLAIMER: Due diligence is imperative when assessing this NFT. Make sure addresses match the expected addresses, as symbols may be imitated.") + )); } function test_native_tokenURI_succeeds() public { @@ -167,11 +183,24 @@ contract PositionDescriptorTest is Test, PosmTestSetup, GasSnapshot { bytes memory data = vm.parseJson(json); Token memory token = abi.decode(data, (Token)); - assertEq(token.name, "Uniswap - 0.3% - TEST/ETH - 1.0060<>1.0121"); + + // quote is currency1, base is currency0 + string memory symbol0 = SafeAddressMetadata.addressSymbol(Currency.unwrap(nativeKey.currency0), nativeCurrencyLabel); + string memory symbol1 = SafeAddressMetadata.addressSymbol(Currency.unwrap(nativeKey.currency1), nativeCurrencyLabel); + string memory managerAddress = toHexString(address(manager)); + string memory currency0Address = Currency.unwrap(nativeKey.currency0) == address(0) ? "Native" : toHexString(Currency.unwrap(nativeKey.currency0)); + string memory currency1Address = Currency.unwrap(nativeKey.currency1) == address(0) ? "Native" : toHexString(Currency.unwrap(nativeKey.currency1)); + string memory id = uintToString(tokenId); + string memory hookAddress = address(nativeKey.hooks) == address(0) ? "No Hook" : string(abi.encodePacked("0x", toHexString(address(nativeKey.hooks)))); + string memory fee = Descriptor.feeToPercentString(nativeKey.fee); + string memory tickToDecimal0 = Descriptor.tickToDecimalString(tickLower, nativeKey.tickSpacing, SafeAddressMetadata.addressDecimals(Currency.unwrap(currency0)), SafeAddressMetadata.addressDecimals(Currency.unwrap(currency1)), false); + string memory tickToDecimal1 = Descriptor.tickToDecimalString(tickUpper, nativeKey.tickSpacing, SafeAddressMetadata.addressDecimals(Currency.unwrap(currency0)), SafeAddressMetadata.addressDecimals(Currency.unwrap(currency1)), false); + + assertEq(token.name, string(abi.encodePacked("Uniswap - ", fee, " - ", symbol1, "/", symbol0, " - ", tickToDecimal0, "<>", tickToDecimal1))); assertEq( token.description, - unicode"This NFT represents a liquidity position in a Uniswap v4 TEST-ETH pool. The owner of this NFT can modify or redeem the position.\n\nPool Manager Address: 0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f\nTEST Address: 0x5991a2df15a8f6a256d3ec51e99254cd3fb576a9\nETH Address: Native\nHook Address: 0x0000000000000000000000000000000000000000\nFee Tier: 0.3%\nToken ID: 1\n\n⚠️ DISCLAIMER: Due diligence is imperative when assessing this NFT. Make sure addresses match the expected addresses, as symbols may be imitated." - ); + string(abi.encodePacked(unicode"This NFT represents a liquidity position in a Uniswap v4 ", symbol1, "-", symbol0, " pool. The owner of this NFT can modify or redeem the position.\n\nPool Manager Address: ", managerAddress, "\n", symbol1, " Address: ", currency1Address, "\n", symbol0, " Address: ", currency0Address, "\nHook Address: ", hookAddress, "\nFee Tier: ", fee, "\nToken ID: ", id, "\n\n", unicode"⚠️ DISCLAIMER: Due diligence is imperative when assessing this NFT. Make sure addresses match the expected addresses, as symbols may be imitated.") + )); } function test_tokenURI_revertsWithInvalidTokenId() public { @@ -195,4 +224,42 @@ contract PositionDescriptorTest is Test, PosmTestSetup, GasSnapshot { positionDescriptor.tokenURI(lpm, tokenId + 1); } + + // Helper functions for testing purposes + function toHexString(address account) internal pure returns (string memory) { + return toHexString(uint256(uint160(account)), 20); + } + + // different from AddressStringUtil.toHexString. this one is all lowercase hex and includes the 0x prefix + function toHexString(uint256 value, uint256 length) internal pure returns (string memory) { + bytes memory buffer = new bytes(2 * length + 2); + buffer[0] = "0"; + buffer[1] = "x"; + for (uint256 i = 2 * length + 1; i > 1; --i) { + uint8 digit = uint8(value & 0xf); + buffer[i] = digit < 10 ? bytes1(digit + 48) : bytes1(digit + 87); // Lowercase hex (0x61 is 'a' in ASCII) + value >>= 4; + } + require(value == 0, "Hex length insufficient"); + return string(buffer); + } + + function uintToString(uint256 value) internal pure returns (string memory) { + if (value == 0) { + return "0"; + } + uint256 temp = value; + uint256 digits; + while (temp != 0) { + digits++; + temp /= 10; + } + bytes memory buffer = new bytes(digits); + while (value != 0) { + digits -= 1; + buffer[digits] = bytes1(uint8(48 + uint256(value % 10))); + value /= 10; + } + return string(buffer); + } } diff --git a/test/libraries/CalldataDecoder.t.sol b/test/libraries/CalldataDecoder.t.sol index 0a3fc737..de100667 100644 --- a/test/libraries/CalldataDecoder.t.sol +++ b/test/libraries/CalldataDecoder.t.sol @@ -103,7 +103,6 @@ contract CalldataDecoderTest is Test { assertEq(swapParams.zeroForOne, _swapParams.zeroForOne); assertEq(swapParams.amountIn, _swapParams.amountIn); assertEq(swapParams.amountOutMinimum, _swapParams.amountOutMinimum); - assertEq(swapParams.sqrtPriceLimitX96, _swapParams.sqrtPriceLimitX96); assertEq(swapParams.hookData, _swapParams.hookData); _assertEq(swapParams.poolKey, _swapParams.poolKey); } @@ -128,7 +127,6 @@ contract CalldataDecoderTest is Test { assertEq(swapParams.zeroForOne, _swapParams.zeroForOne); assertEq(swapParams.amountOut, _swapParams.amountOut); assertEq(swapParams.amountInMaximum, _swapParams.amountInMaximum); - assertEq(swapParams.sqrtPriceLimitX96, _swapParams.sqrtPriceLimitX96); assertEq(swapParams.hookData, _swapParams.hookData); _assertEq(swapParams.poolKey, _swapParams.poolKey); } @@ -234,6 +232,13 @@ contract CalldataDecoderTest is Test { assertEq(amount, _amount); } + function test_fuzz_decodeUint256(uint256 _amount) public { + bytes memory params = abi.encode(_amount); + uint256 amount = decoder.decodeUint256(params); + + assertEq(amount, _amount); + } + function _assertEq(PathKey[] memory path1, PathKey[] memory path2) internal pure { assertEq(path1.length, path2.length); for (uint256 i = 0; i < path1.length; i++) { diff --git a/test/libraries/Descriptor.t.sol b/test/libraries/Descriptor.t.sol index e191c5a5..2f3d5fd8 100644 --- a/test/libraries/Descriptor.t.sol +++ b/test/libraries/Descriptor.t.sol @@ -39,15 +39,20 @@ contract DescriptorTest is Test { ); } - function test_escapeQuotes_succeeds() public pure { - assertEq(Descriptor.escapeQuotes(""), ""); - assertEq(Descriptor.escapeQuotes("a"), "a"); - assertEq(Descriptor.escapeQuotes("abc"), "abc"); - assertEq(Descriptor.escapeQuotes("a\"bc"), "a\\\"bc"); - assertEq(Descriptor.escapeQuotes("a\"b\"c"), "a\\\"b\\\"c"); - assertEq(Descriptor.escapeQuotes("a\"b\"c\""), "a\\\"b\\\"c\\\""); - assertEq(Descriptor.escapeQuotes("\"a\"b\"c\""), "\\\"a\\\"b\\\"c\\\""); - assertEq(Descriptor.escapeQuotes("\"a\"b\"c\"\""), "\\\"a\\\"b\\\"c\\\"\\\""); + function test_escapeSpecialCharacters_succeeds() public pure { + assertEq(Descriptor.escapeSpecialCharacters(""), ""); + assertEq(Descriptor.escapeSpecialCharacters("a"), "a"); + assertEq(Descriptor.escapeSpecialCharacters("abc"), "abc"); + assertEq(Descriptor.escapeSpecialCharacters("a\"bc"), "a\\\"bc"); + assertEq(Descriptor.escapeSpecialCharacters("a\"b\"c"), "a\\\"b\\\"c"); + assertEq(Descriptor.escapeSpecialCharacters("a\"b\"c\""), "a\\\"b\\\"c\\\""); + assertEq(Descriptor.escapeSpecialCharacters("\"a\"b\"c\""), "\\\"a\\\"b\\\"c\\\""); + assertEq(Descriptor.escapeSpecialCharacters("\"a\"b\"c\"\""), "\\\"a\\\"b\\\"c\\\"\\\""); + + assertEq(Descriptor.escapeSpecialCharacters("a\rbc"), "a\\\rbc"); + assertEq(Descriptor.escapeSpecialCharacters("a\nbc"), "a\\\nbc"); + assertEq(Descriptor.escapeSpecialCharacters("a\tbc"), "a\\\tbc"); + assertEq(Descriptor.escapeSpecialCharacters("a\u000cbc"), "a\\\u000cbc"); } function test_tickToDecimalString_withTickSpacing10() public pure { diff --git a/test/mocks/MockCalldataDecoder.sol b/test/mocks/MockCalldataDecoder.sol index e25d8ec7..c61db4af 100644 --- a/test/mocks/MockCalldataDecoder.sol +++ b/test/mocks/MockCalldataDecoder.sol @@ -136,4 +136,8 @@ contract MockCalldataDecoder { { return params.decodeCurrencyAddressAndUint256(); } + + function decodeUint256(bytes calldata params) external pure returns (uint256) { + return params.decodeUint256(); + } } diff --git a/test/mocks/MockReenterHook.sol b/test/mocks/MockReenterHook.sol new file mode 100644 index 00000000..e6d6f2db --- /dev/null +++ b/test/mocks/MockReenterHook.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; +import {BaseTestHooks} from "@uniswap/v4-core/src/test/BaseTestHooks.sol"; +import {PositionManager} from "../../src/PositionManager.sol"; + +contract MockReenterHook is BaseTestHooks { + PositionManager posm; + + function beforeAddLiquidity( + address, + PoolKey calldata, + IPoolManager.ModifyLiquidityParams calldata, + bytes calldata functionSelector + ) external override returns (bytes4) { + if (functionSelector.length == 0) { + return this.beforeAddLiquidity.selector; + } + (bytes4 selector, address owner, uint256 tokenId) = abi.decode(functionSelector, (bytes4, address, uint256)); + + if (selector == posm.transferFrom.selector) { + posm.transferFrom(owner, address(this), tokenId); + } else if (selector == posm.subscribe.selector) { + posm.subscribe(tokenId, address(this), ""); + } else if (selector == posm.unsubscribe.selector) { + posm.unsubscribe(tokenId); + } + return this.beforeAddLiquidity.selector; + } + + function setPosm(PositionManager _posm) external { + posm = _posm; + } +} diff --git a/test/position-managers/PositionManager.modifyLiquidities.t.sol b/test/position-managers/PositionManager.modifyLiquidities.t.sol index e71c121f..c805dd27 100644 --- a/test/position-managers/PositionManager.modifyLiquidities.t.sol +++ b/test/position-managers/PositionManager.modifyLiquidities.t.sol @@ -2,6 +2,8 @@ pragma solidity ^0.8.24; import "forge-std/Test.sol"; +import {IERC20} from "forge-std/interfaces/IERC20.sol"; + import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; @@ -13,8 +15,11 @@ import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; import {Position} from "@uniswap/v4-core/src/libraries/Position.sol"; import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; +import {LiquidityAmounts} from "@uniswap/v4-core/test/utils/LiquidityAmounts.sol"; +import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; import {IPositionManager} from "../../src/interfaces/IPositionManager.sol"; +import {IMulticall_v4} from "../../src/interfaces/IMulticall_v4.sol"; import {ReentrancyLock} from "../../src/base/ReentrancyLock.sol"; import {Actions} from "../../src/libraries/Actions.sol"; import {PositionManager} from "../../src/PositionManager.sol"; @@ -23,10 +28,16 @@ import {PositionConfig} from "../shared/PositionConfig.sol"; import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; import {Planner, Plan} from "../shared/Planner.sol"; import {PosmTestSetup} from "../shared/PosmTestSetup.sol"; +import {ActionConstants} from "../../src/libraries/ActionConstants.sol"; +import {Planner, Plan} from "../shared/Planner.sol"; +import {DeltaResolver} from "../../src/base/DeltaResolver.sol"; + +import "forge-std/console2.sol"; contract PositionManagerModifyLiquiditiesTest is Test, PosmTestSetup, LiquidityFuzzers { using StateLibrary for IPoolManager; using PoolIdLibrary for PoolKey; + using Planner for Plan; PoolId poolId; address alice; @@ -34,6 +45,8 @@ contract PositionManagerModifyLiquiditiesTest is Test, PosmTestSetup, LiquidityF address bob; PositionConfig config; + PositionConfig wethConfig; + PositionConfig nativeConfig; function setUp() public { (alice, alicePK) = makeAddrAndKey("ALICE"); @@ -54,8 +67,23 @@ contract PositionManagerModifyLiquiditiesTest is Test, PosmTestSetup, LiquidityF seedBalance(address(hookModifyLiquidities)); (key, poolId) = initPool(currency0, currency1, IHooks(hookModifyLiquidities), 3000, SQRT_PRICE_1_1); + initWethPool(currency1, IHooks(address(0)), 3000, SQRT_PRICE_1_1); + + seedWeth(address(this)); + approvePosmCurrency(Currency.wrap(address(_WETH9))); + + nativeKey = PoolKey(CurrencyLibrary.ADDRESS_ZERO, currency1, 3000, 60, IHooks(address(0))); + manager.initialize(nativeKey, SQRT_PRICE_1_1); config = PositionConfig({poolKey: key, tickLower: -60, tickUpper: 60}); + wethConfig = PositionConfig({ + poolKey: wethKey, + tickLower: TickMath.minUsableTick(wethKey.tickSpacing), + tickUpper: TickMath.maxUsableTick(wethKey.tickSpacing) + }); + nativeConfig = PositionConfig({poolKey: nativeKey, tickLower: -120, tickUpper: 120}); + + vm.deal(address(this), 1000 ether); } /// @dev minting liquidity without approval is allowable @@ -308,4 +336,353 @@ contract PositionManagerModifyLiquiditiesTest is Test, PosmTestSetup, LiquidityF ); lpm.modifyLiquidities(calls, _deadline); } + + function test_wrap_mint_usingContractBalance() public { + // weth-currency1 pool initialized as wethKey + // input: eth, currency1 + // modifyLiquidities call to mint liquidity weth and currency1 + // 1 _wrap with contract balance + // 2 _mint + // 3 _settle weth where the payer is the contract + // 4 _close currency1, payer is caller + // 5 _sweep weth since eth was entirely wrapped + + uint256 balanceEthBefore = address(this).balance; + uint256 balance1Before = IERC20(Currency.unwrap(currency1)).balanceOf(address(this)); + uint256 tokenId = lpm.nextTokenId(); + + uint128 liquidityAmount = LiquidityAmounts.getLiquidityForAmounts( + SQRT_PRICE_1_1, + TickMath.getSqrtPriceAtTick(wethConfig.tickLower), + TickMath.getSqrtPriceAtTick(wethConfig.tickUpper), + 100 ether, + 100 ether + ); + + Plan memory planner = Planner.init(); + planner.add(Actions.WRAP, abi.encode(ActionConstants.CONTRACT_BALANCE)); + planner.add( + Actions.MINT_POSITION, + abi.encode( + wethConfig.poolKey, + wethConfig.tickLower, + wethConfig.tickUpper, + liquidityAmount, + MAX_SLIPPAGE_INCREASE, + MAX_SLIPPAGE_INCREASE, + ActionConstants.MSG_SENDER, + ZERO_BYTES + ) + ); + + // weth9 payer is the contract + planner.add(Actions.SETTLE, abi.encode(address(_WETH9), ActionConstants.OPEN_DELTA, false)); + // other currency can close normally + planner.add(Actions.CLOSE_CURRENCY, abi.encode(currency1)); + // we wrapped the full contract balance so we sweep back in the wrapped currency + planner.add(Actions.SWEEP, abi.encode(address(_WETH9), ActionConstants.MSG_SENDER)); + bytes memory actions = planner.encode(); + + // Overestimate eth amount. + lpm.modifyLiquidities{value: 102 ether}(actions, _deadline); + + uint256 balanceEthAfter = address(this).balance; + uint256 balance1After = IERC20(Currency.unwrap(currency1)).balanceOf(address(this)); + + // The full eth amount was "spent" because some was wrapped into weth and refunded. + assertApproxEqAbs(balanceEthBefore - balanceEthAfter, 102 ether, 1 wei); + assertApproxEqAbs(balance1Before - balance1After, 100 ether, 1 wei); + assertEq(lpm.ownerOf(tokenId), address(this)); + assertEq(lpm.getPositionLiquidity(tokenId), liquidityAmount); + assertEq(_WETH9.balanceOf(address(lpm)), 0); + assertEq(address(lpm).balance, 0); + } + + function test_wrap_mint_openDelta() public { + // weth-currency1 pool initialized as wethKey + // input: eth, currency1 + // modifyLiquidities call to mint liquidity weth and currency1 + // 1 _mint + // 2 _wrap with open delta + // 3 _settle weth where the payer is the contract + // 4 _close currency1, payer is caller + // 5 _sweep eth since only the open delta amount was wrapped + + uint256 balanceEthBefore = address(this).balance; + uint256 balance1Before = IERC20(Currency.unwrap(currency1)).balanceOf(address(this)); + uint256 tokenId = lpm.nextTokenId(); + + uint128 liquidityAmount = LiquidityAmounts.getLiquidityForAmounts( + SQRT_PRICE_1_1, + TickMath.getSqrtPriceAtTick(wethConfig.tickLower), + TickMath.getSqrtPriceAtTick(wethConfig.tickUpper), + 100 ether, + 100 ether + ); + + Plan memory planner = Planner.init(); + + planner.add( + Actions.MINT_POSITION, + abi.encode( + wethConfig.poolKey, + wethConfig.tickLower, + wethConfig.tickUpper, + liquidityAmount, + MAX_SLIPPAGE_INCREASE, + MAX_SLIPPAGE_INCREASE, + ActionConstants.MSG_SENDER, + ZERO_BYTES + ) + ); + + planner.add(Actions.WRAP, abi.encode(ActionConstants.OPEN_DELTA)); + + // weth9 payer is the contract + planner.add(Actions.SETTLE, abi.encode(address(_WETH9), ActionConstants.OPEN_DELTA, false)); + // other currency can close normally + planner.add(Actions.CLOSE_CURRENCY, abi.encode(currency1)); + // we wrapped the open delta balance so we sweep back in the native currency + planner.add(Actions.SWEEP, abi.encode(CurrencyLibrary.ADDRESS_ZERO, ActionConstants.MSG_SENDER)); + bytes memory actions = planner.encode(); + + lpm.modifyLiquidities{value: 102 ether}(actions, _deadline); + + uint256 balanceEthAfter = address(this).balance; + uint256 balance1After = IERC20(Currency.unwrap(currency1)).balanceOf(address(this)); + + // Approx 100 eth was spent because the extra 2 were refunded. + assertApproxEqAbs(balanceEthBefore - balanceEthAfter, 100 ether, 1 wei); + assertApproxEqAbs(balance1Before - balance1After, 100 ether, 1 wei); + assertEq(lpm.ownerOf(tokenId), address(this)); + assertEq(lpm.getPositionLiquidity(tokenId), liquidityAmount); + assertEq(_WETH9.balanceOf(address(lpm)), 0); + assertEq(address(lpm).balance, 0); + } + + function test_wrap_mint_usingExactAmount() public { + // weth-currency1 pool initialized as wethKey + // input: eth, currency1 + // modifyLiquidities call to mint liquidity weth and currency1 + // 1 _wrap with an amount + // 2 _mint + // 3 _settle weth where the payer is the contract + // 4 _close currency1, payer is caller + // 5 _sweep weth since eth was entirely wrapped + + uint256 balanceEthBefore = address(this).balance; + uint256 balance1Before = IERC20(Currency.unwrap(currency1)).balanceOf(address(this)); + uint256 tokenId = lpm.nextTokenId(); + + uint128 liquidityAmount = LiquidityAmounts.getLiquidityForAmounts( + SQRT_PRICE_1_1, + TickMath.getSqrtPriceAtTick(wethConfig.tickLower), + TickMath.getSqrtPriceAtTick(wethConfig.tickUpper), + 100 ether, + 100 ether + ); + + Plan memory planner = Planner.init(); + planner.add(Actions.WRAP, abi.encode(100 ether)); + planner.add( + Actions.MINT_POSITION, + abi.encode( + wethConfig.poolKey, + wethConfig.tickLower, + wethConfig.tickUpper, + liquidityAmount, + MAX_SLIPPAGE_INCREASE, + MAX_SLIPPAGE_INCREASE, + ActionConstants.MSG_SENDER, + ZERO_BYTES + ) + ); + + // weth9 payer is the contract + planner.add(Actions.SETTLE, abi.encode(address(_WETH9), ActionConstants.OPEN_DELTA, false)); + // other currency can close normally + planner.add(Actions.CLOSE_CURRENCY, abi.encode(currency1)); + // we wrapped all 100 eth so we sweep back in the wrapped currency for safety measure + planner.add(Actions.SWEEP, abi.encode(address(_WETH9), ActionConstants.MSG_SENDER)); + bytes memory actions = planner.encode(); + + lpm.modifyLiquidities{value: 100 ether}(actions, _deadline); + + uint256 balanceEthAfter = address(this).balance; + uint256 balance1After = IERC20(Currency.unwrap(currency1)).balanceOf(address(this)); + + // The full eth amount was "spent" because some was wrapped into weth and refunded. + assertApproxEqAbs(balanceEthBefore - balanceEthAfter, 100 ether, 1 wei); + assertApproxEqAbs(balance1Before - balance1After, 100 ether, 1 wei); + assertEq(lpm.ownerOf(tokenId), address(this)); + assertEq(lpm.getPositionLiquidity(tokenId), liquidityAmount); + assertEq(_WETH9.balanceOf(address(lpm)), 0); + assertEq(address(lpm).balance, 0); + } + + function test_wrap_mint_revertsInsufficientBalance() public { + // 1 _wrap with more eth than is sent in + + Plan memory planner = Planner.init(); + // Wrap more eth than what is sent in. + planner.add(Actions.WRAP, abi.encode(101 ether)); + + bytes memory actions = planner.encode(); + + vm.expectRevert(DeltaResolver.InsufficientBalance.selector); + lpm.modifyLiquidities{value: 100 ether}(actions, _deadline); + } + + function test_unwrap_usingContractBalance() public { + // weth-currency1 pool + // output: eth, currency1 + // modifyLiquidities call to mint liquidity weth and currency1 + // 1 _burn + // 2 _take where the weth is sent to the lpm contract + // 3 _take where currency1 is sent to the msg sender + // 4 _unwrap using contract balance + // 5 _sweep where eth is sent to msg sender + uint256 tokenId = lpm.nextTokenId(); + + uint128 liquidityAmount = LiquidityAmounts.getLiquidityForAmounts( + SQRT_PRICE_1_1, + TickMath.getSqrtPriceAtTick(wethConfig.tickLower), + TickMath.getSqrtPriceAtTick(wethConfig.tickUpper), + 100 ether, + 100 ether + ); + + bytes memory actions = getMintEncoded(wethConfig, liquidityAmount, address(this), ZERO_BYTES); + lpm.modifyLiquidities(actions, _deadline); + + assertEq(lpm.getPositionLiquidity(tokenId), liquidityAmount); + + uint256 balanceEthBefore = address(this).balance; + uint256 balance1Before = IERC20(Currency.unwrap(currency1)).balanceOf(address(this)); + + Plan memory planner = Planner.init(); + planner.add( + Actions.BURN_POSITION, abi.encode(tokenId, MIN_SLIPPAGE_DECREASE, MIN_SLIPPAGE_DECREASE, ZERO_BYTES) + ); + // take the weth to the position manager to be unwrapped + planner.add(Actions.TAKE, abi.encode(address(_WETH9), ActionConstants.ADDRESS_THIS, ActionConstants.OPEN_DELTA)); + planner.add( + Actions.TAKE, + abi.encode(address(Currency.unwrap(currency1)), ActionConstants.MSG_SENDER, ActionConstants.OPEN_DELTA) + ); + planner.add(Actions.UNWRAP, abi.encode(ActionConstants.CONTRACT_BALANCE)); + planner.add(Actions.SWEEP, abi.encode(CurrencyLibrary.ADDRESS_ZERO, ActionConstants.MSG_SENDER)); + + actions = planner.encode(); + + lpm.modifyLiquidities(actions, _deadline); + + uint256 balanceEthAfter = address(this).balance; + uint256 balance1After = IERC20(Currency.unwrap(currency1)).balanceOf(address(this)); + + assertApproxEqAbs(balanceEthAfter - balanceEthBefore, 100 ether, 1 wei); + assertApproxEqAbs(balance1After - balance1Before, 100 ether, 1 wei); + assertEq(lpm.getPositionLiquidity(tokenId), 0); + assertEq(_WETH9.balanceOf(address(lpm)), 0); + assertEq(address(lpm).balance, 0); + } + + function test_unwrap_openDelta_reinvest() public { + // weth-currency1 pool rolls half to eth-currency1 pool + // output: eth, currency1 + // modifyLiquidities call to mint liquidity weth and currency1 + // 1 _burn (weth-currency1) + // 2 _take where the weth is sent to the lpm contract + // 4 _mint to an eth pool + // 4 _unwrap using open delta (pool managers ETH balance) + // 3 _take where leftover currency1 is sent to the msg sender + // 5 _settle eth open delta + // 5 _sweep leftover weth + + uint256 tokenId = lpm.nextTokenId(); + + uint128 liquidityAmount = LiquidityAmounts.getLiquidityForAmounts( + SQRT_PRICE_1_1, + TickMath.getSqrtPriceAtTick(wethConfig.tickLower), + TickMath.getSqrtPriceAtTick(wethConfig.tickUpper), + 100 ether, + 100 ether + ); + + bytes memory actions = getMintEncoded(wethConfig, liquidityAmount, address(this), ZERO_BYTES); + lpm.modifyLiquidities(actions, _deadline); + + assertEq(lpm.getPositionLiquidity(tokenId), liquidityAmount); + + uint256 balanceEthBefore = address(this).balance; + uint256 balance1Before = IERC20(Currency.unwrap(currency1)).balanceOf(address(this)); + uint256 balanceWethBefore = _WETH9.balanceOf(address(this)); + + uint128 newLiquidityAmount = LiquidityAmounts.getLiquidityForAmounts( + SQRT_PRICE_1_1, + TickMath.getSqrtPriceAtTick(nativeConfig.tickLower), + TickMath.getSqrtPriceAtTick(nativeConfig.tickUpper), + 50 ether, + 50 ether + ); + + Plan memory planner = Planner.init(); + planner.add( + Actions.BURN_POSITION, abi.encode(tokenId, MIN_SLIPPAGE_DECREASE, MIN_SLIPPAGE_DECREASE, ZERO_BYTES) + ); + // take the weth to the position manager to be unwrapped + planner.add(Actions.TAKE, abi.encode(address(_WETH9), ActionConstants.ADDRESS_THIS, ActionConstants.OPEN_DELTA)); + planner.add( + Actions.MINT_POSITION, + abi.encode( + nativeConfig.poolKey, + nativeConfig.tickLower, + nativeConfig.tickUpper, + newLiquidityAmount, + MAX_SLIPPAGE_INCREASE, + MAX_SLIPPAGE_INCREASE, + ActionConstants.MSG_SENDER, + ZERO_BYTES + ) + ); + planner.add(Actions.UNWRAP, abi.encode(ActionConstants.OPEN_DELTA)); + // pay the eth + planner.add(Actions.SETTLE, abi.encode(CurrencyLibrary.ADDRESS_ZERO, ActionConstants.OPEN_DELTA, false)); + // take the leftover currency1 + planner.add( + Actions.TAKE, + abi.encode(address(Currency.unwrap(currency1)), ActionConstants.MSG_SENDER, ActionConstants.OPEN_DELTA) + ); + planner.add(Actions.SWEEP, abi.encode(address(_WETH9), ActionConstants.MSG_SENDER)); + + actions = planner.encode(); + + lpm.modifyLiquidities(actions, _deadline); + + uint256 balanceEthAfter = address(this).balance; + uint256 balance1After = IERC20(Currency.unwrap(currency1)).balanceOf(address(this)); + uint256 balanceWethAfter = _WETH9.balanceOf(address(this)); + + // Eth balance should not change. + assertEq(balanceEthAfter, balanceEthBefore); + // Only half of the original liquidity was reinvested. + assertApproxEqAbs(balance1After - balance1Before, 50 ether, 1 wei); + assertApproxEqAbs(balanceWethAfter - balanceWethBefore, 50 ether, 1 wei); + assertEq(lpm.getPositionLiquidity(tokenId), 0); + assertEq(_WETH9.balanceOf(address(lpm)), 0); + assertEq(address(lpm).balance, 0); + } + + function test_unwrap_revertsInsufficientBalance() public { + // 1 _unwrap with more than is in the contract + + Plan memory planner = Planner.init(); + // unwraps more eth than what is in the contract + planner.add(Actions.UNWRAP, abi.encode(101 ether)); + + bytes memory actions = planner.encode(); + + vm.expectRevert(DeltaResolver.InsufficientBalance.selector); + lpm.modifyLiquidities(actions, _deadline); + } } diff --git a/test/position-managers/PositionManager.notifier.t.sol b/test/position-managers/PositionManager.notifier.t.sol index 391446a8..74acc7d0 100644 --- a/test/position-managers/PositionManager.notifier.t.sol +++ b/test/position-managers/PositionManager.notifier.t.sol @@ -9,6 +9,7 @@ import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; import {PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; import {PosmTestSetup} from "../shared/PosmTestSetup.sol"; import {MockSubscriber} from "../mocks/MockSubscriber.sol"; @@ -20,6 +21,7 @@ import {Actions} from "../../src/libraries/Actions.sol"; import {INotifier} from "../../src/interfaces/INotifier.sol"; import {MockReturnDataSubscriber, MockRevertSubscriber} from "../mocks/MockBadSubscribers.sol"; import {PositionInfoLibrary, PositionInfo} from "../../src/libraries/PositionInfoLibrary.sol"; +import {MockReenterHook} from "../mocks/MockReenterHook.sol"; contract PositionManagerNotifierTest is Test, PosmTestSetup, GasSnapshot { using PoolIdLibrary for PoolKey; @@ -31,10 +33,13 @@ contract PositionManagerNotifierTest is Test, PosmTestSetup, GasSnapshot { MockReturnDataSubscriber badSubscriber; PositionConfig config; MockRevertSubscriber revertSubscriber; + MockReenterHook reenterHook; address alice = makeAddr("ALICE"); address bob = makeAddr("BOB"); + PositionConfig reenterConfig; + function setUp() public { deployFreshManagerAndRouters(); deployMintAndApprove2Currencies(); @@ -49,6 +54,17 @@ contract PositionManagerNotifierTest is Test, PosmTestSetup, GasSnapshot { revertSubscriber = new MockRevertSubscriber(lpm); config = PositionConfig({poolKey: key, tickLower: -300, tickUpper: 300}); + // set the reenter hook + MockReenterHook impl = new MockReenterHook(); + address hookAddr = payable(address(uint160(Hooks.BEFORE_ADD_LIQUIDITY_FLAG))); + vm.etch(hookAddr, address(impl).code); + reenterHook = MockReenterHook(hookAddr); + reenterHook.setPosm(lpm); + + PoolKey memory reenterKey = PoolKey(currency0, currency1, 3000, 60, IHooks(reenterHook)); + manager.initialize(reenterKey, SQRT_PRICE_1_1); + reenterConfig = PositionConfig({poolKey: reenterKey, tickLower: -60, tickUpper: 60}); + // TODO: Test NATIVE poolKey } @@ -647,4 +663,69 @@ contract PositionManagerNotifierTest is Test, PosmTestSetup, GasSnapshot { assertEq(sub.notifyUnsubscribeCount(), beforeUnsubCount + 1); } } + + function test_unsubscribe_reverts_PoolManagerMustBeLocked() public { + uint256 tokenId = lpm.nextTokenId(); + mint(reenterConfig, 10e18, address(this), ZERO_BYTES); + + bytes memory hookData = abi.encode(lpm.unsubscribe.selector, address(this), tokenId); + bytes memory actions = getMintEncoded(reenterConfig, 10e18, address(this), hookData); + + // approve hook as it should not revert because it does not have permissions + lpm.approve(address(reenterHook), tokenId); + // subscribe as it should not revert because there is no subscriber + lpm.subscribe(tokenId, address(sub), ZERO_BYTES); + + // should revert since the pool manager is unlocked + vm.expectRevert( + abi.encodeWithSelector( + Hooks.Wrap__FailedHookCall.selector, + address(reenterHook), + abi.encodeWithSelector(IPositionManager.PoolManagerMustBeLocked.selector) + ) + ); + lpm.modifyLiquidities(actions, _deadline); + } + + function test_subscribe_reverts_PoolManagerMustBeLocked() public { + uint256 tokenId = lpm.nextTokenId(); + mint(reenterConfig, 10e18, address(this), ZERO_BYTES); + + bytes memory hookData = abi.encode(lpm.subscribe.selector, address(this), tokenId); + bytes memory actions = getMintEncoded(reenterConfig, 10e18, address(this), hookData); + + // approve hook as it should not revert because it does not have permissions + lpm.approve(address(reenterHook), tokenId); + + // should revert since the pool manager is unlocked + vm.expectRevert( + abi.encodeWithSelector( + Hooks.Wrap__FailedHookCall.selector, + address(reenterHook), + abi.encodeWithSelector(IPositionManager.PoolManagerMustBeLocked.selector) + ) + ); + lpm.modifyLiquidities(actions, _deadline); + } + + function test_transferFrom_reverts_PoolManagerMustBeLocked() public { + uint256 tokenId = lpm.nextTokenId(); + mint(reenterConfig, 10e18, address(this), ZERO_BYTES); + + bytes memory hookData = abi.encode(lpm.transferFrom.selector, address(this), tokenId); + bytes memory actions = getMintEncoded(reenterConfig, 10e18, address(this), hookData); + + // approve hook as it should not revert because it does not have permissions + lpm.approve(address(reenterHook), tokenId); + + // should revert since the pool manager is unlocked + vm.expectRevert( + abi.encodeWithSelector( + Hooks.Wrap__FailedHookCall.selector, + address(reenterHook), + abi.encodeWithSelector(IPositionManager.PoolManagerMustBeLocked.selector) + ) + ); + lpm.modifyLiquidities(actions, _deadline); + } } diff --git a/test/router/Payments.gas.t.sol b/test/router/Payments.gas.t.sol index 9717fdad..2e42a936 100644 --- a/test/router/Payments.gas.t.sol +++ b/test/router/Payments.gas.t.sol @@ -22,7 +22,7 @@ contract PaymentsTests is RoutingTestHelpers, GasSnapshot { function test_gas_swap_settleFromCaller_takeAllToSpecifiedAddress() public { uint256 amountIn = 1 ether; IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, 0, bytes("")); + IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, bytes("")); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); plan = plan.add(Actions.SETTLE_ALL, abi.encode(key0.currency0, MAX_SETTLE_AMOUNT)); @@ -36,7 +36,7 @@ contract PaymentsTests is RoutingTestHelpers, GasSnapshot { function test_gas_swap_settleFromCaller_takeAllToMsgSender() public { uint256 amountIn = 1 ether; IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, 0, bytes("")); + IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, bytes("")); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); plan = plan.add(Actions.SETTLE_TAKE_PAIR, abi.encode(key0.currency0, key0.currency1)); @@ -49,7 +49,7 @@ contract PaymentsTests is RoutingTestHelpers, GasSnapshot { function test_gas_swap_settleWithBalance_takeAllToSpecifiedAddress() public { uint256 amountIn = 1 ether; IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, 0, bytes("")); + IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, bytes("")); // seed the router with tokens key0.currency0.transfer(address(router), amountIn); @@ -66,7 +66,7 @@ contract PaymentsTests is RoutingTestHelpers, GasSnapshot { function test_gas_swap_settleWithBalance_takeAllToMsgSender() public { uint256 amountIn = 1 ether; IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, 0, bytes("")); + IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, bytes("")); // seed the router with tokens key0.currency0.transfer(address(router), amountIn); diff --git a/test/router/Payments.t.sol b/test/router/Payments.t.sol index 3b1b2c32..29442f9c 100644 --- a/test/router/Payments.t.sol +++ b/test/router/Payments.t.sol @@ -26,7 +26,7 @@ contract PaymentsTests is RoutingTestHelpers, GasSnapshot { uint256 amountIn = 1 ether; uint256 expectedAmountOut = 992054607780215625; IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, 0, bytes("")); + IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, bytes("")); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); plan = plan.add(Actions.SETTLE_TAKE_PAIR, abi.encode(key0.currency0, key0.currency1)); @@ -54,7 +54,7 @@ contract PaymentsTests is RoutingTestHelpers, GasSnapshot { function test_exactIn_settleAll_revertsSlippage() public { uint256 amountIn = 1 ether; IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, 0, bytes("")); + IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, bytes("")); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); plan = plan.add(Actions.SETTLE_ALL, abi.encode(key0.currency0, amountIn - 1)); @@ -69,7 +69,7 @@ contract PaymentsTests is RoutingTestHelpers, GasSnapshot { uint256 amountIn = 1 ether; uint256 expectedAmountOut = 992054607780215625; IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, 0, bytes("")); + IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, bytes("")); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); plan = plan.add(Actions.SETTLE_ALL, abi.encode(key0.currency0, MAX_SETTLE_AMOUNT)); @@ -87,7 +87,7 @@ contract PaymentsTests is RoutingTestHelpers, GasSnapshot { uint256 expectedAmountIn = 1008049273448486163; IV4Router.ExactOutputSingleParams memory params = - IV4Router.ExactOutputSingleParams(key0, true, uint128(amountOut), uint128(expectedAmountIn), 0, bytes("")); + IV4Router.ExactOutputSingleParams(key0, true, uint128(amountOut), uint128(expectedAmountIn), bytes("")); plan = plan.add(Actions.SWAP_EXACT_OUT_SINGLE, abi.encode(params)); plan = plan.add(Actions.SETTLE_ALL, abi.encode(key0.currency0, expectedAmountIn - 1)); @@ -105,7 +105,7 @@ contract PaymentsTests is RoutingTestHelpers, GasSnapshot { uint256 expectedAmountIn = 1008049273448486163; IV4Router.ExactOutputSingleParams memory params = - IV4Router.ExactOutputSingleParams(key0, true, uint128(amountOut), uint128(expectedAmountIn), 0, bytes("")); + IV4Router.ExactOutputSingleParams(key0, true, uint128(amountOut), uint128(expectedAmountIn), bytes("")); plan = plan.add(Actions.SWAP_EXACT_OUT_SINGLE, abi.encode(params)); plan = plan.add(Actions.SETTLE_ALL, abi.encode(key0.currency0, MAX_SETTLE_AMOUNT)); @@ -121,7 +121,7 @@ contract PaymentsTests is RoutingTestHelpers, GasSnapshot { uint256 expectedAmountIn = 1008049273448486163; IV4Router.ExactOutputSingleParams memory params = - IV4Router.ExactOutputSingleParams(key0, true, uint128(amountOut), uint128(expectedAmountIn), 0, bytes("")); + IV4Router.ExactOutputSingleParams(key0, true, uint128(amountOut), uint128(expectedAmountIn), bytes("")); plan = plan.add(Actions.SWAP_EXACT_OUT_SINGLE, abi.encode(params)); plan = plan.add(Actions.SETTLE_ALL, abi.encode(key0.currency0, expectedAmountIn)); @@ -135,7 +135,7 @@ contract PaymentsTests is RoutingTestHelpers, GasSnapshot { uint256 amountIn = 1 ether; uint256 expectedAmountOut = 992054607780215625; IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, 0, bytes("")); + IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, bytes("")); // seed the router with tokens key0.currency0.transfer(address(router), amountIn); @@ -169,7 +169,7 @@ contract PaymentsTests is RoutingTestHelpers, GasSnapshot { uint256 amountIn = 1 ether; uint256 expectedAmountOut = 992054607780215625; IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, 0, bytes("")); + IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, bytes("")); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); // take 15 bips to Bob @@ -205,7 +205,7 @@ contract PaymentsTests is RoutingTestHelpers, GasSnapshot { function test_settle_takePortion_reverts() public { uint256 amountIn = 1 ether; IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, 0, bytes("")); + IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, bytes("")); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); // bips is larger than maximum bips diff --git a/test/router/V4Router.gas.t.sol b/test/router/V4Router.gas.t.sol index 8d3eac92..c49f96dc 100644 --- a/test/router/V4Router.gas.t.sol +++ b/test/router/V4Router.gas.t.sol @@ -31,7 +31,7 @@ contract V4RouterTest is RoutingTestHelpers, GasSnapshot { uint256 amountIn = 1 ether; IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, 0, bytes("")); + IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, bytes("")); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); bytes memory data = plan.finalizeSwap(key0.currency0, key0.currency1, ActionConstants.MSG_SENDER); @@ -107,7 +107,7 @@ contract V4RouterTest is RoutingTestHelpers, GasSnapshot { uint256 amountIn = 1 ether; IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(nativeKey, true, uint128(amountIn), 0, 0, bytes("")); + IV4Router.ExactInputSingleParams(nativeKey, true, uint128(amountIn), 0, bytes("")); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); bytes memory data = plan.finalizeSwap(nativeKey.currency0, nativeKey.currency1, ActionConstants.MSG_SENDER); @@ -120,7 +120,7 @@ contract V4RouterTest is RoutingTestHelpers, GasSnapshot { uint256 amountIn = 1 ether; IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(nativeKey, false, uint128(amountIn), 0, 0, bytes("")); + IV4Router.ExactInputSingleParams(nativeKey, false, uint128(amountIn), 0, bytes("")); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); bytes memory data = plan.finalizeSwap(nativeKey.currency1, nativeKey.currency0, ActionConstants.MSG_SENDER); @@ -196,7 +196,7 @@ contract V4RouterTest is RoutingTestHelpers, GasSnapshot { uint256 amountOut = 1 ether; IV4Router.ExactOutputSingleParams memory params = - IV4Router.ExactOutputSingleParams(key0, true, uint128(amountOut), type(uint128).max, 0, bytes("")); + IV4Router.ExactOutputSingleParams(key0, true, uint128(amountOut), type(uint128).max, bytes("")); plan = plan.add(Actions.SWAP_EXACT_OUT_SINGLE, abi.encode(params)); bytes memory data = plan.finalizeSwap(key0.currency0, key0.currency1, ActionConstants.MSG_SENDER); @@ -272,7 +272,7 @@ contract V4RouterTest is RoutingTestHelpers, GasSnapshot { uint256 amountOut = 1 ether; IV4Router.ExactOutputSingleParams memory params = - IV4Router.ExactOutputSingleParams(nativeKey, true, uint128(amountOut), type(uint128).max, 0, bytes("")); + IV4Router.ExactOutputSingleParams(nativeKey, true, uint128(amountOut), type(uint128).max, bytes("")); plan = plan.add(Actions.SWAP_EXACT_OUT_SINGLE, abi.encode(params)); bytes memory data = plan.finalizeSwap(nativeKey.currency0, nativeKey.currency1, ActionConstants.MSG_SENDER); @@ -285,7 +285,7 @@ contract V4RouterTest is RoutingTestHelpers, GasSnapshot { uint256 amountOut = 1 ether; IV4Router.ExactOutputSingleParams memory params = - IV4Router.ExactOutputSingleParams(nativeKey, false, uint128(amountOut), type(uint128).max, 0, bytes("")); + IV4Router.ExactOutputSingleParams(nativeKey, false, uint128(amountOut), type(uint128).max, bytes("")); plan = plan.add(Actions.SWAP_EXACT_OUT_SINGLE, abi.encode(params)); bytes memory data = plan.finalizeSwap(nativeKey.currency1, nativeKey.currency0, ActionConstants.MSG_SENDER); diff --git a/test/router/V4Router.t.sol b/test/router/V4Router.t.sol index e32da052..0c74e004 100644 --- a/test/router/V4Router.t.sol +++ b/test/router/V4Router.t.sol @@ -28,9 +28,8 @@ contract V4RouterTest is RoutingTestHelpers { uint256 expectedAmountOut = 992054607780215625; // min amount out of 1 higher than the actual amount out - IV4Router.ExactInputSingleParams memory params = IV4Router.ExactInputSingleParams( - key0, true, uint128(amountIn), uint128(expectedAmountOut + 1), 0, bytes("") - ); + IV4Router.ExactInputSingleParams memory params = + IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), uint128(expectedAmountOut + 1), bytes("")); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); bytes memory data = plan.finalizeSwap(key0.currency0, key0.currency1, ActionConstants.MSG_SENDER); @@ -46,7 +45,7 @@ contract V4RouterTest is RoutingTestHelpers { uint256 expectedAmountOut = 992054607780215625; IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, 0, bytes("")); + IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, bytes("")); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); (uint256 inputBalanceBefore, uint256 outputBalanceBefore, uint256 inputBalanceAfter, uint256 outputBalanceAfter) @@ -64,7 +63,7 @@ contract V4RouterTest is RoutingTestHelpers { uint256 expectedAmountOut = 992054607780215625; IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, 0, bytes("")); + IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, bytes("")); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); @@ -92,7 +91,7 @@ contract V4RouterTest is RoutingTestHelpers { uint256 expectedAmountOut = 992054607780215625; IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, 0, bytes("")); + IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, bytes("")); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); @@ -120,7 +119,7 @@ contract V4RouterTest is RoutingTestHelpers { uint256 expectedAmountOut = 992054607780215625; IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, 0, bytes("")); + IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, bytes("")); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); plan = plan.add(Actions.SETTLE_ALL, abi.encode(key0.currency0, expectedAmountOut * 12 / 10)); @@ -153,7 +152,7 @@ contract V4RouterTest is RoutingTestHelpers { uint256 expectedAmountOut = 992054607780215625; IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(key0, false, uint128(amountIn), 0, 0, bytes("")); + IV4Router.ExactInputSingleParams(key0, false, uint128(amountIn), 0, bytes("")); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); (uint256 inputBalanceBefore, uint256 outputBalanceBefore, uint256 inputBalanceAfter, uint256 outputBalanceAfter) @@ -282,7 +281,7 @@ contract V4RouterTest is RoutingTestHelpers { // amount in of 0 to show it should use the open delta IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(key0, true, ActionConstants.OPEN_DELTA, 0, 0, bytes("")); + IV4Router.ExactInputSingleParams(key0, true, ActionConstants.OPEN_DELTA, 0, bytes("")); plan = plan.add(Actions.SETTLE, abi.encode(key0.currency0, ActionConstants.CONTRACT_BALANCE, false)); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); @@ -314,7 +313,7 @@ contract V4RouterTest is RoutingTestHelpers { uint256 expectedAmountOut = 992054607780215625; IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(nativeKey, true, uint128(amountIn), 0, 0, bytes("")); + IV4Router.ExactInputSingleParams(nativeKey, true, uint128(amountIn), 0, bytes("")); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); @@ -334,7 +333,7 @@ contract V4RouterTest is RoutingTestHelpers { // native output means we need !zeroForOne IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(nativeKey, false, uint128(amountIn), 0, 0, bytes("")); + IV4Router.ExactInputSingleParams(nativeKey, false, uint128(amountIn), 0, bytes("")); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); @@ -450,7 +449,7 @@ contract V4RouterTest is RoutingTestHelpers { // amount in of 0 to show it should use the open delta IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(nativeKey, true, ActionConstants.OPEN_DELTA, 0, 0, bytes("")); + IV4Router.ExactInputSingleParams(nativeKey, true, ActionConstants.OPEN_DELTA, 0, bytes("")); plan = plan.add(Actions.SETTLE, abi.encode(nativeKey.currency0, ActionConstants.CONTRACT_BALANCE, false)); plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); @@ -481,9 +480,8 @@ contract V4RouterTest is RoutingTestHelpers { uint256 amountOut = 1 ether; uint256 expectedAmountIn = 1008049273448486163; - IV4Router.ExactOutputSingleParams memory params = IV4Router.ExactOutputSingleParams( - key0, true, uint128(amountOut), uint128(expectedAmountIn - 1), 0, bytes("") - ); + IV4Router.ExactOutputSingleParams memory params = + IV4Router.ExactOutputSingleParams(key0, true, uint128(amountOut), uint128(expectedAmountIn - 1), bytes("")); plan = plan.add(Actions.SWAP_EXACT_OUT_SINGLE, abi.encode(params)); bytes memory data = plan.finalizeSwap(key0.currency0, key0.currency1, ActionConstants.MSG_SENDER); @@ -498,9 +496,8 @@ contract V4RouterTest is RoutingTestHelpers { uint256 amountOut = 1 ether; uint256 expectedAmountIn = 1008049273448486163; - IV4Router.ExactOutputSingleParams memory params = IV4Router.ExactOutputSingleParams( - key0, true, uint128(amountOut), uint128(expectedAmountIn + 1), 0, bytes("") - ); + IV4Router.ExactOutputSingleParams memory params = + IV4Router.ExactOutputSingleParams(key0, true, uint128(amountOut), uint128(expectedAmountIn + 1), bytes("")); plan = plan.add(Actions.SWAP_EXACT_OUT_SINGLE, abi.encode(params)); @@ -518,9 +515,8 @@ contract V4RouterTest is RoutingTestHelpers { uint256 amountOut = 1 ether; uint256 expectedAmountIn = 1008049273448486163; - IV4Router.ExactOutputSingleParams memory params = IV4Router.ExactOutputSingleParams( - key0, false, uint128(amountOut), uint128(expectedAmountIn + 1), 0, bytes("") - ); + IV4Router.ExactOutputSingleParams memory params = + IV4Router.ExactOutputSingleParams(key0, false, uint128(amountOut), uint128(expectedAmountIn + 1), bytes("")); plan = plan.add(Actions.SWAP_EXACT_OUT_SINGLE, abi.encode(params)); @@ -538,7 +534,7 @@ contract V4RouterTest is RoutingTestHelpers { uint256 expectedAmountIn = 1008049273448486163; IV4Router.ExactOutputSingleParams memory params = IV4Router.ExactOutputSingleParams( - key0, true, ActionConstants.OPEN_DELTA, uint128(expectedAmountIn + 1), 0, bytes("") + key0, true, ActionConstants.OPEN_DELTA, uint128(expectedAmountIn + 1), bytes("") ); plan = plan.add(Actions.TAKE, abi.encode(key0.currency1, ActionConstants.ADDRESS_THIS, 1 ether)); @@ -709,7 +705,7 @@ contract V4RouterTest is RoutingTestHelpers { uint256 expectedAmountIn = 1008049273448486163; IV4Router.ExactOutputSingleParams memory params = IV4Router.ExactOutputSingleParams( - nativeKey, true, uint128(amountOut), uint128(expectedAmountIn + 1), 0, bytes("") + nativeKey, true, uint128(amountOut), uint128(expectedAmountIn + 1), bytes("") ); plan = plan.add(Actions.SWAP_EXACT_OUT_SINGLE, abi.encode(params)); @@ -729,7 +725,7 @@ contract V4RouterTest is RoutingTestHelpers { uint256 expectedAmountIn = 1008049273448486163; IV4Router.ExactOutputSingleParams memory params = IV4Router.ExactOutputSingleParams( - nativeKey, false, uint128(amountOut), uint128(expectedAmountIn + 1), 0, bytes("") + nativeKey, false, uint128(amountOut), uint128(expectedAmountIn + 1), bytes("") ); plan = plan.add(Actions.SWAP_EXACT_OUT_SINGLE, abi.encode(params)); diff --git a/test/shared/PosmTestSetup.sol b/test/shared/PosmTestSetup.sol index 0a79edd1..22e09ed9 100644 --- a/test/shared/PosmTestSetup.sol +++ b/test/shared/PosmTestSetup.sol @@ -17,6 +17,12 @@ import {HookSavesDelta} from "./HookSavesDelta.sol"; import {HookModifyLiquidities} from "./HookModifyLiquidities.sol"; import {PositionDescriptor} from "../../src/PositionDescriptor.sol"; import {ERC721PermitHash} from "../../src/libraries/ERC721PermitHash.sol"; +import {IWETH9} from "../../src/interfaces/external/IWETH9.sol"; +import {WETH} from "solmate/src/tokens/WETH.sol"; +import {MockERC20} from "solmate/src/test/utils/mocks/MockERC20.sol"; +import {SortTokens} from "@uniswap/v4-core/test/utils/SortTokens.sol"; +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; +import {PositionConfig} from "../shared/PositionConfig.sol"; /// @notice A shared test contract that wraps the v4-core deployers contract and exposes basic liquidity operations on posm. contract PosmTestSetup is Test, Deployers, DeployPermit2, LiquidityOperations { @@ -26,6 +32,7 @@ contract PosmTestSetup is Test, Deployers, DeployPermit2, LiquidityOperations { PositionDescriptor public positionDescriptor; HookSavesDelta hook; address hookAddr = address(uint160(Hooks.AFTER_ADD_LIQUIDITY_FLAG | Hooks.AFTER_REMOVE_LIQUIDITY_FLAG)); + IWETH9 public _WETH9 = IWETH9(address(new WETH())); HookModifyLiquidities hookModifyLiquidities; address hookModifyLiquiditiesAddr = address( @@ -35,6 +42,8 @@ contract PosmTestSetup is Test, Deployers, DeployPermit2, LiquidityOperations { ) ); + PoolKey wethKey; + function deployPosmHookSavesDelta() public { HookSavesDelta impl = new HookSavesDelta(); vm.etch(hookAddr, address(impl).code); @@ -60,7 +69,7 @@ contract PosmTestSetup is Test, Deployers, DeployPermit2, LiquidityOperations { // We use deployPermit2() to prevent having to use via-ir in this repository. permit2 = IAllowanceTransfer(deployPermit2()); positionDescriptor = new PositionDescriptor(poolManager, 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, "ETH"); - lpm = new PositionManager(poolManager, permit2, 100_000, positionDescriptor); + lpm = new PositionManager(poolManager, permit2, 100_000, positionDescriptor, _WETH9); } function seedBalance(address to) internal { @@ -88,6 +97,19 @@ contract PosmTestSetup is Test, Deployers, DeployPermit2, LiquidityOperations { vm.stopPrank(); } + function seedWeth(address to) internal { + vm.deal(address(this), STARTING_USER_BALANCE); + _WETH9.deposit{value: STARTING_USER_BALANCE}(); + _WETH9.transfer(to, STARTING_USER_BALANCE); + } + + function initWethPool(Currency currencyB, IHooks hooks, uint24 fee, uint160 sqrtPriceX96) internal { + (Currency _currency0, Currency _currency1) = + SortTokens.sort(MockERC20(address(_WETH9)), MockERC20(Currency.unwrap(currencyB))); + + (wethKey,) = initPool(_currency0, _currency1, hooks, fee, sqrtPriceX96); + } + function permit(uint256 privateKey, uint256 tokenId, address operator, uint256 nonce) internal { bytes32 digest = getDigest(operator, tokenId, 1, block.timestamp + 1);