diff --git a/.forge-snapshots/BaseActionsRouter_mock10commands.snap b/.forge-snapshots/BaseActionsRouter_mock10commands.snap index 8a065fc31..20e1c5f43 100644 --- a/.forge-snapshots/BaseActionsRouter_mock10commands.snap +++ b/.forge-snapshots/BaseActionsRouter_mock10commands.snap @@ -1 +1 @@ -60677 \ No newline at end of file +61332 \ No newline at end of file diff --git a/.forge-snapshots/Payments_swap_settleFromCaller_takeAllToMsgSender.snap b/.forge-snapshots/Payments_swap_settleFromCaller_takeAllToMsgSender.snap index 2cd533eed..770caaf99 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 +132997 \ 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 89faf94ce..3541eaf39 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 +134973 \ 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 55ac6b3ac..e05cffe7d 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 +127102 \ 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 00e673a8c..9166a87da 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 +127212 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_burn_empty.snap b/.forge-snapshots/PositionManager_burn_empty.snap index 0e85ca4a1..88081af81 100644 --- a/.forge-snapshots/PositionManager_burn_empty.snap +++ b/.forge-snapshots/PositionManager_burn_empty.snap @@ -1 +1 @@ -50446 \ No newline at end of file +51576 \ 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 0e85ca4a1..88081af81 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 +51576 \ 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 5345a8daa..b8ad8f5e7 100644 --- a/.forge-snapshots/PositionManager_burn_nonEmpty_native_withClose.snap +++ b/.forge-snapshots/PositionManager_burn_nonEmpty_native_withClose.snap @@ -1 +1 @@ -125574 \ No newline at end of file +128316 \ 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 068dd4562..5f7fe3b44 100644 --- a/.forge-snapshots/PositionManager_burn_nonEmpty_native_withTakePair.snap +++ b/.forge-snapshots/PositionManager_burn_nonEmpty_native_withTakePair.snap @@ -1 +1 @@ -125021 \ No newline at end of file +127696 \ 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 2ffdbdf14..f821b49cb 100644 --- a/.forge-snapshots/PositionManager_burn_nonEmpty_withClose.snap +++ b/.forge-snapshots/PositionManager_burn_nonEmpty_withClose.snap @@ -1 +1 @@ -132427 \ No newline at end of file +135236 \ 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 81149d950..46630b5d9 100644 --- a/.forge-snapshots/PositionManager_burn_nonEmpty_withTakePair.snap +++ b/.forge-snapshots/PositionManager_burn_nonEmpty_withTakePair.snap @@ -1 +1 @@ -131874 \ No newline at end of file +134615 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_collect_native.snap b/.forge-snapshots/PositionManager_collect_native.snap index e6d5ff3e3..86f965291 100644 --- a/.forge-snapshots/PositionManager_collect_native.snap +++ b/.forge-snapshots/PositionManager_collect_native.snap @@ -1 +1 @@ -146282 \ No newline at end of file +148571 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_collect_sameRange.snap b/.forge-snapshots/PositionManager_collect_sameRange.snap index fb052d005..e2b8005b9 100644 --- a/.forge-snapshots/PositionManager_collect_sameRange.snap +++ b/.forge-snapshots/PositionManager_collect_sameRange.snap @@ -1 +1 @@ -154848 \ No newline at end of file +157220 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_collect_withClose.snap b/.forge-snapshots/PositionManager_collect_withClose.snap index fb052d005..e2b8005b9 100644 --- a/.forge-snapshots/PositionManager_collect_withClose.snap +++ b/.forge-snapshots/PositionManager_collect_withClose.snap @@ -1 +1 @@ -154848 \ No newline at end of file +157220 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_collect_withTakePair.snap b/.forge-snapshots/PositionManager_collect_withTakePair.snap index 8be5e73cc..ccb34392b 100644 --- a/.forge-snapshots/PositionManager_collect_withTakePair.snap +++ b/.forge-snapshots/PositionManager_collect_withTakePair.snap @@ -1 +1 @@ -154169 \ No newline at end of file +156456 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_decreaseLiquidity_native.snap b/.forge-snapshots/PositionManager_decreaseLiquidity_native.snap index 5f7d954cc..c2b5120a1 100644 --- a/.forge-snapshots/PositionManager_decreaseLiquidity_native.snap +++ b/.forge-snapshots/PositionManager_decreaseLiquidity_native.snap @@ -1 +1 @@ -111971 \ No newline at end of file +114165 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_decreaseLiquidity_withClose.snap b/.forge-snapshots/PositionManager_decreaseLiquidity_withClose.snap index f069ca36d..bdea68d3c 100644 --- a/.forge-snapshots/PositionManager_decreaseLiquidity_withClose.snap +++ b/.forge-snapshots/PositionManager_decreaseLiquidity_withClose.snap @@ -1 +1 @@ -119729 \ No newline at end of file +122555 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_decreaseLiquidity_withTakePair.snap b/.forge-snapshots/PositionManager_decreaseLiquidity_withTakePair.snap index 7f1848a60..8c20e971e 100644 --- a/.forge-snapshots/PositionManager_decreaseLiquidity_withTakePair.snap +++ b/.forge-snapshots/PositionManager_decreaseLiquidity_withTakePair.snap @@ -1 +1 @@ -119050 \ No newline at end of file +121791 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_decrease_burnEmpty.snap b/.forge-snapshots/PositionManager_decrease_burnEmpty.snap index fce34ecdc..dbd7a9ac1 100644 --- a/.forge-snapshots/PositionManager_decrease_burnEmpty.snap +++ b/.forge-snapshots/PositionManager_decrease_burnEmpty.snap @@ -1 +1 @@ -135224 \ No newline at end of file +138177 \ 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 90c8a0d42..4622f7b48 100644 --- a/.forge-snapshots/PositionManager_decrease_burnEmpty_native.snap +++ b/.forge-snapshots/PositionManager_decrease_burnEmpty_native.snap @@ -1 +1 @@ -128371 \ No newline at end of file +131258 \ 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 017e8a133..105b30011 100644 --- a/.forge-snapshots/PositionManager_decrease_sameRange_allLiquidity.snap +++ b/.forge-snapshots/PositionManager_decrease_sameRange_allLiquidity.snap @@ -1 +1 @@ -132416 \ No newline at end of file +135218 \ 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 c3381b75e..a34b498cd 100644 --- a/.forge-snapshots/PositionManager_decrease_take_take.snap +++ b/.forge-snapshots/PositionManager_decrease_take_take.snap @@ -1 +1 @@ -120305 \ No newline at end of file +123169 \ 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 8800975f5..0c144b4d2 100644 --- a/.forge-snapshots/PositionManager_increaseLiquidity_erc20_withClose.snap +++ b/.forge-snapshots/PositionManager_increaseLiquidity_erc20_withClose.snap @@ -1 +1 @@ -159033 \ No newline at end of file +162419 \ 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 4abbe118f..5b88c6213 100644 --- a/.forge-snapshots/PositionManager_increaseLiquidity_erc20_withSettlePair.snap +++ b/.forge-snapshots/PositionManager_increaseLiquidity_erc20_withSettlePair.snap @@ -1 +1 @@ -157973 \ No newline at end of file +161283 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_increaseLiquidity_native.snap b/.forge-snapshots/PositionManager_increaseLiquidity_native.snap index 37c589b6e..dd90e5a55 100644 --- a/.forge-snapshots/PositionManager_increaseLiquidity_native.snap +++ b/.forge-snapshots/PositionManager_increaseLiquidity_native.snap @@ -1 +1 @@ -140860 \ No newline at end of file +145296 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_increase_autocompoundExactUnclaimedFees.snap b/.forge-snapshots/PositionManager_increase_autocompoundExactUnclaimedFees.snap index 770899b46..6312e8ddf 100644 --- a/.forge-snapshots/PositionManager_increase_autocompoundExactUnclaimedFees.snap +++ b/.forge-snapshots/PositionManager_increase_autocompoundExactUnclaimedFees.snap @@ -1 +1 @@ -136359 \ No newline at end of file +138010 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_increase_autocompoundExcessFeesCredit.snap b/.forge-snapshots/PositionManager_increase_autocompoundExcessFeesCredit.snap index 8b02aa231..cf908d5cf 100644 --- a/.forge-snapshots/PositionManager_increase_autocompoundExcessFeesCredit.snap +++ b/.forge-snapshots/PositionManager_increase_autocompoundExcessFeesCredit.snap @@ -1 +1 @@ -177340 \ No newline at end of file +180194 \ 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 01851753c..c9779d027 100644 --- a/.forge-snapshots/PositionManager_increase_autocompound_clearExcess.snap +++ b/.forge-snapshots/PositionManager_increase_autocompound_clearExcess.snap @@ -1 +1 @@ -148016 \ No newline at end of file +150782 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_native.snap b/.forge-snapshots/PositionManager_mint_native.snap index a62444fa7..ad9b48fff 100644 --- a/.forge-snapshots/PositionManager_mint_native.snap +++ b/.forge-snapshots/PositionManager_mint_native.snap @@ -1 +1 @@ -364721 \ No newline at end of file +369623 \ 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 71b95dabb..6d4460fb9 100644 --- a/.forge-snapshots/PositionManager_mint_nativeWithSweep_withClose.snap +++ b/.forge-snapshots/PositionManager_mint_nativeWithSweep_withClose.snap @@ -1 +1 @@ -373244 \ No newline at end of file +378280 \ 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 1883afaae..351997f8d 100644 --- a/.forge-snapshots/PositionManager_mint_nativeWithSweep_withSettlePair.snap +++ b/.forge-snapshots/PositionManager_mint_nativeWithSweep_withSettlePair.snap @@ -1 +1 @@ -372467 \ No newline at end of file +377364 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_onSameTickLower.snap b/.forge-snapshots/PositionManager_mint_onSameTickLower.snap index 36461286f..dcb8d5ae5 100644 --- a/.forge-snapshots/PositionManager_mint_onSameTickLower.snap +++ b/.forge-snapshots/PositionManager_mint_onSameTickLower.snap @@ -1 +1 @@ -317569 \ No newline at end of file +321205 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_onSameTickUpper.snap b/.forge-snapshots/PositionManager_mint_onSameTickUpper.snap index ff59c2650..3a7c7c34a 100644 --- a/.forge-snapshots/PositionManager_mint_onSameTickUpper.snap +++ b/.forge-snapshots/PositionManager_mint_onSameTickUpper.snap @@ -1 +1 @@ -318239 \ No newline at end of file +321875 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_sameRange.snap b/.forge-snapshots/PositionManager_mint_sameRange.snap index 74ca2bf71..7d8a26862 100644 --- a/.forge-snapshots/PositionManager_mint_sameRange.snap +++ b/.forge-snapshots/PositionManager_mint_sameRange.snap @@ -1 +1 @@ -243808 \ No newline at end of file +247444 \ 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 e394e5e8e..4d62f1da1 100644 --- a/.forge-snapshots/PositionManager_mint_settleWithBalance_sweep.snap +++ b/.forge-snapshots/PositionManager_mint_settleWithBalance_sweep.snap @@ -1 +1 @@ -418988 \ No newline at end of file +423280 \ 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 9b34b9e5d..fcd47aaf3 100644 --- a/.forge-snapshots/PositionManager_mint_warmedPool_differentRange.snap +++ b/.forge-snapshots/PositionManager_mint_warmedPool_differentRange.snap @@ -1 +1 @@ -323600 \ No newline at end of file +327236 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_withClose.snap b/.forge-snapshots/PositionManager_mint_withClose.snap index 5c7354cd5..59610f85c 100644 --- a/.forge-snapshots/PositionManager_mint_withClose.snap +++ b/.forge-snapshots/PositionManager_mint_withClose.snap @@ -1 +1 @@ -420122 \ No newline at end of file +423986 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_withSettlePair.snap b/.forge-snapshots/PositionManager_mint_withSettlePair.snap index f18f3a75c..b3a6480c2 100644 --- a/.forge-snapshots/PositionManager_mint_withSettlePair.snap +++ b/.forge-snapshots/PositionManager_mint_withSettlePair.snap @@ -1 +1 @@ -419180 \ No newline at end of file +422936 \ 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 06443a11d..19380f4f4 100644 --- a/.forge-snapshots/PositionManager_multicall_initialize_mint.snap +++ b/.forge-snapshots/PositionManager_multicall_initialize_mint.snap @@ -1 +1 @@ -463927 \ No newline at end of file +460558 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_permit.snap b/.forge-snapshots/PositionManager_permit.snap index 227e327e4..53dd01e77 100644 --- a/.forge-snapshots/PositionManager_permit.snap +++ b/.forge-snapshots/PositionManager_permit.snap @@ -1 +1 @@ -79076 \ No newline at end of file +79259 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_permit_secondPosition.snap b/.forge-snapshots/PositionManager_permit_secondPosition.snap index 31ad61876..ac17c65c1 100644 --- a/.forge-snapshots/PositionManager_permit_secondPosition.snap +++ b/.forge-snapshots/PositionManager_permit_secondPosition.snap @@ -1 +1 @@ -61976 \ No newline at end of file +62159 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_permit_twice.snap b/.forge-snapshots/PositionManager_permit_twice.snap index d650ccbd7..532d2de91 100644 --- a/.forge-snapshots/PositionManager_permit_twice.snap +++ b/.forge-snapshots/PositionManager_permit_twice.snap @@ -1 +1 @@ -44876 \ No newline at end of file +45035 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_subscribe.snap b/.forge-snapshots/PositionManager_subscribe.snap index e4eada758..f6081d8d7 100644 --- a/.forge-snapshots/PositionManager_subscribe.snap +++ b/.forge-snapshots/PositionManager_subscribe.snap @@ -1 +1 @@ -84348 \ No newline at end of file +88475 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_unsubscribe.snap b/.forge-snapshots/PositionManager_unsubscribe.snap index 0151c604d..3651ba8a5 100644 --- a/.forge-snapshots/PositionManager_unsubscribe.snap +++ b/.forge-snapshots/PositionManager_unsubscribe.snap @@ -1 +1 @@ -59238 \ No newline at end of file +63253 \ No newline at end of file diff --git a/.forge-snapshots/Quoter_exactInputSingle_oneForZero_multiplePositions.snap b/.forge-snapshots/Quoter_exactInputSingle_oneForZero_multiplePositions.snap index 485e8f0d7..cb86556e8 100644 --- a/.forge-snapshots/Quoter_exactInputSingle_oneForZero_multiplePositions.snap +++ b/.forge-snapshots/Quoter_exactInputSingle_oneForZero_multiplePositions.snap @@ -1 +1 @@ -143930 \ No newline at end of file +146317 \ No newline at end of file diff --git a/.forge-snapshots/Quoter_exactInputSingle_zeroForOne_multiplePositions.snap b/.forge-snapshots/Quoter_exactInputSingle_zeroForOne_multiplePositions.snap index f89390d96..8cf53dc80 100644 --- a/.forge-snapshots/Quoter_exactInputSingle_zeroForOne_multiplePositions.snap +++ b/.forge-snapshots/Quoter_exactInputSingle_zeroForOne_multiplePositions.snap @@ -1 +1 @@ -149382 \ No newline at end of file +151973 \ No newline at end of file diff --git a/.forge-snapshots/Quoter_exactOutputSingle_oneForZero.snap b/.forge-snapshots/Quoter_exactOutputSingle_oneForZero.snap index a40f3f57a..2f2aa6ed8 100644 --- a/.forge-snapshots/Quoter_exactOutputSingle_oneForZero.snap +++ b/.forge-snapshots/Quoter_exactOutputSingle_oneForZero.snap @@ -1 +1 @@ -78203 \ No newline at end of file +80048 \ No newline at end of file diff --git a/.forge-snapshots/Quoter_exactOutputSingle_zeroForOne.snap b/.forge-snapshots/Quoter_exactOutputSingle_zeroForOne.snap index 23153115b..9606490a7 100644 --- a/.forge-snapshots/Quoter_exactOutputSingle_zeroForOne.snap +++ b/.forge-snapshots/Quoter_exactOutputSingle_zeroForOne.snap @@ -1 +1 @@ -82626 \ No newline at end of file +84626 \ No newline at end of file diff --git a/.forge-snapshots/Quoter_quoteExactInput_oneHop_1TickLoaded.snap b/.forge-snapshots/Quoter_quoteExactInput_oneHop_1TickLoaded.snap index a3ea8ad76..9aebeeca5 100644 --- a/.forge-snapshots/Quoter_quoteExactInput_oneHop_1TickLoaded.snap +++ b/.forge-snapshots/Quoter_quoteExactInput_oneHop_1TickLoaded.snap @@ -1 +1 @@ -120491 \ No newline at end of file +122994 \ No newline at end of file diff --git a/.forge-snapshots/Quoter_quoteExactInput_oneHop_initializedAfter.snap b/.forge-snapshots/Quoter_quoteExactInput_oneHop_initializedAfter.snap index 6dcb3b78d..1a3ae7138 100644 --- a/.forge-snapshots/Quoter_quoteExactInput_oneHop_initializedAfter.snap +++ b/.forge-snapshots/Quoter_quoteExactInput_oneHop_initializedAfter.snap @@ -1 +1 @@ -145414 \ No newline at end of file +147949 \ No newline at end of file diff --git a/.forge-snapshots/Quoter_quoteExactInput_oneHop_startingInitialized.snap b/.forge-snapshots/Quoter_quoteExactInput_oneHop_startingInitialized.snap index 1f604e115..1ce389ff1 100644 --- a/.forge-snapshots/Quoter_quoteExactInput_oneHop_startingInitialized.snap +++ b/.forge-snapshots/Quoter_quoteExactInput_oneHop_startingInitialized.snap @@ -1 +1 @@ -79437 \ No newline at end of file +81420 \ No newline at end of file diff --git a/.forge-snapshots/Quoter_quoteExactInput_twoHops.snap b/.forge-snapshots/Quoter_quoteExactInput_twoHops.snap index bb203fa98..7fe1af46a 100644 --- a/.forge-snapshots/Quoter_quoteExactInput_twoHops.snap +++ b/.forge-snapshots/Quoter_quoteExactInput_twoHops.snap @@ -1 +1 @@ -201179 \ No newline at end of file +205421 \ No newline at end of file diff --git a/.forge-snapshots/Quoter_quoteExactOutput_oneHop_1TickLoaded.snap b/.forge-snapshots/Quoter_quoteExactOutput_oneHop_1TickLoaded.snap index e7385875b..6233611de 100644 --- a/.forge-snapshots/Quoter_quoteExactOutput_oneHop_1TickLoaded.snap +++ b/.forge-snapshots/Quoter_quoteExactOutput_oneHop_1TickLoaded.snap @@ -1 +1 @@ -119782 \ No newline at end of file +122296 \ No newline at end of file diff --git a/.forge-snapshots/Quoter_quoteExactOutput_oneHop_2TicksLoaded.snap b/.forge-snapshots/Quoter_quoteExactOutput_oneHop_2TicksLoaded.snap index 14b51340c..ee524ce0d 100644 --- a/.forge-snapshots/Quoter_quoteExactOutput_oneHop_2TicksLoaded.snap +++ b/.forge-snapshots/Quoter_quoteExactOutput_oneHop_2TicksLoaded.snap @@ -1 +1 @@ -149919 \ No newline at end of file +152648 \ No newline at end of file diff --git a/.forge-snapshots/Quoter_quoteExactOutput_oneHop_initializedAfter.snap b/.forge-snapshots/Quoter_quoteExactOutput_oneHop_initializedAfter.snap index c19a0a13c..f965907ac 100644 --- a/.forge-snapshots/Quoter_quoteExactOutput_oneHop_initializedAfter.snap +++ b/.forge-snapshots/Quoter_quoteExactOutput_oneHop_initializedAfter.snap @@ -1 +1 @@ -119850 \ No newline at end of file +122364 \ No newline at end of file diff --git a/.forge-snapshots/Quoter_quoteExactOutput_oneHop_startingInitialized.snap b/.forge-snapshots/Quoter_quoteExactOutput_oneHop_startingInitialized.snap index c0333d8aa..f813bc4bc 100644 --- a/.forge-snapshots/Quoter_quoteExactOutput_oneHop_startingInitialized.snap +++ b/.forge-snapshots/Quoter_quoteExactOutput_oneHop_startingInitialized.snap @@ -1 +1 @@ -96549 \ No newline at end of file +98875 \ No newline at end of file diff --git a/.forge-snapshots/Quoter_quoteExactOutput_twoHops.snap b/.forge-snapshots/Quoter_quoteExactOutput_twoHops.snap index 7acf5efcd..584ea1427 100644 --- a/.forge-snapshots/Quoter_quoteExactOutput_twoHops.snap +++ b/.forge-snapshots/Quoter_quoteExactOutput_twoHops.snap @@ -1 +1 @@ -200630 \ No newline at end of file +204897 \ No newline at end of file diff --git a/.forge-snapshots/StateView_extsload_getFeeGrowthGlobals.snap b/.forge-snapshots/StateView_extsload_getFeeGrowthGlobals.snap index 98665bfc6..a9e72c33a 100644 --- a/.forge-snapshots/StateView_extsload_getFeeGrowthGlobals.snap +++ b/.forge-snapshots/StateView_extsload_getFeeGrowthGlobals.snap @@ -1 +1 @@ -2259 \ No newline at end of file +2367 \ No newline at end of file diff --git a/.forge-snapshots/StateView_extsload_getFeeGrowthInside.snap b/.forge-snapshots/StateView_extsload_getFeeGrowthInside.snap index 7db58ace5..b1b49287a 100644 --- a/.forge-snapshots/StateView_extsload_getFeeGrowthInside.snap +++ b/.forge-snapshots/StateView_extsload_getFeeGrowthInside.snap @@ -1 +1 @@ -8003 \ No newline at end of file +8444 \ No newline at end of file diff --git a/.forge-snapshots/StateView_extsload_getLiquidity.snap b/.forge-snapshots/StateView_extsload_getLiquidity.snap index a900d0b24..ab57eb1e7 100644 --- a/.forge-snapshots/StateView_extsload_getLiquidity.snap +++ b/.forge-snapshots/StateView_extsload_getLiquidity.snap @@ -1 +1 @@ -1399 \ No newline at end of file +1480 \ No newline at end of file diff --git a/.forge-snapshots/StateView_extsload_getPositionInfo.snap b/.forge-snapshots/StateView_extsload_getPositionInfo.snap index 4b9661fc9..0a1ca398f 100644 --- a/.forge-snapshots/StateView_extsload_getPositionInfo.snap +++ b/.forge-snapshots/StateView_extsload_getPositionInfo.snap @@ -1 +1 @@ -2829 \ No newline at end of file +2973 \ No newline at end of file diff --git a/.forge-snapshots/StateView_extsload_getPositionLiquidity.snap b/.forge-snapshots/StateView_extsload_getPositionLiquidity.snap index 7f3589da4..8d6430831 100644 --- a/.forge-snapshots/StateView_extsload_getPositionLiquidity.snap +++ b/.forge-snapshots/StateView_extsload_getPositionLiquidity.snap @@ -1 +1 @@ -1651 \ No newline at end of file +1750 \ No newline at end of file diff --git a/.forge-snapshots/StateView_extsload_getSlot0.snap b/.forge-snapshots/StateView_extsload_getSlot0.snap index 585cf67a0..466fe4026 100644 --- a/.forge-snapshots/StateView_extsload_getSlot0.snap +++ b/.forge-snapshots/StateView_extsload_getSlot0.snap @@ -1 +1 @@ -1446 \ No newline at end of file +1548 \ No newline at end of file diff --git a/.forge-snapshots/StateView_extsload_getTickBitmap.snap b/.forge-snapshots/StateView_extsload_getTickBitmap.snap index 05fd8282c..0e8ebf59f 100644 --- a/.forge-snapshots/StateView_extsload_getTickBitmap.snap +++ b/.forge-snapshots/StateView_extsload_getTickBitmap.snap @@ -1 +1 @@ -1392 \ No newline at end of file +1476 \ No newline at end of file diff --git a/.forge-snapshots/StateView_extsload_getTickFeeGrowthOutside.snap b/.forge-snapshots/StateView_extsload_getTickFeeGrowthOutside.snap index 6870d0f23..7c43ea017 100644 --- a/.forge-snapshots/StateView_extsload_getTickFeeGrowthOutside.snap +++ b/.forge-snapshots/StateView_extsload_getTickFeeGrowthOutside.snap @@ -1 +1 @@ -2546 \ No newline at end of file +2672 \ No newline at end of file diff --git a/.forge-snapshots/StateView_extsload_getTickInfo.snap b/.forge-snapshots/StateView_extsload_getTickInfo.snap index cd5ecabca..3582118cc 100644 --- a/.forge-snapshots/StateView_extsload_getTickInfo.snap +++ b/.forge-snapshots/StateView_extsload_getTickInfo.snap @@ -1 +1 @@ -2761 \ No newline at end of file +2896 \ No newline at end of file diff --git a/.forge-snapshots/StateView_extsload_getTickLiquidity.snap b/.forge-snapshots/StateView_extsload_getTickLiquidity.snap index 44e788259..0504b068d 100644 --- a/.forge-snapshots/StateView_extsload_getTickLiquidity.snap +++ b/.forge-snapshots/StateView_extsload_getTickLiquidity.snap @@ -1 +1 @@ -1646 \ No newline at end of file +1748 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_Bytecode.snap b/.forge-snapshots/V4Router_Bytecode.snap index fb87573dc..ec718f71f 100644 --- a/.forge-snapshots/V4Router_Bytecode.snap +++ b/.forge-snapshots/V4Router_Bytecode.snap @@ -1 +1 @@ -7148 \ No newline at end of file +5137 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactIn1Hop_nativeIn.snap b/.forge-snapshots/V4Router_ExactIn1Hop_nativeIn.snap index bc9d99894..56a7e65e8 100644 --- a/.forge-snapshots/V4Router_ExactIn1Hop_nativeIn.snap +++ b/.forge-snapshots/V4Router_ExactIn1Hop_nativeIn.snap @@ -1 +1 @@ -115722 \ No newline at end of file +121819 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactIn1Hop_nativeOut.snap b/.forge-snapshots/V4Router_ExactIn1Hop_nativeOut.snap index 764f3bbb0..cbfeb1a9c 100644 --- a/.forge-snapshots/V4Router_ExactIn1Hop_nativeOut.snap +++ b/.forge-snapshots/V4Router_ExactIn1Hop_nativeOut.snap @@ -1 +1 @@ -116043 \ No newline at end of file +120826 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactIn1Hop_oneForZero.snap b/.forge-snapshots/V4Router_ExactIn1Hop_oneForZero.snap index 40990e3ce..6bf2eef7d 100644 --- a/.forge-snapshots/V4Router_ExactIn1Hop_oneForZero.snap +++ b/.forge-snapshots/V4Router_ExactIn1Hop_oneForZero.snap @@ -1 +1 @@ -124861 \ No newline at end of file +129715 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactIn1Hop_zeroForOne.snap b/.forge-snapshots/V4Router_ExactIn1Hop_zeroForOne.snap index a38c964a3..891422a70 100644 --- a/.forge-snapshots/V4Router_ExactIn1Hop_zeroForOne.snap +++ b/.forge-snapshots/V4Router_ExactIn1Hop_zeroForOne.snap @@ -1 +1 @@ -130584 \ No newline at end of file +135623 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactIn2Hops.snap b/.forge-snapshots/V4Router_ExactIn2Hops.snap index 208b10238..a7bc469ae 100644 --- a/.forge-snapshots/V4Router_ExactIn2Hops.snap +++ b/.forge-snapshots/V4Router_ExactIn2Hops.snap @@ -1 +1 @@ -185439 \ No newline at end of file +191979 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactIn2Hops_nativeIn.snap b/.forge-snapshots/V4Router_ExactIn2Hops_nativeIn.snap index 2862d64cc..ef0bcd5fe 100644 --- a/.forge-snapshots/V4Router_ExactIn2Hops_nativeIn.snap +++ b/.forge-snapshots/V4Router_ExactIn2Hops_nativeIn.snap @@ -1 +1 @@ -170577 \ No newline at end of file +178175 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactIn3Hops.snap b/.forge-snapshots/V4Router_ExactIn3Hops.snap index c44d7bb06..89acd064f 100644 --- a/.forge-snapshots/V4Router_ExactIn3Hops.snap +++ b/.forge-snapshots/V4Router_ExactIn3Hops.snap @@ -1 +1 @@ -240297 \ No newline at end of file +248385 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactIn3Hops_nativeIn.snap b/.forge-snapshots/V4Router_ExactIn3Hops_nativeIn.snap index e98fcba77..a93abe955 100644 --- a/.forge-snapshots/V4Router_ExactIn3Hops_nativeIn.snap +++ b/.forge-snapshots/V4Router_ExactIn3Hops_nativeIn.snap @@ -1 +1 @@ -225435 \ No newline at end of file +234581 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactInputSingle.snap b/.forge-snapshots/V4Router_ExactInputSingle.snap index 2cd533eed..24fe6dec9 100644 --- a/.forge-snapshots/V4Router_ExactInputSingle.snap +++ b/.forge-snapshots/V4Router_ExactInputSingle.snap @@ -1 +1 @@ -129854 \ No newline at end of file +134546 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactInputSingle_nativeIn.snap b/.forge-snapshots/V4Router_ExactInputSingle_nativeIn.snap index 5e5c5b3ba..7f06d7f09 100644 --- a/.forge-snapshots/V4Router_ExactInputSingle_nativeIn.snap +++ b/.forge-snapshots/V4Router_ExactInputSingle_nativeIn.snap @@ -1 +1 @@ -114992 \ No newline at end of file +120742 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactInputSingle_nativeOut.snap b/.forge-snapshots/V4Router_ExactInputSingle_nativeOut.snap index f36fa4504..4e26e8532 100644 --- a/.forge-snapshots/V4Router_ExactInputSingle_nativeOut.snap +++ b/.forge-snapshots/V4Router_ExactInputSingle_nativeOut.snap @@ -1 +1 @@ -115282 \ No newline at end of file +119714 \ 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 9c6beb911..1fdaa9cd4 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 +128078 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactOut1Hop_nativeOut.snap b/.forge-snapshots/V4Router_ExactOut1Hop_nativeOut.snap index adfd51ab4..5680b0271 100644 --- a/.forge-snapshots/V4Router_ExactOut1Hop_nativeOut.snap +++ b/.forge-snapshots/V4Router_ExactOut1Hop_nativeOut.snap @@ -1 +1 @@ -117107 \ No newline at end of file +121895 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactOut1Hop_oneForZero.snap b/.forge-snapshots/V4Router_ExactOut1Hop_oneForZero.snap index 7692da738..85542b79f 100644 --- a/.forge-snapshots/V4Router_ExactOut1Hop_oneForZero.snap +++ b/.forge-snapshots/V4Router_ExactOut1Hop_oneForZero.snap @@ -1 +1 @@ -125925 \ No newline at end of file +130784 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactOut1Hop_zeroForOne.snap b/.forge-snapshots/V4Router_ExactOut1Hop_zeroForOne.snap index 93703d01e..a883e8809 100644 --- a/.forge-snapshots/V4Router_ExactOut1Hop_zeroForOne.snap +++ b/.forge-snapshots/V4Router_ExactOut1Hop_zeroForOne.snap @@ -1 +1 @@ -129870 \ No newline at end of file +134905 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactOut2Hops.snap b/.forge-snapshots/V4Router_ExactOut2Hops.snap index abfba6e58..eb9099cb8 100644 --- a/.forge-snapshots/V4Router_ExactOut2Hops.snap +++ b/.forge-snapshots/V4Router_ExactOut2Hops.snap @@ -1 +1 @@ -183787 \ No newline at end of file +190319 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactOut2Hops_nativeIn.snap b/.forge-snapshots/V4Router_ExactOut2Hops_nativeIn.snap index f236e20d6..c2de3e9b9 100644 --- a/.forge-snapshots/V4Router_ExactOut2Hops_nativeIn.snap +++ b/.forge-snapshots/V4Router_ExactOut2Hops_nativeIn.snap @@ -1 +1 @@ -175902 \ No newline at end of file +183492 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactOut3Hops.snap b/.forge-snapshots/V4Router_ExactOut3Hops.snap index cfb3e8279..ddf200aa9 100644 --- a/.forge-snapshots/V4Router_ExactOut3Hops.snap +++ b/.forge-snapshots/V4Router_ExactOut3Hops.snap @@ -1 +1 @@ -237735 \ No newline at end of file +245811 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactOut3Hops_nativeIn.snap b/.forge-snapshots/V4Router_ExactOut3Hops_nativeIn.snap index c30911016..583269aea 100644 --- a/.forge-snapshots/V4Router_ExactOut3Hops_nativeIn.snap +++ b/.forge-snapshots/V4Router_ExactOut3Hops_nativeIn.snap @@ -1 +1 @@ -229850 \ No newline at end of file +238984 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactOut3Hops_nativeOut.snap b/.forge-snapshots/V4Router_ExactOut3Hops_nativeOut.snap index 575b9ea9f..0ff3d5c82 100644 --- a/.forge-snapshots/V4Router_ExactOut3Hops_nativeOut.snap +++ b/.forge-snapshots/V4Router_ExactOut3Hops_nativeOut.snap @@ -1 +1 @@ -217090 \ No newline at end of file +224567 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactOutputSingle.snap b/.forge-snapshots/V4Router_ExactOutputSingle.snap index 5de03712a..d44e2734a 100644 --- a/.forge-snapshots/V4Router_ExactOutputSingle.snap +++ b/.forge-snapshots/V4Router_ExactOutputSingle.snap @@ -1 +1 @@ -129140 \ No newline at end of file +133809 \ 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 6120543a0..fc1ac5689 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 +126982 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactOutputSingle_nativeOut.snap b/.forge-snapshots/V4Router_ExactOutputSingle_nativeOut.snap index b7b122f53..dacad49f6 100644 --- a/.forge-snapshots/V4Router_ExactOutputSingle_nativeOut.snap +++ b/.forge-snapshots/V4Router_ExactOutputSingle_nativeOut.snap @@ -1 +1 @@ -116452 \ No newline at end of file +120876 \ No newline at end of file diff --git a/.forge-snapshots/positionDescriptor bytecode size.snap b/.forge-snapshots/positionDescriptor bytecode size.snap index ec121d73b..60d759c40 100644 --- a/.forge-snapshots/positionDescriptor bytecode size.snap +++ b/.forge-snapshots/positionDescriptor bytecode size.snap @@ -1 +1 @@ -31065 \ No newline at end of file +24110 \ No newline at end of file diff --git a/.forge-snapshots/positionManager bytecode size.snap b/.forge-snapshots/positionManager bytecode size.snap new file mode 100644 index 000000000..404029b94 --- /dev/null +++ b/.forge-snapshots/positionManager bytecode size.snap @@ -0,0 +1 @@ +19060 \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index 9d6618d5b..fb9fdc7d4 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "lib/permit2"] path = lib/permit2 url = https://github.com/Uniswap/permit2 +[submodule "lib/forge-gas-snapshot"] + path = lib/forge-gas-snapshot + url = https://github.com/marktoda/forge-gas-snapshot diff --git a/LICENSE b/LICENSE index ecbc05937..45956f482 100644 --- a/LICENSE +++ b/LICENSE @@ -1,339 +1,7 @@ - GNU GENERAL PUBLIC LICENSE - Version 2, June 1991 +Copyright 2023 Universal Navigation Inc. - Copyright (C) 1989, 1991 Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - Preamble +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -License is intended to guarantee your freedom to share and change free -software--to make sure the software is free for all its users. This -General Public License applies to most of the Free Software -Foundation's software and to any other program whose authors commit to -using it. (Some other Free Software Foundation software is covered by -the GNU Lesser General Public License instead.) You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -this service if you wish), that you receive source code or can get it -if you want it, that you can change the software or use pieces of it -in new free programs; and that you know you can do these things. - - To protect your rights, we need to make restrictions that forbid -anyone to deny you these rights or to ask you to surrender the rights. -These restrictions translate to certain responsibilities for you if you -distribute copies of the software, or if you modify it. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must give the recipients all the rights that -you have. You must make sure that they, too, receive or can get the -source code. And you must show them these terms so they know their -rights. - - We protect your rights with two steps: (1) copyright the software, and -(2) offer you this license which gives you legal permission to copy, -distribute and/or modify the software. - - Also, for each author's protection and ours, we want to make certain -that everyone understands that there is no warranty for this free -software. If the software is modified by someone else and passed on, we -want its recipients to know that what they have is not the original, so -that any problems introduced by others will not reflect on the original -authors' reputations. - - Finally, any free program is threatened constantly by software -patents. We wish to avoid the danger that redistributors of a free -program will individually obtain patent licenses, in effect making the -program proprietary. To prevent this, we have made it clear that any -patent must be licensed for everyone's free use or not licensed at all. - - The precise terms and conditions for copying, distribution and -modification follow. - - GNU GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. This License applies to any program or other work which contains -a notice placed by the copyright holder saying it may be distributed -under the terms of this General Public License. The "Program", below, -refers to any such program or work, and a "work based on the Program" -means either the Program or any derivative work under copyright law: -that is to say, a work containing the Program or a portion of it, -either verbatim or with modifications and/or translated into another -language. (Hereinafter, translation is included without limitation in -the term "modification".) Each licensee is addressed as "you". - -Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running the Program is not restricted, and the output from the Program -is covered only if its contents constitute a work based on the -Program (independent of having been made by running the Program). -Whether that is true depends on what the Program does. - - 1. You may copy and distribute verbatim copies of the Program's -source code as you receive it, in any medium, provided that you -conspicuously and appropriately publish on each copy an appropriate -copyright notice and disclaimer of warranty; keep intact all the -notices that refer to this License and to the absence of any warranty; -and give any other recipients of the Program a copy of this License -along with the Program. - -You may charge a fee for the physical act of transferring a copy, and -you may at your option offer warranty protection in exchange for a fee. - - 2. You may modify your copy or copies of the Program or any portion -of it, thus forming a work based on the Program, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: - - a) You must cause the modified files to carry prominent notices - stating that you changed the files and the date of any change. - - b) You must cause any work that you distribute or publish, that in - whole or in part contains or is derived from the Program or any - part thereof, to be licensed as a whole at no charge to all third - parties under the terms of this License. - - c) If the modified program normally reads commands interactively - when run, you must cause it, when started running for such - interactive use in the most ordinary way, to print or display an - announcement including an appropriate copyright notice and a - notice that there is no warranty (or else, saying that you provide - a warranty) and that users may redistribute the program under - these conditions, and telling the user how to view a copy of this - License. (Exception: if the Program itself is interactive but - does not normally print such an announcement, your work based on - the Program is not required to print an announcement.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Program, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Program, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote it. - -Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Program. - -In addition, mere aggregation of another work not based on the Program -with the Program (or with a work based on the Program) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - - 3. You may copy and distribute the Program (or a work based on it, -under Section 2) in object code or executable form under the terms of -Sections 1 and 2 above provided that you also do one of the following: - - a) Accompany it with the complete corresponding machine-readable - source code, which must be distributed under the terms of Sections - 1 and 2 above on a medium customarily used for software interchange; or, - - b) Accompany it with a written offer, valid for at least three - years, to give any third party, for a charge no more than your - cost of physically performing source distribution, a complete - machine-readable copy of the corresponding source code, to be - distributed under the terms of Sections 1 and 2 above on a medium - customarily used for software interchange; or, - - c) Accompany it with the information you received as to the offer - to distribute corresponding source code. (This alternative is - allowed only for noncommercial distribution and only if you - received the program in object code or executable form with such - an offer, in accord with Subsection b above.) - -The source code for a work means the preferred form of the work for -making modifications to it. For an executable work, complete source -code means all the source code for all modules it contains, plus any -associated interface definition files, plus the scripts used to -control compilation and installation of the executable. However, as a -special exception, the source code distributed need not include -anything that is normally distributed (in either source or binary -form) with the major components (compiler, kernel, and so on) of the -operating system on which the executable runs, unless that component -itself accompanies the executable. - -If distribution of executable or object code is made by offering -access to copy from a designated place, then offering equivalent -access to copy the source code from the same place counts as -distribution of the source code, even though third parties are not -compelled to copy the source along with the object code. - - 4. You may not copy, modify, sublicense, or distribute the Program -except as expressly provided under this License. Any attempt -otherwise to copy, modify, sublicense or distribute the Program is -void, and will automatically terminate your rights under this License. -However, parties who have received copies, or rights, from you under -this License will not have their licenses terminated so long as such -parties remain in full compliance. - - 5. You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Program or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Program (or any work based on the -Program), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Program or works based on it. - - 6. Each time you redistribute the Program (or any work based on the -Program), the recipient automatically receives a license from the -original licensor to copy, distribute or modify the Program subject to -these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties to -this License. - - 7. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Program at all. For example, if a patent -license would not permit royalty-free redistribution of the Program by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Program. - -If any portion of this section is held invalid or unenforceable under -any particular circumstance, the balance of the section is intended to -apply and the section as a whole is intended to apply in other -circumstances. - -It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system, which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. - -This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. - - 8. If the distribution and/or use of the Program is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Program under this License -may add an explicit geographical distribution limitation excluding -those countries, so that distribution is permitted only in or among -countries not thus excluded. In such case, this License incorporates -the limitation as if written in the body of this License. - - 9. The Free Software Foundation may publish revised and/or new versions -of the General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - -Each version is given a distinguishing version number. If the Program -specifies a version number of this License which applies to it and "any -later version", you have the option of following the terms and conditions -either of that version or of any later version published by the Free -Software Foundation. If the Program does not specify a version number of -this License, you may choose any version ever published by the Free Software -Foundation. - - 10. If you wish to incorporate parts of the Program into other free -programs whose distribution conditions are different, write to the author -to ask for permission. For software which is copyrighted by the Free -Software Foundation, write to the Free Software Foundation; we sometimes -make exceptions for this. Our decision will be guided by the two goals -of preserving the free status of all derivatives of our free software and -of promoting the sharing and reuse of software generally. - - NO WARRANTY - - 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY -FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN -OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES -PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED -OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS -TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE -PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, -REPAIR OR CORRECTION. - - 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR -REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, -INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING -OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED -TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY -YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER -PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE -POSSIBILITY OF SUCH DAMAGES. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -convey the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along - with this program; if not, write to the Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -Also add information on how to contact you by electronic and paper mail. - -If the program is interactive, make it output a short notice like this -when it starts in an interactive mode: - - Gnomovision version 69, Copyright (C) year name of author - Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, the commands you use may -be called something other than `show w' and `show c'; they could even be -mouse-clicks or menu items--whatever suits your program. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the program, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the program - `Gnomovision' (which makes passes at compilers) written by James Hacker. - - , 1 April 1989 - Ty Coon, President of Vice - -This General Public License does not permit incorporating your program into -proprietary programs. If your program is a subroutine library, you may -consider it more useful to permit linking proprietary applications with the -library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. \ No newline at end of file +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/foundry.toml b/foundry.toml index 65c65c520..45df582a9 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,13 +1,14 @@ [profile.default] out = 'foundry-out' solc_version = '0.8.26' -optimizer_runs = 44444444 +optimizer_runs = 1 via_ir = true ffi = true fs_permissions = [{ access = "read-write", path = ".forge-snapshots/"}] evm_version = "cancun" gas_limit = "3000000000" fuzz.runs = 10_000 +bytecode_hash = "none" [profile.debug] via_ir = false diff --git a/lib/forge-gas-snapshot b/lib/forge-gas-snapshot new file mode 160000 index 000000000..9fc447c73 --- /dev/null +++ b/lib/forge-gas-snapshot @@ -0,0 +1 @@ +Subproject commit 9fc447c732c89b6dd6352c096042d8d82b44faed diff --git a/lib/v4-core b/lib/v4-core index 88482f7dc..b619b6718 160000 --- a/lib/v4-core +++ b/lib/v4-core @@ -1 +1 @@ -Subproject commit 88482f7dc4356a7c395e199e454d50dc960bb6e7 +Subproject commit b619b6718e31aa5b4fa0286520c455ceb950276d diff --git a/remappings.txt b/remappings.txt index e7868fe96..c4f006e70 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,6 +1,6 @@ @uniswap/v4-core/=lib/v4-core/ -forge-gas-snapshot/=lib/v4-core/lib/forge-gas-snapshot/src/ +forge-gas-snapshot/=lib/forge-gas-snapshot/src/ ds-test/=lib/v4-core/lib/forge-std/lib/ds-test/src/ forge-std/=lib/v4-core/lib/forge-std/src/ openzeppelin-contracts/=lib/v4-core/lib/openzeppelin-contracts/ -solmate/=lib/v4-core/lib/solmate/ \ No newline at end of file +solmate/=lib/v4-core/lib/solmate/ diff --git a/script/01_PoolManager.s.sol b/script/01_PoolManager.s.sol index e412add9e..3d9d570d8 100644 --- a/script/01_PoolManager.s.sol +++ b/script/01_PoolManager.s.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "forge-std/Script.sol"; @@ -13,7 +13,7 @@ contract DeployPoolManager is Script { function run() public returns (IPoolManager manager) { vm.startBroadcast(); - manager = new PoolManager(); + manager = new PoolManager(address(this)); console2.log("PoolManager", address(manager)); vm.stopBroadcast(); diff --git a/script/02_PoolModifyLiquidityTest.s.sol b/script/02_PoolModifyLiquidityTest.s.sol index 3e0b75510..a2c4975ed 100644 --- a/script/02_PoolModifyLiquidityTest.s.sol +++ b/script/02_PoolModifyLiquidityTest.s.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: MIT pragma solidity ^0.8.22; import {Script} from "forge-std/Script.sol"; diff --git a/script/03_PoolSwapTest.s.sol b/script/03_PoolSwapTest.s.sol index a2da989f9..acd650d25 100644 --- a/script/03_PoolSwapTest.s.sol +++ b/script/03_PoolSwapTest.s.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: MIT pragma solidity ^0.8.22; import {Script} from "forge-std/Script.sol"; diff --git a/script/DeployPosm.s.sol b/script/DeployPosm.s.sol index 5bbb6184c..b87fbea0f 100644 --- a/script/DeployPosm.s.sol +++ b/script/DeployPosm.s.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "forge-std/console2.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/script/DeployQuoter.s.sol b/script/DeployQuoter.s.sol deleted file mode 100644 index 501f77611..000000000 --- a/script/DeployQuoter.s.sol +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.20; - -import "forge-std/console2.sol"; -import "forge-std/Script.sol"; - -import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import {Quoter} from "../src/lens/Quoter.sol"; - -contract DeployQuoter is Script { - function setUp() public {} - - function run(address poolManager) public returns (Quoter state) { - vm.startBroadcast(); - - // forge script --broadcast --sig 'run(address)' --rpc-url --private-key --verify script/DeployQuoter.s.sol:DeployQuoter - state = new Quoter(IPoolManager(poolManager)); - console2.log("Quoter", address(state)); - console2.log("PoolManager", address(state.poolManager())); - - vm.stopBroadcast(); - } -} diff --git a/script/DeployStateView.s.sol b/script/DeployStateView.s.sol index b48526bc9..9584099d2 100644 --- a/script/DeployStateView.s.sol +++ b/script/DeployStateView.s.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "forge-std/console2.sol"; diff --git a/script/DeployV4Quoter.s.sol b/script/DeployV4Quoter.s.sol new file mode 100644 index 000000000..7cc61d5f9 --- /dev/null +++ b/script/DeployV4Quoter.s.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "forge-std/console2.sol"; +import "forge-std/Script.sol"; + +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {V4Quoter} from "../src/lens/V4Quoter.sol"; + +contract DeployV4Quoter is Script { + function setUp() public {} + + function run(address poolManager) public returns (V4Quoter state) { + vm.startBroadcast(); + + // forge script --broadcast --sig 'run(address)' --rpc-url --private-key --verify script/DeployV4Quoter.s.sol:DeployV4Quoter + state = new V4Quoter(IPoolManager(poolManager)); + console2.log("V4Quoter", address(state)); + console2.log("PoolManager", address(state.poolManager())); + + vm.stopBroadcast(); + } +} diff --git a/src/PositionDescriptor.sol b/src/PositionDescriptor.sol index 7cf39d943..021cbd5e5 100644 --- a/src/PositionDescriptor.sol +++ b/src/PositionDescriptor.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: MIT pragma solidity 0.8.26; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; @@ -54,14 +54,17 @@ contract PositionDescriptor is IPositionDescriptor { } (, int24 tick,,) = poolManager.getSlot0(poolKey.toId()); + address currency0 = Currency.unwrap(poolKey.currency0); + address currency1 = Currency.unwrap(poolKey.currency1); + // If possible, flip currencies to get the larger currency as the base currency, so that the price (quote/base) is more readable // flip if currency0 priority is greater than currency1 priority - bool _flipRatio = flipRatio(Currency.unwrap(poolKey.currency0), Currency.unwrap(poolKey.currency1)); + bool _flipRatio = flipRatio(currency0, currency1); // If not flipped, quote currency is currency1, base currency is currency0 // If flipped, quote currency is currency0, base currency is currency1 - Currency quoteCurrency = !_flipRatio ? poolKey.currency1 : poolKey.currency0; - Currency baseCurrency = !_flipRatio ? poolKey.currency0 : poolKey.currency1; + address quoteCurrency = !_flipRatio ? currency1 : currency0; + address baseCurrency = !_flipRatio ? currency0 : currency1; return Descriptor.constructTokenURI( Descriptor.ConstructTokenURIParams({ @@ -115,8 +118,6 @@ contract PositionDescriptor is IPositionDescriptor { return CurrencyRatioSortOrder.DENOMINATOR_MORE; } else if (currency == WBTC) { return CurrencyRatioSortOrder.DENOMINATOR_MOST; - } else { - return 0; } } return 0; diff --git a/src/PositionManager.sol b/src/PositionManager.sol index d5721e346..f9dbd2e38 100644 --- a/src/PositionManager.sol +++ b/src/PositionManager.sol @@ -1,18 +1,19 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: MIT 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"; import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; import {TransientStateLibrary} from "@uniswap/v4-core/src/libraries/TransientStateLibrary.sol"; import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol"; -import {IPositionDescriptor} from "./interfaces/IPositionDescriptor.sol"; +import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; +import {IPositionDescriptor} from "./interfaces/IPositionDescriptor.sol"; import {ERC721Permit_v4} from "./base/ERC721Permit_v4.sol"; import {ReentrancyLock} from "./base/ReentrancyLock.sol"; import {IPositionManager} from "./interfaces/IPositionManager.sol"; @@ -26,6 +27,9 @@ 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 {LiquidityAmounts} from "./libraries/LiquidityAmounts.sol"; +import {NativeWrapper} from "./base/NativeWrapper.sol"; +import {IWETH9} from "./interfaces/external/IWETH9.sol"; // 444444444 // 444444444444 444444 @@ -102,7 +106,8 @@ contract PositionManager is ReentrancyLock, BaseActionsRouter, Notifier, - Permit2Forwarder + Permit2Forwarder, + NativeWrapper { using PoolIdLibrary for PoolKey; using StateLibrary for IPoolManager; @@ -126,12 +131,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 +160,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); } @@ -188,6 +201,11 @@ contract PositionManager is params.decodeModifyLiquidityParams(); _increase(tokenId, liquidity, amount0Max, amount1Max, hookData); return; + } else if (action == Actions.INCREASE_LIQUIDITY_FROM_DELTAS) { + (uint256 tokenId, uint128 amount0Max, uint128 amount1Max, bytes calldata hookData) = + params.decodeIncreaseLiquidityFromDeltasParams(); + _increaseFromDeltas(tokenId, amount0Max, amount1Max, hookData); + return; } else if (action == Actions.DECREASE_LIQUIDITY) { (uint256 tokenId, uint256 liquidity, uint128 amount0Min, uint128 amount1Min, bytes calldata hookData) = params.decodeModifyLiquidityParams(); @@ -206,6 +224,18 @@ contract PositionManager is ) = params.decodeMintParams(); _mint(poolKey, tickLower, tickUpper, liquidity, amount0Max, amount1Max, _mapRecipient(owner), hookData); return; + } else if (action == Actions.MINT_POSITION_FROM_DELTAS) { + ( + PoolKey calldata poolKey, + int24 tickLower, + int24 tickUpper, + uint128 amount0Max, + uint128 amount1Max, + address owner, + bytes calldata hookData + ) = params.decodeMintFromDeltasParams(); + _mintFromDeltas(poolKey, tickLower, tickUpper, amount0Max, amount1Max, _mapRecipient(owner), hookData); + return; } else if (action == Actions.BURN_POSITION) { // Will automatically decrease liquidity to 0 if the position is not already empty. (uint256 tokenId, uint128 amount0Min, uint128 amount1Min, bytes calldata hookData) = @@ -242,6 +272,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); @@ -264,6 +302,31 @@ contract PositionManager is (liquidityDelta - feesAccrued).validateMaxIn(amount0Max, amount1Max); } + /// @dev The liquidity delta is derived from open deltas in the pool manager. + function _increaseFromDeltas(uint256 tokenId, uint128 amount0Max, uint128 amount1Max, bytes calldata hookData) + internal + onlyIfApproved(msgSender(), tokenId) + { + (PoolKey memory poolKey, PositionInfo info) = getPoolAndPositionInfo(tokenId); + + (uint160 sqrtPriceX96,,,) = poolManager.getSlot0(poolKey.toId()); + + // Use the credit on the pool manager as the amounts for the mint. + uint256 liquidity = LiquidityAmounts.getLiquidityForAmounts( + sqrtPriceX96, + TickMath.getSqrtPriceAtTick(info.tickLower()), + TickMath.getSqrtPriceAtTick(info.tickUpper()), + _getFullCredit(poolKey.currency0), + _getFullCredit(poolKey.currency1) + ); + + // Note: The tokenId is used as the salt for this position, so every minted position has unique storage in the pool manager. + (BalanceDelta liquidityDelta, BalanceDelta feesAccrued) = + _modifyLiquidity(info, poolKey, liquidity.toInt256(), bytes32(tokenId), hookData); + // Slippage checks should be done on the principal liquidityDelta which is the liquidityDelta - feesAccrued + (liquidityDelta - feesAccrued).validateMaxIn(amount0Max, amount1Max); + } + /// @dev Calling decrease with 0 liquidity will credit the caller with any underlying fees of the position function _decrease( uint256 tokenId, @@ -316,6 +379,29 @@ contract PositionManager is liquidityDelta.validateMaxIn(amount0Max, amount1Max); } + function _mintFromDeltas( + PoolKey calldata poolKey, + int24 tickLower, + int24 tickUpper, + uint128 amount0Max, + uint128 amount1Max, + address owner, + bytes calldata hookData + ) internal { + (uint160 sqrtPriceX96,,,) = poolManager.getSlot0(poolKey.toId()); + + // Use the credit on the pool manager as the amounts for the mint. + uint256 liquidity = LiquidityAmounts.getLiquidityForAmounts( + sqrtPriceX96, + TickMath.getSqrtPriceAtTick(tickLower), + TickMath.getSqrtPriceAtTick(tickUpper), + _getFullCredit(poolKey.currency0), + _getFullCredit(poolKey.currency1) + ); + + _mint(poolKey, tickLower, tickUpper, liquidity, amount0Max, amount1Max, owner, hookData); + } + /// @dev this is overloaded with ERC721Permit_v4._burn function _burn(uint256 tokenId, uint128 amount0Min, uint128 amount1Min, bytes calldata hookData) internal @@ -325,20 +411,34 @@ contract PositionManager is uint256 liquidity = uint256(_getLiquidity(tokenId, poolKey, info.tickLower(), info.tickUpper())); + address owner = ownerOf(tokenId); + // Clear the position info. positionInfo[tokenId] = PositionInfoLibrary.EMPTY_POSITION_INFO; // Burn the token. _burn(tokenId); // Can only call modify if there is non zero liquidity. + BalanceDelta feesAccrued; if (liquidity > 0) { - (BalanceDelta liquidityDelta, BalanceDelta feesAccrued) = - _modifyLiquidity(info, poolKey, -(liquidity.toInt256()), bytes32(tokenId), hookData); + BalanceDelta liquidityDelta; + // do not use _modifyLiquidity as we do not need to notify on modification for burns. + (liquidityDelta, feesAccrued) = poolManager.modifyLiquidity( + poolKey, + IPoolManager.ModifyLiquidityParams({ + tickLower: info.tickLower(), + tickUpper: info.tickUpper(), + liquidityDelta: -(liquidity.toInt256()), + salt: bytes32(tokenId) + }), + hookData + ); // Slippage checks should be done on the principal liquidityDelta which is the liquidityDelta - feesAccrued (liquidityDelta - feesAccrued).validateMinOut(amount0Min, amount1Min); } - if (info.hasSubscriber()) _unsubscribe(tokenId); + // deletes then notifies the subscriber + if (info.hasSubscriber()) _removeSubscriberAndNotifyBurn(tokenId, owner, info, liquidity, feesAccrued); } function _settlePair(Currency currency0, Currency currency1) internal { @@ -363,15 +463,17 @@ 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)); } } /// @dev integrators may elect to forfeit positive deltas with clear /// if the forfeit amount exceeds the user-specified max, the amount is taken instead + /// if there is no credit, no call is made. function _clearOrTake(Currency currency, uint256 amountMax) internal { uint256 delta = _getFullCredit(currency); + if (delta == 0) return; // forfeit the delta if its less than or equal to the user-specified limit if (delta <= amountMax) { @@ -387,6 +489,7 @@ contract PositionManager is if (balance > 0) currency.transfer(to, balance); } + /// @dev if there is a subscriber attached to the position, this function will notify the subscriber function _modifyLiquidity( PositionInfo info, PoolKey memory poolKey, @@ -431,9 +534,10 @@ 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); + if (positionInfo[id].hasSubscriber()) _unsubscribe(id); } /// @inheritdoc IPositionManager diff --git a/src/UniswapV4DeployerCompetition.sol b/src/UniswapV4DeployerCompetition.sol new file mode 100644 index 000000000..deec4177e --- /dev/null +++ b/src/UniswapV4DeployerCompetition.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Create2} from "@openzeppelin/contracts/utils/Create2.sol"; +import {VanityAddressLib} from "./libraries/VanityAddressLib.sol"; +import {IUniswapV4DeployerCompetition} from "./interfaces/IUniswapV4DeployerCompetition.sol"; + +/// @title UniswapV4DeployerCompetition +/// @notice A contract to crowdsource a salt for the best Uniswap V4 address +contract UniswapV4DeployerCompetition is IUniswapV4DeployerCompetition { + using VanityAddressLib for address; + + /// @dev The salt for the best address found so far + bytes32 public bestAddressSalt; + /// @dev The submitter of the best address found so far + address public bestAddressSubmitter; + + /// @dev The deadline for the competition + uint256 public immutable competitionDeadline; + /// @dev The init code hash of the V4 contract + bytes32 public immutable initCodeHash; + + /// @dev The deployer who can initiate the deployment of the v4 PoolManager, until the exclusive deploy deadline. + /// @dev After this deadline anyone can deploy. + address public immutable deployer; + /// @dev The deadline for exclusive deployment by deployer after deadline + uint256 public immutable exclusiveDeployDeadline; + + constructor( + bytes32 _initCodeHash, + uint256 _competitionDeadline, + address _exclusiveDeployer, + uint256 _exclusiveDeployLength + ) { + initCodeHash = _initCodeHash; + competitionDeadline = _competitionDeadline; + exclusiveDeployDeadline = _competitionDeadline + _exclusiveDeployLength; + deployer = _exclusiveDeployer; + } + + /// @inheritdoc IUniswapV4DeployerCompetition + function updateBestAddress(bytes32 salt) external { + if (block.timestamp > competitionDeadline) { + revert CompetitionOver(block.timestamp, competitionDeadline); + } + + address saltSubAddress = address(bytes20(salt)); + if (saltSubAddress != msg.sender && saltSubAddress != address(0)) revert InvalidSender(salt, msg.sender); + + address newAddress = Create2.computeAddress(salt, initCodeHash); + address _bestAddress = bestAddress(); + if (!newAddress.betterThan(_bestAddress)) { + revert WorseAddress(newAddress, _bestAddress, newAddress.score(), _bestAddress.score()); + } + + bestAddressSalt = salt; + bestAddressSubmitter = msg.sender; + + emit NewAddressFound(newAddress, msg.sender, newAddress.score()); + } + + /// @inheritdoc IUniswapV4DeployerCompetition + function deploy(bytes memory bytecode) external { + if (keccak256(bytecode) != initCodeHash) { + revert InvalidBytecode(); + } + + if (block.timestamp <= competitionDeadline) { + revert CompetitionNotOver(block.timestamp, competitionDeadline); + } + + if (msg.sender != deployer && block.timestamp <= exclusiveDeployDeadline) { + // anyone can deploy after the deadline + revert NotAllowedToDeploy(msg.sender, deployer); + } + + // the owner of the contract must be encoded in the bytecode + Create2.deploy(0, bestAddressSalt, bytecode); + } + + /// @dev returns the best address found so far + function bestAddress() public view returns (address) { + return Create2.computeAddress(bestAddressSalt, initCodeHash); + } +} diff --git a/src/V4Router.sol b/src/V4Router.sol index 0aaf77155..e149da215 100644 --- a/src/V4Router.sol +++ b/src/V4Router.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: MIT pragma solidity 0.8.26; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; @@ -7,7 +7,6 @@ import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; -import {BipsLibrary} from "@uniswap/v4-core/src/libraries/BipsLibrary.sol"; import {PathKey, PathKeyLibrary} from "./libraries/PathKey.sol"; import {CalldataDecoder} from "./libraries/CalldataDecoder.sol"; @@ -16,6 +15,7 @@ import {BaseActionsRouter} from "./base/BaseActionsRouter.sol"; import {DeltaResolver} from "./base/DeltaResolver.sol"; import {Actions} from "./libraries/Actions.sol"; import {ActionConstants} from "./libraries/ActionConstants.sol"; +import {BipsLibrary} from "./libraries/BipsLibrary.sol"; /// @title UniswapV4Router /// @notice Abstract contract that contains all internal logic needed for routing through Uniswap v4 pools @@ -50,12 +50,7 @@ abstract contract V4Router is IV4Router, BaseActionsRouter, DeltaResolver { return; } } else { - if (action == Actions.SETTLE_TAKE_PAIR) { - (Currency settleCurrency, Currency takeCurrency) = params.decodeCurrencyPair(); - _settle(settleCurrency, msgSender(), _getFullDebt(settleCurrency)); - _take(takeCurrency, msgSender(), _getFullCredit(takeCurrency)); - return; - } else if (action == Actions.SETTLE_ALL) { + if (action == Actions.SETTLE_ALL) { (Currency currency, uint256 maxAmount) = params.decodeCurrencyAndUint256(); uint256 amount = _getFullDebt(currency); if (amount > maxAmount) revert V4TooMuchRequested(maxAmount, amount); @@ -90,9 +85,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 +104,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 +121,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 +143,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 +153,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/BaseActionsRouter.sol b/src/base/BaseActionsRouter.sol index 56e311906..8cb30f131 100644 --- a/src/base/BaseActionsRouter.sol +++ b/src/base/BaseActionsRouter.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; diff --git a/src/base/BaseV4Quoter.sol b/src/base/BaseV4Quoter.sol index 312cf7d15..52bacdb3f 100644 --- a/src/base/BaseV4Quoter.sol +++ b/src/base/BaseV4Quoter.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: MIT import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; diff --git a/src/base/DeltaResolver.sol b/src/base/DeltaResolver.sol index ccde3d4d5..bd0d39bf0 100644 --- a/src/base/DeltaResolver.sol +++ b/src/base/DeltaResolver.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import {Currency} from "@uniswap/v4-core/src/types/Currency.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,11 +34,14 @@ 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; + + poolManager.sync(currency); if (currency.isAddressZero()) { poolManager.settle{value: amount}(); } else { - poolManager.sync(currency); _pay(currency, payer, amount); poolManager.settle(); } @@ -87,4 +94,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/ERC721Permit_v4.sol b/src/base/ERC721Permit_v4.sol index cb074c9c8..e6ded04dd 100644 --- a/src/base/ERC721Permit_v4.sol +++ b/src/base/ERC721Permit_v4.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {ERC721} from "solmate/src/tokens/ERC721.sol"; diff --git a/src/base/ImmutableState.sol b/src/base/ImmutableState.sol index 8e30f1a4b..708a3d281 100644 --- a/src/base/ImmutableState.sol +++ b/src/base/ImmutableState.sol @@ -1,12 +1,13 @@ -// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {IImmutableState} from "../interfaces/IImmutableState.sol"; /// @title Immutable State /// @notice A collection of immutable state variables, commonly used across multiple contracts -contract ImmutableState { - /// @notice The Uniswap v4 PoolManager contract +contract ImmutableState is IImmutableState { + /// @inheritdoc IImmutableState IPoolManager public immutable poolManager; constructor(IPoolManager _poolManager) { diff --git a/src/base/Multicall_v4.sol b/src/base/Multicall_v4.sol index e632270af..e4fb7dacb 100644 --- a/src/base/Multicall_v4.sol +++ b/src/base/Multicall_v4.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {IMulticall_v4} from "../interfaces/IMulticall_v4.sol"; diff --git a/src/base/NativeWrapper.sol b/src/base/NativeWrapper.sol new file mode 100644 index 000000000..eb4eba073 --- /dev/null +++ b/src/base/NativeWrapper.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +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 558e85b7f..7e755c660 100644 --- a/src/base/Notifier.sol +++ b/src/base/Notifier.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {ISubscriber} from "../interfaces/ISubscriber.sol"; @@ -9,7 +9,7 @@ import {PositionInfo} from "../libraries/PositionInfoLibrary.sol"; /// @notice Notifier is used to opt in to sending updates to external contracts about position modifications or transfers abstract contract Notifier is INotifier { - using CustomRevert for bytes4; + using CustomRevert for *; ISubscriber private constant NO_SUBSCRIBER = ISubscriber(address(0)); @@ -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]; @@ -49,14 +53,19 @@ abstract contract Notifier is INotifier { bool success = _call(newSubscriber, abi.encodeCall(ISubscriber.notifySubscribe, (tokenId, data))); if (!success) { - Wrap__SubscriptionReverted.selector.bubbleUpAndRevertWith(newSubscriber); + newSubscriber.bubbleUpAndRevertWith(ISubscriber.notifySubscribe.selector, SubscriptionReverted.selector); } emit Subscription(tokenId, newSubscriber); } /// @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); } @@ -79,27 +88,38 @@ abstract contract Notifier is INotifier { emit Unsubscription(tokenId, address(_subscriber)); } - function _notifyModifyLiquidity(uint256 tokenId, int256 liquidityChange, BalanceDelta feesAccrued) internal { - ISubscriber _subscriber = subscriber[tokenId]; + /// @dev note this function also deletes the subscriber address from the mapping + function _removeSubscriberAndNotifyBurn( + uint256 tokenId, + address owner, + PositionInfo info, + uint256 liquidity, + BalanceDelta feesAccrued + ) internal { + address _subscriber = address(subscriber[tokenId]); + + // remove the subscriber + delete subscriber[tokenId]; - bool success = _call( - address(_subscriber), - abi.encodeCall(ISubscriber.notifyModifyLiquidity, (tokenId, liquidityChange, feesAccrued)) - ); + bool success = + _call(_subscriber, abi.encodeCall(ISubscriber.notifyBurn, (tokenId, owner, info, liquidity, feesAccrued))); if (!success) { - Wrap__ModifyLiquidityNotificationReverted.selector.bubbleUpAndRevertWith(address(_subscriber)); + _subscriber.bubbleUpAndRevertWith(ISubscriber.notifyBurn.selector, BurnNotificationReverted.selector); } } - function _notifyTransfer(uint256 tokenId, address previousOwner, address newOwner) internal { - ISubscriber _subscriber = subscriber[tokenId]; + function _notifyModifyLiquidity(uint256 tokenId, int256 liquidityChange, BalanceDelta feesAccrued) internal { + address _subscriber = address(subscriber[tokenId]); - bool success = - _call(address(_subscriber), abi.encodeCall(ISubscriber.notifyTransfer, (tokenId, previousOwner, newOwner))); + bool success = _call( + _subscriber, abi.encodeCall(ISubscriber.notifyModifyLiquidity, (tokenId, liquidityChange, feesAccrued)) + ); if (!success) { - Wrap__TransferNotificationReverted.selector.bubbleUpAndRevertWith(address(_subscriber)); + _subscriber.bubbleUpAndRevertWith( + ISubscriber.notifyModifyLiquidity.selector, ModifyLiquidityNotificationReverted.selector + ); } } diff --git a/src/base/Permit2Forwarder.sol b/src/base/Permit2Forwarder.sol index c90b406a3..d5a749a85 100644 --- a/src/base/Permit2Forwarder.sol +++ b/src/base/Permit2Forwarder.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol"; diff --git a/src/base/PoolInitializer.sol b/src/base/PoolInitializer.sol index 445248e18..fea603402 100644 --- a/src/base/PoolInitializer.sol +++ b/src/base/PoolInitializer.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {ImmutableState} from "./ImmutableState.sol"; @@ -10,9 +10,15 @@ import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; /// @dev Enables create pool + mint liquidity in a single transaction with multicall abstract contract PoolInitializer is ImmutableState { /// @notice Initialize a Uniswap v4 Pool + /// @dev If the pool is already initialized, this function will not revert and just return type(int24).max /// @param key the PoolKey of the pool to initialize /// @param sqrtPriceX96 the initial sqrtPriceX96 of the pool + /// @return tick The current tick of the pool function initializePool(PoolKey calldata key, uint160 sqrtPriceX96) external payable returns (int24) { - return poolManager.initialize(key, sqrtPriceX96); + try poolManager.initialize(key, sqrtPriceX96) returns (int24 tick) { + return tick; + } catch { + return type(int24).max; + } } } diff --git a/src/base/ReentrancyLock.sol b/src/base/ReentrancyLock.sol index 29fa3d34f..c1abf028f 100644 --- a/src/base/ReentrancyLock.sol +++ b/src/base/ReentrancyLock.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import {Locker} from "../libraries/Locker.sol"; diff --git a/src/base/SafeCallback.sol b/src/base/SafeCallback.sol index cda2b4a04..45e1c6bf9 100644 --- a/src/base/SafeCallback.sol +++ b/src/base/SafeCallback.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {IUnlockCallback} from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol"; diff --git a/src/base/UnorderedNonce.sol b/src/base/UnorderedNonce.sol index fc33b63d8..b08b5d92c 100644 --- a/src/base/UnorderedNonce.sol +++ b/src/base/UnorderedNonce.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; /// @title Unordered Nonce diff --git a/src/base/hooks/BaseHook.sol b/src/base/hooks/BaseHook.sol index c53f9b753..635602a63 100644 --- a/src/base/hooks/BaseHook.sol +++ b/src/base/hooks/BaseHook.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; diff --git a/src/interfaces/IERC721Permit_v4.sol b/src/interfaces/IERC721Permit_v4.sol index bc4c7aa06..e15e00bd8 100644 --- a/src/interfaces/IERC721Permit_v4.sol +++ b/src/interfaces/IERC721Permit_v4.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; /// @title ERC721 with permit diff --git a/src/interfaces/IImmutableState.sol b/src/interfaces/IImmutableState.sol new file mode 100644 index 000000000..83f7cf034 --- /dev/null +++ b/src/interfaces/IImmutableState.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; + +/// @title Interface for ImmutableState +interface IImmutableState { + /// @notice The Uniswap v4 PoolManager contract + function poolManager() external view returns (IPoolManager); +} diff --git a/src/interfaces/IMulticall_v4.sol b/src/interfaces/IMulticall_v4.sol index 1d053a97d..07c321b32 100644 --- a/src/interfaces/IMulticall_v4.sol +++ b/src/interfaces/IMulticall_v4.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; /// @title Multicall_v4 interface diff --git a/src/interfaces/INotifier.sol b/src/interfaces/INotifier.sol index d6cca9083..a415e27b6 100644 --- a/src/interfaces/INotifier.sol +++ b/src/interfaces/INotifier.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {ISubscriber} from "./ISubscriber.sol"; @@ -12,11 +12,11 @@ interface INotifier { /// @notice Thrown when a user specifies a gas limit too low to avoid valid unsubscribe notifications error GasLimitTooLow(); /// @notice Wraps the revert message of the subscriber contract on a reverting subscription - error Wrap__SubscriptionReverted(address subscriber, bytes reason); + error SubscriptionReverted(address subscriber, bytes reason); /// @notice Wraps the revert message of the subscriber contract on a reverting modify liquidity notification - error Wrap__ModifyLiquidityNotificationReverted(address subscriber, bytes reason); - /// @notice Wraps the revert message of the subscriber contract on a reverting transfer notification - error Wrap__TransferNotificationReverted(address subscriber, bytes reason); + error ModifyLiquidityNotificationReverted(address subscriber, bytes reason); + /// @notice Wraps the revert message of the subscriber contract on a reverting burn notification + error BurnNotificationReverted(address subscriber, bytes reason); /// @notice Thrown when a tokenId already has a subscriber error AlreadySubscribed(uint256 tokenId, address subscriber); @@ -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/IPositionDescriptor.sol b/src/interfaces/IPositionDescriptor.sol index 8c3227324..a1736ceab 100644 --- a/src/interfaces/IPositionDescriptor.sol +++ b/src/interfaces/IPositionDescriptor.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import "./IPositionManager.sol"; diff --git a/src/interfaces/IPositionManager.sol b/src/interfaces/IPositionManager.sol index 616b51ccb..86df7bbf9 100644 --- a/src/interfaces/IPositionManager.sol +++ b/src/interfaces/IPositionManager.sol @@ -1,18 +1,22 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {PositionInfo} from "../libraries/PositionInfoLibrary.sol"; import {INotifier} from "./INotifier.sol"; +import {IImmutableState} from "./IImmutableState.sol"; /// @title IPositionManager /// @notice Interface for the PositionManager contract -interface IPositionManager is INotifier { +interface IPositionManager is INotifier, IImmutableState { /// @notice Thrown when the caller is not approved to modify a position 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/ISubscriber.sol b/src/interfaces/ISubscriber.sol index 1e6f04762..f2fc94df6 100644 --- a/src/interfaces/ISubscriber.sol +++ b/src/interfaces/ISubscriber.sol @@ -1,7 +1,8 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {PositionInfo} from "../libraries/PositionInfoLibrary.sol"; /// @notice Interface that a Subscriber contract should implement to receive updates from the v4 position manager interface ISubscriber { @@ -13,12 +14,16 @@ interface ISubscriber { /// @dev Because of EIP-150, solidity may only allocate 63/64 of gasleft() /// @param tokenId the token ID of the position function notifyUnsubscribe(uint256 tokenId) external; + /// @notice Called when a position is burned + /// @param tokenId the token ID of the position + /// @param owner the current owner of the tokenId + /// @param info information about the position + /// @param liquidity the amount of liquidity decreased in the position, may be 0 + /// @param feesAccrued the fees accrued by the position if liquidity was decreased + function notifyBurn(uint256 tokenId, address owner, PositionInfo info, uint256 liquidity, BalanceDelta feesAccrued) + external; /// @param tokenId the token ID of the position /// @param liquidityChange the change in liquidity on the underlying position /// @param feesAccrued the fees to be collected from the position as a result of the modifyLiquidity call function notifyModifyLiquidity(uint256 tokenId, int256 liquidityChange, BalanceDelta feesAccrued) external; - /// @param tokenId the token ID of the position - /// @param previousOwner address of the old owner - /// @param newOwner address of the new owner - function notifyTransfer(uint256 tokenId, address previousOwner, address newOwner) external; } diff --git a/src/interfaces/IUniswapV4DeployerCompetition.sol b/src/interfaces/IUniswapV4DeployerCompetition.sol new file mode 100644 index 000000000..5bb1a4be1 --- /dev/null +++ b/src/interfaces/IUniswapV4DeployerCompetition.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +/// @title UniswapV4DeployerCompetition +/// @notice A competition to deploy the UniswapV4 contract with the best address +interface IUniswapV4DeployerCompetition { + event NewAddressFound(address indexed bestAddress, address indexed submitter, uint256 score); + + error InvalidBytecode(); + error CompetitionNotOver(uint256 currentTime, uint256 deadline); + error CompetitionOver(uint256 currentTime, uint256 deadline); + error NotAllowedToDeploy(address sender, address deployer); + error WorseAddress(address newAddress, address bestAddress, uint256 newScore, uint256 bestScore); + error InvalidSender(bytes32 salt, address sender); + + /// @notice Updates the best address if the new address has a better vanity score + /// @param salt The salt to use to compute the new address with CREATE2 + /// @dev The first 20 bytes of the salt must be either address(0) or msg.sender + function updateBestAddress(bytes32 salt) external; + + /// @notice deploys the Uniswap v4 PoolManager contract + /// @param bytecode The bytecode of the Uniswap v4 PoolManager contract + /// @dev The bytecode must match the initCodeHash + function deploy(bytes memory bytecode) external; +} diff --git a/src/interfaces/IQuoter.sol b/src/interfaces/IV4Quoter.sol similarity index 97% rename from src/interfaces/IQuoter.sol rename to src/interfaces/IV4Quoter.sol index 9815d74b7..f502a4f44 100644 --- a/src/interfaces/IQuoter.sol +++ b/src/interfaces/IV4Quoter.sol @@ -1,16 +1,16 @@ -// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; import {PathKey} from "../libraries/PathKey.sol"; -/// @title Quoter Interface +/// @title V4 Quoter Interface /// @notice Supports quoting the delta amounts for exact input or exact output swaps. /// @notice For each pool also tells you the sqrt price of the pool after the swap. /// @dev These functions are not marked view because they rely on calling non-view functions and reverting /// to compute the result. They are also not gas efficient and should not be called on-chain. -interface IQuoter { +interface IV4Quoter { struct QuoteExactSingleParams { PoolKey poolKey; bool zeroForOne; diff --git a/src/interfaces/IV4Router.sol b/src/interfaces/IV4Router.sol index 13ed4775f..f42715aa0 100644 --- a/src/interfaces/IV4Router.sol +++ b/src/interfaces/IV4Router.sol @@ -1,13 +1,14 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; import {PathKey} from "../libraries/PathKey.sol"; +import {IImmutableState} from "./IImmutableState.sol"; /// @title IV4Router /// @notice Interface containing all the structs and errors for different v4 swap types -interface IV4Router { +interface IV4Router is IImmutableState { /// @notice Emitted when an exactInput swap does not receive its minAmountOut error V4TooLittleReceived(uint256 minAmountOutReceived, uint256 amountReceived); /// @notice Emitted when an exactOutput is asked for more than its maxAmountIn @@ -19,7 +20,6 @@ interface IV4Router { bool zeroForOne; uint128 amountIn; uint128 amountOutMinimum; - uint160 sqrtPriceLimitX96; bytes hookData; } @@ -37,7 +37,6 @@ interface IV4Router { 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 000000000..b8e68b363 --- /dev/null +++ b/src/interfaces/external/IWETH9.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT +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/lens/StateView.sol b/src/lens/StateView.sol index f361b905a..6527d2f72 100644 --- a/src/lens/StateView.sol +++ b/src/lens/StateView.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; diff --git a/src/lens/Quoter.sol b/src/lens/V4Quoter.sol similarity index 95% rename from src/lens/Quoter.sol rename to src/lens/V4Quoter.sol index fd1597978..0f6b78648 100644 --- a/src/lens/Quoter.sol +++ b/src/lens/V4Quoter.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; @@ -6,18 +6,18 @@ import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; -import {IQuoter} from "../interfaces/IQuoter.sol"; +import {IV4Quoter} from "../interfaces/IV4Quoter.sol"; import {PathKey, PathKeyLibrary} from "../libraries/PathKey.sol"; import {QuoterRevert} from "../libraries/QuoterRevert.sol"; import {BaseV4Quoter} from "../base/BaseV4Quoter.sol"; -contract Quoter is IQuoter, BaseV4Quoter { +contract V4Quoter is IV4Quoter, BaseV4Quoter { using PathKeyLibrary for PathKey; using QuoterRevert for *; constructor(IPoolManager _poolManager) BaseV4Quoter(_poolManager) {} - /// @inheritdoc IQuoter + /// @inheritdoc IV4Quoter function quoteExactInputSingle(QuoteExactSingleParams memory params) external returns (uint256 amountOut, uint256 gasEstimate) @@ -31,7 +31,7 @@ contract Quoter is IQuoter, BaseV4Quoter { } } - /// @inheritdoc IQuoter + /// @inheritdoc IV4Quoter function quoteExactInput(QuoteExactParams memory params) external returns (uint256 amountOut, uint256 gasEstimate) @@ -45,7 +45,7 @@ contract Quoter is IQuoter, BaseV4Quoter { } } - /// @inheritdoc IQuoter + /// @inheritdoc IV4Quoter function quoteExactOutputSingle(QuoteExactSingleParams memory params) external returns (uint256 amountIn, uint256 gasEstimate) @@ -59,7 +59,7 @@ contract Quoter is IQuoter, BaseV4Quoter { } } - /// @inheritdoc IQuoter + /// @inheritdoc IV4Quoter function quoteExactOutput(QuoteExactParams memory params) external returns (uint256 amountIn, uint256 gasEstimate) diff --git a/src/libraries/ActionConstants.sol b/src/libraries/ActionConstants.sol index 914528b02..4c84e11a1 100644 --- a/src/libraries/ActionConstants.sol +++ b/src/libraries/ActionConstants.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; /// @title Action Constants diff --git a/src/libraries/Actions.sol b/src/libraries/Actions.sol index 49d3e04f1..f87038476 100644 --- a/src/libraries/Actions.sol +++ b/src/libraries/Actions.sol @@ -1,8 +1,9 @@ -// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; /// @notice Library to define different pool actions. /// @dev These are suggested common commands, however additional commands should be defined as required +/// Some of these actions are not supported in the Router contracts or Position Manager contracts, but are left as they may be helpful commands for other peripheral contracts. library Actions { // pool actions // liquidity actions @@ -10,31 +11,39 @@ library Actions { uint256 constant DECREASE_LIQUIDITY = 0x01; uint256 constant MINT_POSITION = 0x02; uint256 constant BURN_POSITION = 0x03; + uint256 constant INCREASE_LIQUIDITY_FROM_DELTAS = 0x04; + uint256 constant MINT_POSITION_FROM_DELTAS = 0x05; + // swapping - uint256 constant SWAP_EXACT_IN_SINGLE = 0x04; - uint256 constant SWAP_EXACT_IN = 0x05; - uint256 constant SWAP_EXACT_OUT_SINGLE = 0x06; - uint256 constant SWAP_EXACT_OUT = 0x07; + uint256 constant SWAP_EXACT_IN_SINGLE = 0x06; + uint256 constant SWAP_EXACT_IN = 0x07; + uint256 constant SWAP_EXACT_OUT_SINGLE = 0x08; + uint256 constant SWAP_EXACT_OUT = 0x09; + // donate - uint256 constant DONATE = 0x08; + // note this is not supported in the position manager or router + uint256 constant DONATE = 0x0a; // closing deltas on the pool manager // settling - uint256 constant SETTLE = 0x09; - uint256 constant SETTLE_ALL = 0x10; - uint256 constant SETTLE_PAIR = 0x11; + uint256 constant SETTLE = 0x0b; + uint256 constant SETTLE_ALL = 0x0c; + uint256 constant SETTLE_PAIR = 0x0d; // taking - uint256 constant TAKE = 0x12; - uint256 constant TAKE_ALL = 0x13; - uint256 constant TAKE_PORTION = 0x14; - uint256 constant TAKE_PAIR = 0x15; + uint256 constant TAKE = 0x0e; + uint256 constant TAKE_ALL = 0x0f; + uint256 constant TAKE_PORTION = 0x10; + uint256 constant TAKE_PAIR = 0x11; + + uint256 constant CLOSE_CURRENCY = 0x12; + uint256 constant CLEAR_OR_TAKE = 0x13; + uint256 constant SWEEP = 0x14; - uint256 constant SETTLE_TAKE_PAIR = 0x16; - uint256 constant CLOSE_CURRENCY = 0x17; - uint256 constant CLEAR_OR_TAKE = 0x18; - uint256 constant SWEEP = 0x19; + uint256 constant WRAP = 0x15; + uint256 constant UNWRAP = 0x16; // minting/burning 6909s to close deltas - uint256 constant MINT_6909 = 0x20; - uint256 constant BURN_6909 = 0x21; + // note this is not supported in the position manager or router + uint256 constant MINT_6909 = 0x17; + uint256 constant BURN_6909 = 0x18; } diff --git a/src/libraries/AddressStringUtil.sol b/src/libraries/AddressStringUtil.sol index 999bc2d95..3add136f8 100644 --- a/src/libraries/AddressStringUtil.sol +++ b/src/libraries/AddressStringUtil.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; /// @title AddressStringUtil diff --git a/src/libraries/BipsLibrary.sol b/src/libraries/BipsLibrary.sol new file mode 100644 index 000000000..d85486a82 --- /dev/null +++ b/src/libraries/BipsLibrary.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/// @title For calculating a percentage of an amount, using bips +library BipsLibrary { + uint256 internal constant BPS_DENOMINATOR = 10_000; + + /// @notice emitted when an invalid percentage is provided + error InvalidBips(); + + /// @param amount The total amount to calculate a percentage of + /// @param bips The percentage to calculate, in bips + function calculatePortion(uint256 amount, uint256 bips) internal pure returns (uint256) { + if (bips > BPS_DENOMINATOR) revert InvalidBips(); + return (amount * bips) / BPS_DENOMINATOR; + } +} diff --git a/src/libraries/CalldataDecoder.sol b/src/libraries/CalldataDecoder.sol index 00cf6e33e..496852bdc 100644 --- a/src/libraries/CalldataDecoder.sol +++ b/src/libraries/CalldataDecoder.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; @@ -76,6 +76,7 @@ library CalldataDecoder { pure returns (uint256 tokenId, uint256 liquidity, uint128 amount0, uint128 amount1, bytes calldata hookData) { + // no length check performed, as there is a length check in `toBytes` assembly ("memory-safe") { tokenId := calldataload(params.offset) liquidity := calldataload(add(params.offset, 0x20)) @@ -86,6 +87,22 @@ library CalldataDecoder { hookData = params.toBytes(4); } + /// @dev equivalent to: abi.decode(params, (uint256, uint128, uint128, bytes)) in calldata + function decodeIncreaseLiquidityFromDeltasParams(bytes calldata params) + internal + pure + returns (uint256 tokenId, uint128 amount0Max, uint128 amount1Max, bytes calldata hookData) + { + // no length check performed, as there is a length check in `toBytes` + assembly ("memory-safe") { + tokenId := calldataload(params.offset) + amount0Max := calldataload(add(params.offset, 0x20)) + amount1Max := calldataload(add(params.offset, 0x40)) + } + + hookData = params.toBytes(3); + } + /// @dev equivalent to: abi.decode(params, (PoolKey, int24, int24, uint256, uint128, uint128, address, bytes)) in calldata function decodeMintParams(bytes calldata params) internal @@ -101,6 +118,7 @@ library CalldataDecoder { bytes calldata hookData ) { + // no length check performed, as there is a length check in `toBytes` assembly ("memory-safe") { poolKey := params.offset tickLower := calldataload(add(params.offset, 0xa0)) @@ -113,12 +131,40 @@ library CalldataDecoder { hookData = params.toBytes(11); } + /// @dev equivalent to: abi.decode(params, (PoolKey, int24, int24, uint128, uint128, address, bytes)) in calldata + function decodeMintFromDeltasParams(bytes calldata params) + internal + pure + returns ( + PoolKey calldata poolKey, + int24 tickLower, + int24 tickUpper, + uint128 amount0Max, + uint128 amount1Max, + address owner, + bytes calldata hookData + ) + { + // no length check performed, as there is a length check in `toBytes` + assembly ("memory-safe") { + poolKey := params.offset + tickLower := calldataload(add(params.offset, 0xa0)) + tickUpper := calldataload(add(params.offset, 0xc0)) + amount0Max := calldataload(add(params.offset, 0xe0)) + amount1Max := calldataload(add(params.offset, 0x100)) + owner := calldataload(add(params.offset, 0x120)) + } + + hookData = params.toBytes(10); + } + /// @dev equivalent to: abi.decode(params, (uint256, uint128, uint128, bytes)) in calldata function decodeBurnParams(bytes calldata params) internal pure returns (uint256 tokenId, uint128 amount0Min, uint128 amount1Min, bytes calldata hookData) { + // no length check performed, as there is a length check in `toBytes` assembly ("memory-safe") { tokenId := calldataload(params.offset) amount0Min := calldataload(add(params.offset, 0x20)) @@ -136,6 +182,12 @@ library CalldataDecoder { { // ExactInputParams is a variable length struct so we just have to look up its location assembly ("memory-safe") { + // only safety checks for the minimum length, where path is empty + // 0xa0 = 5 * 0x20 -> 3 elements, path offset, and path length 0 + if lt(params.length, 0xa0) { + mstore(0, SLICE_ERROR_SELECTOR) + revert(0x1c, 4) + } swapParams := add(params.offset, calldataload(params.offset)) } } @@ -148,6 +200,12 @@ library CalldataDecoder { { // ExactInputSingleParams is a variable length struct so we just have to look up its location assembly ("memory-safe") { + // only safety checks for the minimum length, where hookData is empty + // 0x140 = 10 * 0x20 -> 8 elements, bytes offset, and bytes length 0 + if lt(params.length, 0x140) { + mstore(0, SLICE_ERROR_SELECTOR) + revert(0x1c, 4) + } swapParams := add(params.offset, calldataload(params.offset)) } } @@ -160,6 +218,12 @@ library CalldataDecoder { { // ExactOutputParams is a variable length struct so we just have to look up its location assembly ("memory-safe") { + // only safety checks for the minimum length, where path is empty + // 0xa0 = 5 * 0x20 -> 3 elements, path offset, and path length 0 + if lt(params.length, 0xa0) { + mstore(0, SLICE_ERROR_SELECTOR) + revert(0x1c, 4) + } swapParams := add(params.offset, calldataload(params.offset)) } } @@ -172,6 +236,12 @@ library CalldataDecoder { { // ExactOutputSingleParams is a variable length struct so we just have to look up its location assembly ("memory-safe") { + // only safety checks for the minimum length, where hookData is empty + // 0x140 = 10 * 0x20 -> 8 elements, bytes offset, and bytes length 0 + if lt(params.length, 0x140) { + mstore(0, SLICE_ERROR_SELECTOR) + revert(0x1c, 4) + } swapParams := add(params.offset, calldataload(params.offset)) } } @@ -179,6 +249,10 @@ library CalldataDecoder { /// @dev equivalent to: abi.decode(params, (Currency)) in calldata function decodeCurrency(bytes calldata params) internal pure returns (Currency currency) { assembly ("memory-safe") { + if lt(params.length, 0x20) { + mstore(0, SLICE_ERROR_SELECTOR) + revert(0x1c, 4) + } currency := calldataload(params.offset) } } @@ -186,6 +260,10 @@ library CalldataDecoder { /// @dev equivalent to: abi.decode(params, (Currency, Currency)) in calldata function decodeCurrencyPair(bytes calldata params) internal pure returns (Currency currency0, Currency currency1) { assembly ("memory-safe") { + if lt(params.length, 0x40) { + mstore(0, SLICE_ERROR_SELECTOR) + revert(0x1c, 4) + } currency0 := calldataload(params.offset) currency1 := calldataload(add(params.offset, 0x20)) } @@ -198,6 +276,10 @@ library CalldataDecoder { returns (Currency currency0, Currency currency1, address _address) { assembly ("memory-safe") { + if lt(params.length, 0x60) { + mstore(0, SLICE_ERROR_SELECTOR) + revert(0x1c, 4) + } currency0 := calldataload(params.offset) currency1 := calldataload(add(params.offset, 0x20)) _address := calldataload(add(params.offset, 0x40)) @@ -211,6 +293,10 @@ library CalldataDecoder { returns (Currency currency, address _address) { assembly ("memory-safe") { + if lt(params.length, 0x40) { + mstore(0, SLICE_ERROR_SELECTOR) + revert(0x1c, 4) + } currency := calldataload(params.offset) _address := calldataload(add(params.offset, 0x20)) } @@ -223,6 +309,10 @@ library CalldataDecoder { returns (Currency currency, address _address, uint256 amount) { assembly ("memory-safe") { + if lt(params.length, 0x60) { + mstore(0, SLICE_ERROR_SELECTOR) + revert(0x1c, 4) + } currency := calldataload(params.offset) _address := calldataload(add(params.offset, 0x20)) amount := calldataload(add(params.offset, 0x40)) @@ -236,11 +326,26 @@ library CalldataDecoder { returns (Currency currency, uint256 amount) { assembly ("memory-safe") { + if lt(params.length, 0x40) { + mstore(0, SLICE_ERROR_SELECTOR) + revert(0x1c, 4) + } currency := calldataload(params.offset) amount := calldataload(add(params.offset, 0x20)) } } + /// @dev equivalent to: abi.decode(params, (uint256)) in calldata + function decodeUint256(bytes calldata params) internal pure returns (uint256 amount) { + assembly ("memory-safe") { + if lt(params.length, 0x20) { + mstore(0, SLICE_ERROR_SELECTOR) + revert(0x1c, 4) + } + amount := calldataload(params.offset) + } + } + /// @dev equivalent to: abi.decode(params, (Currency, uint256, bool)) in calldata function decodeCurrencyUint256AndBool(bytes calldata params) internal @@ -248,6 +353,10 @@ library CalldataDecoder { returns (Currency currency, uint256 amount, bool boolean) { assembly ("memory-safe") { + if lt(params.length, 0x60) { + mstore(0, SLICE_ERROR_SELECTOR) + revert(0x1c, 4) + } currency := calldataload(params.offset) amount := calldataload(add(params.offset, 0x20)) boolean := calldataload(add(params.offset, 0x40)) diff --git a/src/libraries/CurrencyRatioSortOrder.sol b/src/libraries/CurrencyRatioSortOrder.sol index 1f3a719a4..c02d1084f 100644 --- a/src/libraries/CurrencyRatioSortOrder.sol +++ b/src/libraries/CurrencyRatioSortOrder.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; /// @title CurrencyRatioSortOrder diff --git a/src/libraries/Descriptor.sol b/src/libraries/Descriptor.sol index 527e7bd8b..327937c8d 100644 --- a/src/libraries/Descriptor.sol +++ b/src/libraries/Descriptor.sol @@ -1,7 +1,6 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; import {FullMath} from "@uniswap/v4-core/src/libraries/FullMath.sol"; import {LPFeeLibrary} from "@uniswap/v4-core/src/libraries/LPFeeLibrary.sol"; @@ -23,8 +22,8 @@ library Descriptor { struct ConstructTokenURIParams { uint256 tokenId; - Currency quoteCurrency; - Currency baseCurrency; + address quoteCurrency; + address baseCurrency; string quoteCurrencySymbol; string baseCurrencySymbol; uint8 quoteCurrencyDecimals; @@ -45,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.quoteCurrencySymbol), - escapeQuotes(params.baseCurrencySymbol), + escapeSpecialCharacters(params.quoteCurrencySymbol), + escapeSpecialCharacters(params.baseCurrencySymbol), addressToString(params.poolManager) ); string memory descriptionPartTwo = generateDescriptionPartTwo( params.tokenId.toString(), - escapeQuotes(params.baseCurrencySymbol), - addressToString(Currency.unwrap(params.quoteCurrency)), - addressToString(Currency.unwrap(params.baseCurrency)), - addressToString(params.hooks), + escapeSpecialCharacters(params.baseCurrencySymbol), + params.quoteCurrency == address(0) ? "Native" : addressToString(params.quoteCurrency), + params.baseCurrency == address(0) ? "Native" : addressToString(params.baseCurrency), + params.hooks == address(0) ? "No Hook" : addressToString(params.hooks), feeToPercentString(params.fee) ); string memory image = Base64.encode(bytes(generateSVGImage(params))); @@ -81,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 @@ -186,9 +185,9 @@ library Descriptor { "Uniswap - ", feeTier, " - ", - escapeQuotes(params.quoteCurrencySymbol), + escapeSpecialCharacters(params.quoteCurrencySymbol), "/", - escapeQuotes(params.baseCurrencySymbol), + escapeSpecialCharacters(params.baseCurrencySymbol), " - ", tickToDecimalString( !params.flipRatio ? params.tickLower : params.tickUpper, @@ -462,8 +461,8 @@ library Descriptor { /// @return svg The SVG image as a string function generateSVGImage(ConstructTokenURIParams memory params) internal pure returns (string memory svg) { SVG.SVGParams memory svgParams = SVG.SVGParams({ - quoteCurrency: addressToString(Currency.unwrap(params.quoteCurrency)), - baseCurrency: addressToString(Currency.unwrap(params.baseCurrency)), + quoteCurrency: addressToString(params.quoteCurrency), + baseCurrency: addressToString(params.baseCurrency), hooks: params.hooks, quoteCurrencySymbol: params.quoteCurrencySymbol, baseCurrencySymbol: params.baseCurrencySymbol, @@ -473,16 +472,16 @@ library Descriptor { tickSpacing: params.tickSpacing, overRange: overRange(params.tickLower, params.tickUpper, params.tickCurrent), tokenId: params.tokenId, - color0: currencyToColorHex(params.quoteCurrency.toId(), 136), - color1: currencyToColorHex(params.baseCurrency.toId(), 136), - color2: currencyToColorHex(params.quoteCurrency.toId(), 0), - color3: currencyToColorHex(params.baseCurrency.toId(), 0), - x1: scale(getCircleCoord(params.quoteCurrency.toId(), 16, params.tokenId), 0, 255, 16, 274), - y1: scale(getCircleCoord(params.baseCurrency.toId(), 16, params.tokenId), 0, 255, 100, 484), - x2: scale(getCircleCoord(params.quoteCurrency.toId(), 32, params.tokenId), 0, 255, 16, 274), - y2: scale(getCircleCoord(params.baseCurrency.toId(), 32, params.tokenId), 0, 255, 100, 484), - x3: scale(getCircleCoord(params.quoteCurrency.toId(), 48, params.tokenId), 0, 255, 16, 274), - y3: scale(getCircleCoord(params.baseCurrency.toId(), 48, params.tokenId), 0, 255, 100, 484) + color0: currencyToColorHex(uint256(uint160(params.quoteCurrency)), 136), + color1: currencyToColorHex(uint256(uint160(params.baseCurrency)), 136), + color2: currencyToColorHex(uint256(uint160(params.quoteCurrency)), 0), + color3: currencyToColorHex(uint256(uint160(params.baseCurrency)), 0), + x1: scale(getCircleCoord(uint256(uint160(params.quoteCurrency)), 16, params.tokenId), 0, 255, 16, 274), + y1: scale(getCircleCoord(uint256(uint160(params.baseCurrency)), 16, params.tokenId), 0, 255, 100, 484), + x2: scale(getCircleCoord(uint256(uint160(params.quoteCurrency)), 32, params.tokenId), 0, 255, 16, 274), + y2: scale(getCircleCoord(uint256(uint160(params.baseCurrency)), 32, params.tokenId), 0, 255, 100, 484), + x3: scale(getCircleCoord(uint256(uint160(params.quoteCurrency)), 48, params.tokenId), 0, 255, 16, 274), + y3: scale(getCircleCoord(uint256(uint160(params.baseCurrency)), 48, params.tokenId), 0, 255, 100, 484) }); return SVG.generateSVG(svgParams); @@ -503,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/ERC721PermitHash.sol b/src/libraries/ERC721PermitHash.sol index a8c9cfc87..3ead157a2 100644 --- a/src/libraries/ERC721PermitHash.sol +++ b/src/libraries/ERC721PermitHash.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; library ERC721PermitHash { diff --git a/src/libraries/HexStrings.sol b/src/libraries/HexStrings.sol index d5a323875..714a9c8ef 100644 --- a/src/libraries/HexStrings.sol +++ b/src/libraries/HexStrings.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; /// @title HexStrings diff --git a/src/libraries/LiquidityAmounts.sol b/src/libraries/LiquidityAmounts.sol new file mode 100644 index 000000000..d6d2dd96d --- /dev/null +++ b/src/libraries/LiquidityAmounts.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {FullMath} from "@uniswap/v4-core/src/libraries/FullMath.sol"; +import {FixedPoint96} from "@uniswap/v4-core/src/libraries/FixedPoint96.sol"; +import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; + +/// @notice Provides functions for computing liquidity amounts from token amounts and prices +library LiquidityAmounts { + using SafeCast for uint256; + + /// @notice Computes the amount of liquidity received for a given amount of token0 and price range + /// @dev Calculates amount0 * (sqrt(upper) * sqrt(lower)) / (sqrt(upper) - sqrt(lower)) + /// @param sqrtPriceAX96 A sqrt price representing the first tick boundary + /// @param sqrtPriceBX96 A sqrt price representing the second tick boundary + /// @param amount0 The amount0 being sent in + /// @return liquidity The amount of returned liquidity + function getLiquidityForAmount0(uint160 sqrtPriceAX96, uint160 sqrtPriceBX96, uint256 amount0) + internal + pure + returns (uint128 liquidity) + { + unchecked { + if (sqrtPriceAX96 > sqrtPriceBX96) (sqrtPriceAX96, sqrtPriceBX96) = (sqrtPriceBX96, sqrtPriceAX96); + uint256 intermediate = FullMath.mulDiv(sqrtPriceAX96, sqrtPriceBX96, FixedPoint96.Q96); + return FullMath.mulDiv(amount0, intermediate, sqrtPriceBX96 - sqrtPriceAX96).toUint128(); + } + } + + /// @notice Computes the amount of liquidity received for a given amount of token1 and price range + /// @dev Calculates amount1 / (sqrt(upper) - sqrt(lower)). + /// @param sqrtPriceAX96 A sqrt price representing the first tick boundary + /// @param sqrtPriceBX96 A sqrt price representing the second tick boundary + /// @param amount1 The amount1 being sent in + /// @return liquidity The amount of returned liquidity + function getLiquidityForAmount1(uint160 sqrtPriceAX96, uint160 sqrtPriceBX96, uint256 amount1) + internal + pure + returns (uint128 liquidity) + { + unchecked { + if (sqrtPriceAX96 > sqrtPriceBX96) (sqrtPriceAX96, sqrtPriceBX96) = (sqrtPriceBX96, sqrtPriceAX96); + return FullMath.mulDiv(amount1, FixedPoint96.Q96, sqrtPriceBX96 - sqrtPriceAX96).toUint128(); + } + } + + /// @notice Computes the maximum amount of liquidity received for a given amount of token0, token1, the current + /// pool prices and the prices at the tick boundaries + /// @param sqrtPriceX96 A sqrt price representing the current pool prices + /// @param sqrtPriceAX96 A sqrt price representing the first tick boundary + /// @param sqrtPriceBX96 A sqrt price representing the second tick boundary + /// @param amount0 The amount of token0 being sent in + /// @param amount1 The amount of token1 being sent in + /// @return liquidity The maximum amount of liquidity received + function getLiquidityForAmounts( + uint160 sqrtPriceX96, + uint160 sqrtPriceAX96, + uint160 sqrtPriceBX96, + uint256 amount0, + uint256 amount1 + ) internal pure returns (uint128 liquidity) { + if (sqrtPriceAX96 > sqrtPriceBX96) (sqrtPriceAX96, sqrtPriceBX96) = (sqrtPriceBX96, sqrtPriceAX96); + + if (sqrtPriceX96 <= sqrtPriceAX96) { + liquidity = getLiquidityForAmount0(sqrtPriceAX96, sqrtPriceBX96, amount0); + } else if (sqrtPriceX96 < sqrtPriceBX96) { + uint128 liquidity0 = getLiquidityForAmount0(sqrtPriceX96, sqrtPriceBX96, amount0); + uint128 liquidity1 = getLiquidityForAmount1(sqrtPriceAX96, sqrtPriceX96, amount1); + + liquidity = liquidity0 < liquidity1 ? liquidity0 : liquidity1; + } else { + liquidity = getLiquidityForAmount1(sqrtPriceAX96, sqrtPriceBX96, amount1); + } + } +} diff --git a/src/libraries/Locker.sol b/src/libraries/Locker.sol index 713779f16..246b10a96 100644 --- a/src/libraries/Locker.sol +++ b/src/libraries/Locker.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; /// @notice This is a temporary library that allows us to use transient storage (tstore/tload) diff --git a/src/libraries/PathKey.sol b/src/libraries/PathKey.sol index b3fa1e7f8..daa2fdd07 100644 --- a/src/libraries/PathKey.sol +++ b/src/libraries/PathKey.sol @@ -1,4 +1,4 @@ -//SPDX-License-Identifier: UNLICENSED +//SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; diff --git a/src/libraries/PositionConfig.sol b/src/libraries/PositionConfig.sol index 007e7bb9d..2fd6cd0d6 100644 --- a/src/libraries/PositionConfig.sol +++ b/src/libraries/PositionConfig.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; diff --git a/src/libraries/PositionConfigId.sol b/src/libraries/PositionConfigId.sol index 4e31c760c..e6bdf84d6 100644 --- a/src/libraries/PositionConfigId.sol +++ b/src/libraries/PositionConfigId.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; /// @notice A configId is set per tokenId diff --git a/src/libraries/PositionInfoLibrary.sol b/src/libraries/PositionInfoLibrary.sol index 981500559..2baba26a1 100644 --- a/src/libraries/PositionInfoLibrary.sol +++ b/src/libraries/PositionInfoLibrary.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; diff --git a/src/libraries/QuoterRevert.sol b/src/libraries/QuoterRevert.sol index bb0eda905..d53ec844d 100644 --- a/src/libraries/QuoterRevert.sol +++ b/src/libraries/QuoterRevert.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {ParseBytes} from "@uniswap/v4-core/src/libraries/ParseBytes.sol"; diff --git a/src/libraries/SVG.sol b/src/libraries/SVG.sol index dea00e663..03c4236cb 100644 --- a/src/libraries/SVG.sol +++ b/src/libraries/SVG.sol @@ -1,7 +1,6 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; import {BitMath} from "@uniswap/v4-core/src/libraries/BitMath.sol"; import {Strings} from "openzeppelin-contracts/contracts/utils/Strings.sol"; @@ -346,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/SafeCurrencyMetadata.sol b/src/libraries/SafeCurrencyMetadata.sol index f4d7cf276..e1a4c059c 100644 --- a/src/libraries/SafeCurrencyMetadata.sol +++ b/src/libraries/SafeCurrencyMetadata.sol @@ -1,47 +1,50 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {IERC20Metadata} from "openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol"; -import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; import {AddressStringUtil} from "./AddressStringUtil.sol"; /// @title SafeCurrencyMetadata /// @notice can produce symbols and decimals from inconsistent or absent ERC20 implementations /// @dev Reference: https://github.com/Uniswap/solidity-lib/blob/master/contracts/libraries/SafeERC20Namer.sol library SafeCurrencyMetadata { - using CurrencyLibrary for Currency; + uint8 constant MAX_SYMBOL_LENGTH = 12; - /// @notice attempts to extract the token symbol. if it does not implement symbol, returns a symbol derived from the address - /// @param currency The currency + /// @notice attempts to extract the currency symbol. if it does not implement symbol, returns a symbol derived from the address + /// @param currency The currency address /// @param nativeLabel The native label - /// @return the token symbol - function currencySymbol(Currency currency, string memory nativeLabel) internal view returns (string memory) { - if (currency.isAddressZero()) { + /// @return the currency symbol + function currencySymbol(address currency, string memory nativeLabel) internal view returns (string memory) { + if (currency == address(0)) { return nativeLabel; } - address currencyAddress = Currency.unwrap(currency); - string memory symbol = callAndParseStringReturn(currencyAddress, IERC20Metadata.symbol.selector); + string memory symbol = callAndParseStringReturn(currency, IERC20Metadata.symbol.selector); if (bytes(symbol).length == 0) { // fallback to 6 uppercase hex of address - return addressToSymbol(currencyAddress); + return addressToSymbol(currency); + } + if (bytes(symbol).length > MAX_SYMBOL_LENGTH) { + return truncateSymbol(symbol); } return symbol; } /// @notice attempts to extract the token decimals, returns 0 if not implemented or not a uint8 - /// @param currency The currency - /// @return the token decimals - function currencyDecimals(Currency currency) internal view returns (uint8) { - if (currency.isAddressZero()) { + /// @param currency The currency address + /// @return the currency decimals + function currencyDecimals(address currency) internal view returns (uint8) { + if (currency == address(0)) { return 18; } - (bool success, bytes memory data) = - Currency.unwrap(currency).staticcall(abi.encodeCall(IERC20Metadata.decimals, ())); + (bool success, bytes memory data) = currency.staticcall(abi.encodeCall(IERC20Metadata.decimals, ())); if (!success) { 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; } @@ -89,4 +92,17 @@ library SafeCurrencyMetadata { } return ""; } + + /// @notice truncates the symbol to the MAX_SYMBOL_LENGTH + /// @dev assumes the string is already longer than MAX_SYMBOL_LENGTH (or the same) + /// @param str the symbol + /// @return the truncated symbol + function truncateSymbol(string memory str) internal pure returns (string memory) { + bytes memory strBytes = bytes(str); + bytes memory truncatedBytes = new bytes(MAX_SYMBOL_LENGTH); + for (uint256 i = 0; i < MAX_SYMBOL_LENGTH; i++) { + truncatedBytes[i] = strBytes[i]; + } + return string(truncatedBytes); + } } diff --git a/src/libraries/SlippageCheck.sol b/src/libraries/SlippageCheck.sol index 48700c153..e4c7e960e 100644 --- a/src/libraries/SlippageCheck.sol +++ b/src/libraries/SlippageCheck.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; diff --git a/src/libraries/VanityAddressLib.sol b/src/libraries/VanityAddressLib.sol new file mode 100644 index 000000000..0139aa54d --- /dev/null +++ b/src/libraries/VanityAddressLib.sol @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/// @title VanityAddressLib +/// @notice A library to score addresses based on their vanity +library VanityAddressLib { + /// @notice Compares two addresses and returns true if the first address has a better vanity score + /// @param first The first address to compare + /// @param second The second address to compare + /// @return better True if the first address has a better vanity score + function betterThan(address first, address second) internal pure returns (bool better) { + return score(first) > score(second); + } + + /// @notice Scores an address based on its vanity + /// @dev Scoring rules: + /// Requirement: The first nonzero nibble must be 4 + /// 10 points for every leading 0 nibble + /// 40 points if the first 4 is followed by 3 more 4s + /// 20 points if the first nibble after the 4 4s is NOT a 4 + /// 20 points if the last 4 nibbles are 4s + /// 1 point for every 4 + /// @param addr The address to score + /// @return calculatedScore The vanity score of the address + function score(address addr) internal pure returns (uint256 calculatedScore) { + // convert the address to bytes for easier parsing + bytes20 addrBytes = bytes20(addr); + + unchecked { + // 10 points per leading zero nibble + uint256 leadingZeroCount = getLeadingNibbleCount(addrBytes, 0, 0); + calculatedScore += (leadingZeroCount * 10); + + // special handling for 4s immediately after leading 0s + uint256 leadingFourCount = getLeadingNibbleCount(addrBytes, leadingZeroCount, 4); + // If the first nonzero nibble is not 4, return 0 + if (leadingFourCount == 0) { + return 0; + } else if (leadingFourCount == 4) { + // 60 points if exactly 4 4s + calculatedScore += 60; + } else if (leadingFourCount > 4) { + // 40 points if more than 4 4s + calculatedScore += 40; + } + + // handling for remaining nibbles + for (uint256 i = 0; i < addrBytes.length * 2; i++) { + uint8 currentNibble = getNibble(addrBytes, i); + + // 1 extra point for any 4 nibbles + if (currentNibble == 4) { + calculatedScore += 1; + } + } + + // If the last 4 nibbles are 4s, add 20 points + if (addrBytes[18] == 0x44 && addrBytes[19] == 0x44) { + calculatedScore += 20; + } + } + } + + /// @notice Returns the number of leading nibbles in an address that match a given value + /// @param addrBytes The address to count the leading zero nibbles in + function getLeadingNibbleCount(bytes20 addrBytes, uint256 startIndex, uint8 comparison) + internal + pure + returns (uint256 count) + { + if (startIndex >= addrBytes.length * 2) { + return count; + } + + for (uint256 i = startIndex; i < addrBytes.length * 2; i++) { + uint8 currentNibble = getNibble(addrBytes, i); + if (currentNibble != comparison) { + return count; + } + count += 1; + } + } + + /// @notice Returns the nibble at a given index in an address + /// @param input The address to get the nibble from + /// @param nibbleIndex The index of the nibble to get + function getNibble(bytes20 input, uint256 nibbleIndex) internal pure returns (uint8 currentNibble) { + uint8 currByte = uint8(input[nibbleIndex / 2]); + if (nibbleIndex % 2 == 0) { + // Get the higher nibble of the byte + currentNibble = currByte >> 4; + } else { + // Get the lower nibble of the byte + currentNibble = currByte & 0x0F; + } + } +} diff --git a/test/BaseActionsRouter.t.sol b/test/BaseActionsRouter.t.sol index 57ca984fa..0a6bbc76f 100644 --- a/test/BaseActionsRouter.t.sol +++ b/test/BaseActionsRouter.t.sol @@ -1,4 +1,4 @@ -//SPDX-License-Identifier: UNLICENSED +//SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import {MockBaseActionsRouter} from "./mocks/MockBaseActionsRouter.sol"; diff --git a/test/DeltaResolver.t.sol b/test/DeltaResolver.t.sol index 61c032038..212939b6f 100644 --- a/test/DeltaResolver.t.sol +++ b/test/DeltaResolver.t.sol @@ -1,4 +1,4 @@ -//SPDX-License-Identifier: UNLICENSED +//SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import {Test} from "forge-std/Test.sol"; diff --git a/test/Multicall.t.sol b/test/Multicall.t.sol index 8591c2712..83503c005 100644 --- a/test/Multicall.t.sol +++ b/test/Multicall.t.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "forge-std/Test.sol"; diff --git a/test/PositionDescriptor.t.sol b/test/PositionDescriptor.t.sol index dabe83930..4b326814c 100644 --- a/test/PositionDescriptor.t.sol +++ b/test/PositionDescriptor.t.sol @@ -12,9 +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 {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import {SafeCurrencyMetadata} from "../src/libraries/SafeCurrencyMetadata.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; @@ -119,10 +124,185 @@ 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 + assertFalse(positionDescriptor.flipRatio(Currency.unwrap(key.currency0), Currency.unwrap(key.currency1))); + + string memory symbol0 = SafeCurrencyMetadata.currencySymbol(Currency.unwrap(currency0), nativeCurrencyLabel); + string memory symbol1 = SafeCurrencyMetadata.currencySymbol(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, + SafeCurrencyMetadata.currencyDecimals(Currency.unwrap(currency0)), + SafeCurrencyMetadata.currencyDecimals(Currency.unwrap(currency1)), + false + ); + string memory tickToDecimal1 = Descriptor.tickToDecimalString( + tickUpper, + key.tickSpacing, + SafeCurrencyMetadata.currencyDecimals(Currency.unwrap(currency0)), + SafeCurrencyMetadata.currencyDecimals(Currency.unwrap(currency1)), + false + ); + + assertEq( + token.name, + string( + abi.encodePacked( + "Uniswap - ", fee, " - ", symbol1, "/", symbol0, " - ", tickToDecimal0, "<>", tickToDecimal1 + ) + ) + ); + assertEq( + token.description, + 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 currency addresses match the expected currencies, as currency symbols may be imitated." + ) + ) + ); + } + + function test_native_tokenURI_succeeds() public { + (nativeKey,) = initPool(CurrencyLibrary.ADDRESS_ZERO, currency1, IHooks(address(0)), 3000, SQRT_PRICE_1_1); + int24 tickLower = int24(nativeKey.tickSpacing); + int24 tickUpper = int24(nativeKey.tickSpacing * 2); + uint256 amount0Desired = 100e18; + uint256 amount1Desired = 100e18; + uint256 liquidityToAdd = LiquidityAmounts.getLiquidityForAmounts( + SQRT_PRICE_1_1, + TickMath.getSqrtPriceAtTick(tickLower), + TickMath.getSqrtPriceAtTick(tickUpper), + amount0Desired, + amount1Desired + ); + + PositionConfig memory config = PositionConfig({poolKey: nativeKey, tickLower: tickLower, tickUpper: tickUpper}); + uint256 tokenId = lpm.nextTokenId(); + mintWithNative(SQRT_PRICE_1_1, config, liquidityToAdd, ActionConstants.MSG_SENDER, ZERO_BYTES); + + // The prefix length is calculated by converting the string to bytes and finding its length + uint256 prefixLength = bytes("data:application/json;base64,").length; + + string memory uri = positionDescriptor.tokenURI(lpm, tokenId); + // Convert the uri to bytes + bytes memory uriBytes = bytes(uri); + + // Slice the uri to get only the base64-encoded part + bytes memory base64Part = new bytes(uriBytes.length - prefixLength); + + for (uint256 i = 0; i < base64Part.length; i++) { + base64Part[i] = uriBytes[i + prefixLength]; + } + + // Decode the base64-encoded part + bytes memory decoded = Base64.decode(string(base64Part)); + string memory json = string(decoded); + + // decode json + bytes memory data = vm.parseJson(json); + Token memory token = abi.decode(data, (Token)); + + // quote is currency1, base is currency0 + assertFalse( + positionDescriptor.flipRatio(Currency.unwrap(nativeKey.currency0), Currency.unwrap(nativeKey.currency1)) + ); + + string memory symbol0 = + SafeCurrencyMetadata.currencySymbol(Currency.unwrap(nativeKey.currency0), nativeCurrencyLabel); + string memory symbol1 = + SafeCurrencyMetadata.currencySymbol(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, + SafeCurrencyMetadata.currencyDecimals(Currency.unwrap(currency0)), + SafeCurrencyMetadata.currencyDecimals(Currency.unwrap(currency1)), + false + ); + string memory tickToDecimal1 = Descriptor.tickToDecimalString( + tickUpper, + nativeKey.tickSpacing, + SafeCurrencyMetadata.currencyDecimals(Currency.unwrap(currency0)), + SafeCurrencyMetadata.currencyDecimals(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 currency addresses match the expected currencies, as currency 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 currency addresses match the expected currencies, as currency symbols may be imitated." + ) + ) ); } @@ -147,4 +327,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/SafeCallback.t.sol b/test/SafeCallback.t.sol index fd5157ab3..1e7673227 100644 --- a/test/SafeCallback.t.sol +++ b/test/SafeCallback.t.sol @@ -1,4 +1,4 @@ -//SPDX-License-Identifier: UNLICENSED +//SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import "forge-std/Test.sol"; diff --git a/test/UniswapV4DeployerCompetition.t.sol b/test/UniswapV4DeployerCompetition.t.sol new file mode 100644 index 000000000..265d3b6fe --- /dev/null +++ b/test/UniswapV4DeployerCompetition.t.sol @@ -0,0 +1,178 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +import {Owned} from "solmate/src/auth/Owned.sol"; +import {Test} from "forge-std/Test.sol"; +import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; +import {UniswapV4DeployerCompetition} from "../src/UniswapV4DeployerCompetition.sol"; +import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; +import {VanityAddressLib} from "../src/libraries/VanityAddressLib.sol"; +import {Create2} from "@openzeppelin/contracts/utils/Create2.sol"; +import {IUniswapV4DeployerCompetition} from "../src/interfaces/IUniswapV4DeployerCompetition.sol"; + +contract UniswapV4DeployerCompetitionTest is Test { + using VanityAddressLib for address; + + UniswapV4DeployerCompetition competition; + bytes32 initCodeHash; + address deployer; + address v4Owner; + address winner; + address defaultAddress; + uint256 competitionDeadline; + uint256 exclusiveDeployLength = 1 days; + + bytes32 mask20bytes = bytes32(uint256(type(uint96).max)); + + function setUp() public { + competitionDeadline = block.timestamp + 7 days; + v4Owner = makeAddr("V4Owner"); + winner = makeAddr("Winner"); + deployer = makeAddr("Deployer"); + vm.prank(deployer); + initCodeHash = keccak256(abi.encodePacked(type(PoolManager).creationCode, uint256(uint160(v4Owner)))); + competition = + new UniswapV4DeployerCompetition(initCodeHash, competitionDeadline, deployer, exclusiveDeployLength); + defaultAddress = Create2.computeAddress(bytes32(0), initCodeHash, address(competition)); + } + + function test_defaultSalt_deploy_succeeds() public { + assertEq(competition.bestAddressSubmitter(), address(0)); + assertEq(competition.bestAddressSalt(), bytes32(0)); + assertEq(competition.bestAddress(), defaultAddress); + + assertEq(defaultAddress.code.length, 0); + vm.warp(competition.competitionDeadline() + 1); + vm.prank(deployer); + competition.deploy(abi.encodePacked(type(PoolManager).creationCode, uint256(uint160(v4Owner)))); + assertFalse(defaultAddress.code.length == 0); + assertEq(Owned(defaultAddress).owner(), v4Owner); + } + + function test_updateBestAddress_succeeds(bytes32 salt) public { + salt = (salt & mask20bytes) | bytes32(bytes20(winner)); + + assertEq(competition.bestAddressSubmitter(), address(0)); + assertEq(competition.bestAddressSalt(), bytes32(0)); + assertEq(competition.bestAddress(), defaultAddress); + + address newAddress = Create2.computeAddress(salt, initCodeHash, address(competition)); + vm.assume(newAddress.betterThan(defaultAddress)); + + vm.prank(winner); + vm.expectEmit(true, true, true, false, address(competition)); + emit IUniswapV4DeployerCompetition.NewAddressFound(newAddress, winner, VanityAddressLib.score(newAddress)); + competition.updateBestAddress(salt); + assertFalse(competition.bestAddress() == address(0), "best address not set"); + assertEq(competition.bestAddress(), newAddress, "wrong address set"); + assertEq(competition.bestAddressSubmitter(), winner, "wrong submitter set"); + assertEq(competition.bestAddressSalt(), salt, "incorrect salt set"); + address v4Core = competition.bestAddress(); + + assertEq(v4Core.code.length, 0); + vm.warp(competition.competitionDeadline() + 1); + vm.prank(deployer); + competition.deploy(abi.encodePacked(type(PoolManager).creationCode, uint256(uint160(v4Owner)))); + assertFalse(v4Core.code.length == 0); + assertEq(Owned(v4Core).owner(), v4Owner); + assertEq(address(competition).balance, 0 ether); + } + + function test_updateBestAddress_reverts_CompetitionOver(bytes32 salt) public { + vm.warp(competition.competitionDeadline() + 1); + vm.expectRevert( + abi.encodeWithSelector( + IUniswapV4DeployerCompetition.CompetitionOver.selector, + block.timestamp, + competition.competitionDeadline() + ) + ); + competition.updateBestAddress(salt); + } + + function test_updateBestAddress_reverts_InvalidSigner(bytes32 salt) public { + vm.assume(bytes20(salt) != bytes20(0)); + vm.assume(bytes20(salt) != bytes20(winner)); + + vm.expectRevert(abi.encodeWithSelector(IUniswapV4DeployerCompetition.InvalidSender.selector, salt, winner)); + vm.prank(winner); + competition.updateBestAddress(salt); + } + + function test_updateBestAddress_reverts_WorseAddress(bytes32 salt) public { + vm.assume(salt != bytes32(0)); + salt = (salt & mask20bytes) | bytes32(bytes20(winner)); + + address newAddr = Create2.computeAddress(salt, initCodeHash, address(competition)); + if (!newAddr.betterThan(defaultAddress)) { + vm.expectRevert( + abi.encodeWithSelector( + IUniswapV4DeployerCompetition.WorseAddress.selector, + newAddr, + competition.bestAddress(), + newAddr.score(), + competition.bestAddress().score() + ) + ); + vm.prank(winner); + competition.updateBestAddress(salt); + } else { + vm.prank(winner); + competition.updateBestAddress(salt); + assertEq(competition.bestAddressSubmitter(), winner); + assertEq(competition.bestAddressSalt(), salt); + assertEq(competition.bestAddress(), newAddr); + } + } + + function test_deploy_succeeds(bytes32 salt) public { + salt = (salt & mask20bytes) | bytes32(bytes20(winner)); + + address newAddress = Create2.computeAddress(salt, initCodeHash, address(competition)); + vm.assume(newAddress.betterThan(defaultAddress)); + + vm.prank(winner); + competition.updateBestAddress(salt); + address v4Core = competition.bestAddress(); + + vm.warp(competition.competitionDeadline() + 1); + vm.prank(deployer); + competition.deploy(abi.encodePacked(type(PoolManager).creationCode, uint256(uint160(v4Owner)))); + assertFalse(v4Core.code.length == 0); + assertEq(Owned(v4Core).owner(), v4Owner); + assertEq(TickMath.MAX_TICK_SPACING, type(int16).max); + } + + function test_deploy_reverts_CompetitionNotOver(uint256 timestamp) public { + vm.assume(timestamp < competition.competitionDeadline()); + vm.warp(timestamp); + vm.expectRevert( + abi.encodeWithSelector( + IUniswapV4DeployerCompetition.CompetitionNotOver.selector, timestamp, competition.competitionDeadline() + ) + ); + competition.deploy(abi.encodePacked(type(PoolManager).creationCode, uint256(uint160(v4Owner)))); + } + + function test_deploy_reverts_InvalidBytecode() public { + vm.expectRevert(IUniswapV4DeployerCompetition.InvalidBytecode.selector); + vm.prank(deployer); + // set the owner as the winner not the correct owner + competition.deploy(abi.encodePacked(type(PoolManager).creationCode, uint256(uint160(winner)))); + } + + function test_deploy_reverts_NotAllowedToDeploy() public { + vm.warp(competition.competitionDeadline() + 1); + vm.prank(address(1)); + vm.expectRevert( + abi.encodeWithSelector(IUniswapV4DeployerCompetition.NotAllowedToDeploy.selector, address(1), deployer) + ); + competition.deploy(abi.encodePacked(type(PoolManager).creationCode, uint256(uint160(v4Owner)))); + } + + function test_deploy_succeeds_afterExcusiveDeployDeadline() public { + vm.warp(competition.exclusiveDeployDeadline() + 1); + vm.prank(address(1)); + competition.deploy(abi.encodePacked(type(PoolManager).creationCode, uint256(uint160(v4Owner)))); + } +} diff --git a/test/UnorderedNonce.t.sol b/test/UnorderedNonce.t.sol index d3939fd24..f7b46b2da 100644 --- a/test/UnorderedNonce.t.sol +++ b/test/UnorderedNonce.t.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "forge-std/Test.sol"; diff --git a/test/Quoter.t.sol b/test/V4Quoter.t.sol similarity index 88% rename from test/Quoter.t.sol rename to test/V4Quoter.t.sol index 2dc1c8540..fc91ce47a 100644 --- a/test/Quoter.t.sol +++ b/test/V4Quoter.t.sol @@ -1,11 +1,11 @@ -//SPDX-License-Identifier: UNLICENSED +//SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {Test} from "forge-std/Test.sol"; import {PathKey} from "../src/libraries/PathKey.sol"; -import {IQuoter} from "../src/interfaces/IQuoter.sol"; -import {Quoter} from "../src/lens/Quoter.sol"; +import {IV4Quoter} from "../src/interfaces/IV4Quoter.sol"; +import {V4Quoter} from "../src/lens/V4Quoter.sol"; import {BaseV4Quoter} from "../src/base/BaseV4Quoter.sol"; import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; @@ -40,7 +40,7 @@ contract QuoterTest is Test, Deployers, GasSnapshot { uint256 internal constant CONTROLLER_GAS_LIMIT = 500000; - Quoter quoter; + V4Quoter quoter; PoolModifyLiquidityTest positionManager; @@ -56,7 +56,7 @@ contract QuoterTest is Test, Deployers, GasSnapshot { function setUp() public { deployFreshManagerAndRouters(); - quoter = new Quoter(IPoolManager(manager)); + quoter = new V4Quoter(IPoolManager(manager)); positionManager = new PoolModifyLiquidityTest(manager); // salts are chosen so that address(token0) < address(token1) && address(token1) < address(token2) @@ -86,7 +86,7 @@ contract QuoterTest is Test, Deployers, GasSnapshot { uint256 expectedAmountOut = 9871; (uint256 amountOut, uint256 gasEstimate) = quoter.quoteExactInputSingle( - IQuoter.QuoteExactSingleParams({ + IV4Quoter.QuoteExactSingleParams({ poolKey: key02, zeroForOne: true, exactAmount: uint128(amountIn), @@ -105,7 +105,7 @@ contract QuoterTest is Test, Deployers, GasSnapshot { uint256 expectedAmountOut = 9871; (uint256 amountOut, uint256 gasEstimate) = quoter.quoteExactInputSingle( - IQuoter.QuoteExactSingleParams({ + IV4Quoter.QuoteExactSingleParams({ poolKey: key02, zeroForOne: false, exactAmount: uint128(amountIn), @@ -122,7 +122,7 @@ contract QuoterTest is Test, Deployers, GasSnapshot { function testQuoter_quoteExactInput_0to2_2TicksLoaded() public { tokenPath.push(token0); tokenPath.push(token2); - IQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 10000); + IV4Quoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 10000); (uint256 amountOut, uint256 gasEstimate) = quoter.quoteExactInput(params); @@ -137,7 +137,7 @@ contract QuoterTest is Test, Deployers, GasSnapshot { // The swap amount is set such that the active tick after the swap is -120. // -120 is an initialized tick for this pool. We check that we don't count it. - IQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 6200); + IV4Quoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 6200); (uint256 amountOut, uint256 gasEstimate) = quoter.quoteExactInput(params); @@ -152,7 +152,7 @@ contract QuoterTest is Test, Deployers, GasSnapshot { // The swap amount is set such that the active tick after the swap is -60. // -60 is an initialized tick for this pool. We check that we don't count it. - IQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 4000); + IV4Quoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 4000); (uint256 amountOut, uint256 gasEstimate) = quoter.quoteExactInput(params); @@ -166,7 +166,7 @@ contract QuoterTest is Test, Deployers, GasSnapshot { function testQuoter_quoteExactInput_0to2_0TickLoaded_startingNotInitialized() public { tokenPath.push(token0); tokenPath.push(token2); - IQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 10); + IV4Quoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 10); (uint256 amountOut, uint256 gasEstimate) = quoter.quoteExactInput(params); @@ -179,7 +179,7 @@ contract QuoterTest is Test, Deployers, GasSnapshot { setupPoolWithZeroTickInitialized(key02); tokenPath.push(token0); tokenPath.push(token2); - IQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 10); + IV4Quoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 10); (uint256 amountOut, uint256 gasEstimate) = quoter.quoteExactInput(params); @@ -191,7 +191,7 @@ contract QuoterTest is Test, Deployers, GasSnapshot { function testQuoter_quoteExactInput_2to0_2TicksLoaded() public { tokenPath.push(token2); tokenPath.push(token0); - IQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 10000); + IV4Quoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 10000); (uint256 amountOut, uint256 gasEstimate) = quoter.quoteExactInput(params); @@ -206,7 +206,7 @@ contract QuoterTest is Test, Deployers, GasSnapshot { // The swap amount is set such that the active tick after the swap is 120. // 120 is an initialized tick for this pool. We check that we don't count it. - IQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 6250); + IV4Quoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 6250); (uint256 amountOut, uint256 gasEstimate) = quoter.quoteExactInput(params); @@ -221,7 +221,7 @@ contract QuoterTest is Test, Deployers, GasSnapshot { setupPoolWithZeroTickInitialized(key02); tokenPath.push(token2); tokenPath.push(token0); - IQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 200); + IV4Quoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 200); // Tick 0 initialized. Tick after = 1 (uint256 amountOut, uint256 gasEstimate) = quoter.quoteExactInput(params); @@ -237,7 +237,7 @@ contract QuoterTest is Test, Deployers, GasSnapshot { function testQuoter_quoteExactInput_2to0_0TickLoaded_startingNotInitialized() public { tokenPath.push(token2); tokenPath.push(token0); - IQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 103); + IV4Quoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 103); (uint256 amountOut, uint256 gasEstimate) = quoter.quoteExactInput(params); @@ -249,7 +249,7 @@ contract QuoterTest is Test, Deployers, GasSnapshot { function testQuoter_quoteExactInput_2to1() public { tokenPath.push(token2); tokenPath.push(token1); - IQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 10000); + IV4Quoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 10000); (uint256 amountOut, uint256 gasEstimate) = quoter.quoteExactInput(params); assertGt(gasEstimate, 50000); @@ -261,7 +261,7 @@ contract QuoterTest is Test, Deployers, GasSnapshot { tokenPath.push(token0); tokenPath.push(token2); tokenPath.push(token1); - IQuoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 10000); + IV4Quoter.QuoteExactParams memory params = getExactInputParams(tokenPath, 10000); (uint256 amountOut, uint256 gasEstimate) = quoter.quoteExactInput(params); @@ -275,7 +275,7 @@ contract QuoterTest is Test, Deployers, GasSnapshot { function testQuoter_quoteExactOutputSingle_0to1() public { uint256 amountOut = 10000; (uint256 amountIn, uint256 gasEstimate) = quoter.quoteExactOutputSingle( - IQuoter.QuoteExactSingleParams({ + IV4Quoter.QuoteExactSingleParams({ poolKey: key01, zeroForOne: true, exactAmount: uint128(amountOut), @@ -292,7 +292,7 @@ contract QuoterTest is Test, Deployers, GasSnapshot { function testQuoter_quoteExactOutputSingle_1to0() public { uint256 amountOut = 10000; (uint256 amountIn, uint256 gasEstimate) = quoter.quoteExactOutputSingle( - IQuoter.QuoteExactSingleParams({ + IV4Quoter.QuoteExactSingleParams({ poolKey: key01, zeroForOne: false, exactAmount: uint128(amountOut), @@ -309,7 +309,7 @@ contract QuoterTest is Test, Deployers, GasSnapshot { function testQuoter_quoteExactOutput_0to2_2TicksLoaded() public { tokenPath.push(token0); tokenPath.push(token2); - IQuoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 15000); + IV4Quoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 15000); (uint256 amountIn, uint256 gasEstimate) = quoter.quoteExactOutput(params); @@ -323,7 +323,7 @@ contract QuoterTest is Test, Deployers, GasSnapshot { tokenPath.push(token0); tokenPath.push(token2); - IQuoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 6143); + IV4Quoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 6143); (uint256 amountIn, uint256 gasEstimate) = quoter.quoteExactOutput(params); @@ -337,7 +337,7 @@ contract QuoterTest is Test, Deployers, GasSnapshot { tokenPath.push(token0); tokenPath.push(token2); - IQuoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 4000); + IV4Quoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 4000); (uint256 amountIn, uint256 gasEstimate) = quoter.quoteExactOutput(params); @@ -352,7 +352,7 @@ contract QuoterTest is Test, Deployers, GasSnapshot { tokenPath.push(token0); tokenPath.push(token2); - IQuoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 100); + IV4Quoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 100); // Tick 0 initialized. Tick after = 1 (uint256 amountIn, uint256 gasEstimate) = quoter.quoteExactOutput(params); @@ -367,7 +367,7 @@ contract QuoterTest is Test, Deployers, GasSnapshot { tokenPath.push(token0); tokenPath.push(token2); - IQuoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 10); + IV4Quoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 10); (uint256 amountIn, uint256 gasEstimate) = quoter.quoteExactOutput(params); @@ -379,7 +379,7 @@ contract QuoterTest is Test, Deployers, GasSnapshot { function testQuoter_quoteExactOutput_2to0_2TicksLoaded() public { tokenPath.push(token2); tokenPath.push(token0); - IQuoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 15000); + IV4Quoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 15000); (uint256 amountIn, uint256 gasEstimate) = quoter.quoteExactOutput(params); @@ -392,7 +392,7 @@ contract QuoterTest is Test, Deployers, GasSnapshot { tokenPath.push(token2); tokenPath.push(token0); - IQuoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 6223); + IV4Quoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 6223); (uint256 amountIn, uint256 gasEstimate) = quoter.quoteExactOutput(params); @@ -405,7 +405,7 @@ contract QuoterTest is Test, Deployers, GasSnapshot { tokenPath.push(token2); tokenPath.push(token0); - IQuoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 6000); + IV4Quoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 6000); (uint256 amountIn, uint256 gasEstimate) = quoter.quoteExactOutput(params); assertGt(gasEstimate, 50000); @@ -417,7 +417,7 @@ contract QuoterTest is Test, Deployers, GasSnapshot { tokenPath.push(token2); tokenPath.push(token1); - IQuoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 9871); + IV4Quoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 9871); (uint256 amountIn, uint256 gasEstimate) = quoter.quoteExactOutput(params); @@ -431,7 +431,7 @@ contract QuoterTest is Test, Deployers, GasSnapshot { tokenPath.push(token2); tokenPath.push(token1); - IQuoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 9745); + IV4Quoter.QuoteExactParams memory params = getExactOutputParams(tokenPath, 9745); (uint256 amountIn, uint256 gasEstimate) = quoter.quoteExactOutput(params); @@ -548,7 +548,7 @@ contract QuoterTest is Test, Deployers, GasSnapshot { function getExactInputParams(MockERC20[] memory _tokenPath, uint256 amountIn) internal pure - returns (IQuoter.QuoteExactParams memory params) + returns (IV4Quoter.QuoteExactParams memory params) { PathKey[] memory path = new PathKey[](_tokenPath.length - 1); for (uint256 i = 0; i < _tokenPath.length - 1; i++) { @@ -563,7 +563,7 @@ contract QuoterTest is Test, Deployers, GasSnapshot { function getExactOutputParams(MockERC20[] memory _tokenPath, uint256 amountOut) internal pure - returns (IQuoter.QuoteExactParams memory params) + returns (IV4Quoter.QuoteExactParams memory params) { PathKey[] memory path = new PathKey[](_tokenPath.length - 1); for (uint256 i = _tokenPath.length - 1; i > 0; i--) { diff --git a/test/libraries/BipsLibrary.t.sol b/test/libraries/BipsLibrary.t.sol new file mode 100644 index 000000000..02cc67d71 --- /dev/null +++ b/test/libraries/BipsLibrary.t.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; +import {BipsLibrary} from "../../src/libraries/BipsLibrary.sol"; + +contract BipsLibraryTest is Test { + using BipsLibrary for uint256; + + // The block gas limit set in foundry config is 300_000_000 (300M) for testing purposes + uint256 BLOCK_GAS_LIMIT; + + function setUp() public { + BLOCK_GAS_LIMIT = block.gaslimit; + } + + function test_fuzz_calculatePortion(uint256 amount, uint256 bips) public { + amount = bound(amount, 0, uint256(type(uint128).max)); + if (bips > BipsLibrary.BPS_DENOMINATOR) { + vm.expectRevert(BipsLibrary.InvalidBips.selector); + amount.calculatePortion(bips); + } else { + assertEq(amount.calculatePortion(bips), amount * bips / BipsLibrary.BPS_DENOMINATOR); + } + } + + function test_fuzz_gasLimit(uint256 bips) public { + if (bips > BipsLibrary.BPS_DENOMINATOR) { + vm.expectRevert(BipsLibrary.InvalidBips.selector); + block.gaslimit.calculatePortion(bips); + } else { + assertEq(block.gaslimit.calculatePortion(bips), BLOCK_GAS_LIMIT * bips / BipsLibrary.BPS_DENOMINATOR); + } + } + + function test_gasLimit_100_percent() public view { + assertEq(block.gaslimit, block.gaslimit.calculatePortion(10_000)); + } + + function test_gasLimit_1_percent() public view { + // 100 bps = 1% + // 1% of 30M is 300K + assertEq(BLOCK_GAS_LIMIT / 100, block.gaslimit.calculatePortion(100)); + } + + function test_gasLimit_1BP() public view { + // 1bp is 0.01% + // 0.01% of 30M is 300 + assertEq(BLOCK_GAS_LIMIT / 10000, block.gaslimit.calculatePortion(1)); + } +} diff --git a/test/libraries/CalldataDecoder.t.sol b/test/libraries/CalldataDecoder.t.sol index 0a3fc7375..b2aa2d78b 100644 --- a/test/libraries/CalldataDecoder.t.sol +++ b/test/libraries/CalldataDecoder.t.sol @@ -83,6 +83,28 @@ contract CalldataDecoderTest is Test { assertEq(mintParams.tickUpper, _config.tickUpper); } + function test_fuzz_decodeMintFromDeltasParams( + PositionConfig calldata _config, + uint128 _amount0Max, + uint128 _amount1Max, + address _owner, + bytes calldata _hookData + ) public view { + bytes memory params = abi.encode( + _config.poolKey, _config.tickLower, _config.tickUpper, _amount0Max, _amount1Max, _owner, _hookData + ); + + (MockCalldataDecoder.MintFromDeltasParams memory mintParams) = decoder.decodeMintFromDeltasParams(params); + + _assertEq(mintParams.poolKey, _config.poolKey); + assertEq(mintParams.tickLower, _config.tickLower); + assertEq(mintParams.tickUpper, _config.tickUpper); + assertEq(mintParams.amount0Max, _amount0Max); + assertEq(mintParams.amount1Max, _amount1Max); + assertEq(mintParams.owner, _owner); + assertEq(mintParams.hookData, _hookData); + } + function test_fuzz_decodeSwapExactInParams(IV4Router.ExactInputParams calldata _swapParams) public view { bytes memory params = abi.encode(_swapParams); IV4Router.ExactInputParams memory swapParams = decoder.decodeSwapExactInParams(params); @@ -103,7 +125,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 +149,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); } @@ -141,6 +161,18 @@ contract CalldataDecoderTest is Test { assertEq(_address, __address); } + function test_decodeCurrencyAndAddress_outOutBounds() public { + Currency currency = Currency.wrap(address(0x12341234)); + address addy = address(0x23453456); + + bytes memory params = abi.encode(currency, addy); + bytes memory invalidParams = _removeFinalByte(params); + assertEq(invalidParams.length, params.length - 1); + + vm.expectRevert(CalldataDecoder.SliceOutOfBounds.selector); + decoder.decodeCurrencyAndAddress(invalidParams); + } + function test_fuzz_decodeCurrency(Currency _currency) public view { bytes memory params = abi.encode(_currency); (Currency currency) = decoder.decodeCurrency(params); @@ -148,6 +180,17 @@ contract CalldataDecoderTest is Test { assertEq(Currency.unwrap(currency), Currency.unwrap(_currency)); } + function test_decodeCurrency_outOutBounds() public { + Currency currency = Currency.wrap(address(0x12341234)); + + bytes memory params = abi.encode(currency); + bytes memory invalidParams = _removeFinalByte(params); + assertEq(invalidParams.length, params.length - 1); + + vm.expectRevert(CalldataDecoder.SliceOutOfBounds.selector); + decoder.decodeCurrency(invalidParams); + } + function test_fuzz_decodeActionsRouterParams(bytes memory _actions, bytes[] memory _actionParams) public view { bytes memory params = abi.encode(_actions, _actionParams); (bytes memory actions, bytes[] memory actionParams) = decoder.decodeActionsRouterParams(params); @@ -169,11 +212,7 @@ contract CalldataDecoderTest is Test { bytes memory params = abi.encode(_actions, _actionParams); - bytes memory invalidParams = new bytes(params.length - 1); - // dont copy the final byte - for (uint256 i = 0; i < params.length - 2; i++) { - invalidParams[i] = params[i]; - } + bytes memory invalidParams = _removeFinalByte(params); assertEq(invalidParams.length, params.length - 1); @@ -202,6 +241,18 @@ contract CalldataDecoderTest is Test { assertEq(Currency.unwrap(currency1), Currency.unwrap(_currency1)); } + function test_decodeCurrencyPair_outOutBounds() public { + Currency currency = Currency.wrap(address(0x12341234)); + Currency currency2 = Currency.wrap(address(0x56785678)); + + bytes memory params = abi.encode(currency, currency2); + bytes memory invalidParams = _removeFinalByte(params); + assertEq(invalidParams.length, params.length - 1); + + vm.expectRevert(CalldataDecoder.SliceOutOfBounds.selector); + decoder.decodeCurrencyPair(invalidParams); + } + function test_fuzz_decodeCurrencyPairAndAddress(Currency _currency0, Currency _currency1, address __address) public view @@ -214,6 +265,19 @@ contract CalldataDecoderTest is Test { assertEq(_address, __address); } + function test_decodeCurrencyPairAndAddress_outOutBounds() public { + Currency currency = Currency.wrap(address(0x12341234)); + Currency currency2 = Currency.wrap(address(0x56785678)); + address addy = address(0x23453456); + + bytes memory params = abi.encode(currency, currency2, addy); + bytes memory invalidParams = _removeFinalByte(params); + assertEq(invalidParams.length, params.length - 1); + + vm.expectRevert(CalldataDecoder.SliceOutOfBounds.selector); + decoder.decodeCurrencyPairAndAddress(invalidParams); + } + function test_fuzz_decodeCurrencyAddressAndUint256(Currency _currency, address _addr, uint256 _amount) public view @@ -226,6 +290,19 @@ contract CalldataDecoderTest is Test { assertEq(amount, _amount); } + function test_decodeCurrencyAddressAndUint256_outOutBounds() public { + uint256 value = 12345678; + Currency currency = Currency.wrap(address(0x12341234)); + address addy = address(0x67896789); + + bytes memory params = abi.encode(currency, addy, value); + bytes memory invalidParams = _removeFinalByte(params); + assertEq(invalidParams.length, params.length - 1); + + vm.expectRevert(CalldataDecoder.SliceOutOfBounds.selector); + decoder.decodeCurrencyAddressAndUint256(invalidParams); + } + function test_fuzz_decodeCurrencyAndUint256(Currency _currency, uint256 _amount) public view { bytes memory params = abi.encode(_currency, _amount); (Currency currency, uint256 amount) = decoder.decodeCurrencyAndUint256(params); @@ -234,6 +311,74 @@ contract CalldataDecoderTest is Test { assertEq(amount, _amount); } + function test_decodeCurrencyAndUint256_outOutBounds() public { + uint256 value = 12345678; + Currency currency = Currency.wrap(address(0x12341234)); + + bytes memory params = abi.encode(currency, value); + bytes memory invalidParams = _removeFinalByte(params); + assertEq(invalidParams.length, params.length - 1); + + vm.expectRevert(CalldataDecoder.SliceOutOfBounds.selector); + decoder.decodeCurrencyAndUint256(invalidParams); + } + + function test_fuzz_decodeIncreaseLiquidityFromAmountsParams( + uint256 _tokenId, + uint128 _amount0Max, + uint128 _amount1Max, + bytes calldata _hookData + ) public view { + bytes memory params = abi.encode(_tokenId, _amount0Max, _amount1Max, _hookData); + + (uint256 tokenId, uint128 amount0Max, uint128 amount1Max, bytes memory hookData) = + decoder.decodeIncreaseLiquidityFromDeltasParams(params); + assertEq(_tokenId, tokenId); + assertEq(_amount0Max, amount0Max); + assertEq(_amount1Max, amount1Max); + assertEq(_hookData, hookData); + } + + function test_fuzz_decodeUint256(uint256 _amount) public view { + bytes memory params = abi.encode(_amount); + uint256 amount = decoder.decodeUint256(params); + + assertEq(amount, _amount); + } + + function test_decodeUint256_outOutBounds() public { + uint256 value = 12345678; + + bytes memory params = abi.encode(value); + bytes memory invalidParams = _removeFinalByte(params); + assertEq(invalidParams.length, params.length - 1); + + vm.expectRevert(CalldataDecoder.SliceOutOfBounds.selector); + decoder.decodeUint256(invalidParams); + } + + function test_fuzz_decodeCurrencyUint256AndBool(Currency _currency, uint256 _amount, bool _boolean) public view { + bytes memory params = abi.encode(_currency, _amount, _boolean); + (Currency currency, uint256 amount, bool boolean) = decoder.decodeCurrencyUint256AndBool(params); + + assertEq(Currency.unwrap(currency), Currency.unwrap(_currency)); + assertEq(amount, _amount); + assertEq(boolean, _boolean); + } + + function test_decodeCurrencyUint256AndBool_outOutBounds() public { + uint256 value = 12345678; + Currency currency = Currency.wrap(address(0x12341234)); + bool boolean = true; + + bytes memory params = abi.encode(currency, value, boolean); + bytes memory invalidParams = _removeFinalByte(params); + assertEq(invalidParams.length, params.length - 1); + + vm.expectRevert(CalldataDecoder.SliceOutOfBounds.selector); + decoder.decodeCurrencyUint256AndBool(invalidParams); + } + function _assertEq(PathKey[] memory path1, PathKey[] memory path2) internal pure { assertEq(path1.length, path2.length); for (uint256 i = 0; i < path1.length; i++) { @@ -252,4 +397,12 @@ contract CalldataDecoderTest is Test { assertEq(key1.tickSpacing, key2.tickSpacing); assertEq(address(key1.hooks), address(key2.hooks)); } + + function _removeFinalByte(bytes memory params) internal pure returns (bytes memory result) { + result = new bytes(params.length - 1); + // dont copy the final byte + for (uint256 i = 0; i < params.length - 2; i++) { + result[i] = params[i]; + } + } } diff --git a/test/libraries/Descriptor.t.sol b/test/libraries/Descriptor.t.sol index e191c5a5e..2f3d5fd80 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/libraries/SVG.t.sol b/test/libraries/SVG.t.sol index c322483e2..915557cbb 100644 --- a/test/libraries/SVG.t.sol +++ b/test/libraries/SVG.t.sol @@ -47,4 +47,11 @@ contract DescriptorTest is Test { result = SVG.isRare(2, 0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB); assertFalse(result); } + + function test_substring_succeeds() public pure { + string memory result = SVG.substring("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", 0, 5); + assertEq(result, "0xC02"); + result = SVG.substring("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", 39, 42); + assertEq(result, "Cc2"); + } } diff --git a/test/libraries/SafeCurrencyMetadata.t.sol b/test/libraries/SafeCurrencyMetadata.t.sol new file mode 100644 index 000000000..15f6e0f5d --- /dev/null +++ b/test/libraries/SafeCurrencyMetadata.t.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; +import {SafeCurrencyMetadata} from "../../src/libraries/SafeCurrencyMetadata.sol"; + +contract SafeCurrencyMetadataTest is Test { + function test_truncateSymbol_succeeds() public pure { + // 12 characters + assertEq(SafeCurrencyMetadata.truncateSymbol("123456789012"), "123456789012"); + // 13 characters + assertEq(SafeCurrencyMetadata.truncateSymbol("1234567890123"), "123456789012"); + // 14 characters + assertEq(SafeCurrencyMetadata.truncateSymbol("12345678901234"), "123456789012"); + } +} diff --git a/test/libraries/VanityAddressLib.t.sol b/test/libraries/VanityAddressLib.t.sol new file mode 100644 index 000000000..f9ae5474a --- /dev/null +++ b/test/libraries/VanityAddressLib.t.sol @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import {Test} from "forge-std/Test.sol"; +import {VanityAddressLib} from "../../src/libraries/VanityAddressLib.sol"; + +contract VanityAddressLibTest is Test { + function test_fuzz_reasonableScoreNeverReverts(address test) public pure { + uint256 score = VanityAddressLib.score(address(test)); + assertGe(score, 0); + assertLe(score, 444); + } + + function test_scoreAllFours() public pure { + address addr = address(0x4444444444444444444444444444444444444444); + uint256 score = VanityAddressLib.score(addr); + uint256 expected = 100; // 40 + 40 + 20 = 100 + assertEq(score, expected); + } + + function test_scoreLaterFours() public pure { + address addr = address(0x1444444444444444444444444444444444444444); + uint256 score = VanityAddressLib.score(addr); + uint256 expected = 0; // no leading 4 + assertEq(score, expected); + } + + function test_scoreMixed_4() public pure { + address addr = address(0x0044001111111111111111111111111111114114); + // counts first null byte + // counts first leading 4s after that + // does not count future null bytes + // counts 4 nibbles after that + uint256 score = VanityAddressLib.score(addr); + uint256 expected = 24; // 10 * 2 + 2 + 2 = 24 + assertEq(score, expected); + } + + function test_scoreMixed_44() public pure { + address addr = address(0x0044001111111111111111111111111111114444); + // counts first null byte + // counts first leading 4s after that + // does not count future null bytes + // counts 4 nibbles after that + uint256 score = VanityAddressLib.score(addr); + uint256 expected = 46; // 10 * 2 + 6 + 20 = 46 + assertEq(score, expected); + } + + function test_scoreMixed_halfZeroHalf4() public pure { + address addr = address(0x0004111111111111111111111111111111111111); + // counts first null byte + // counts first leading 4s after that + uint256 score = VanityAddressLib.score(addr); + uint256 expected = 31; // 10 * 3 + 1 = 31 + assertEq(score, expected); + } + + function test_scores_succeed() public pure { + assertEq(VanityAddressLib.score(address(0x0000000000000000000000000000000000000082)), 0); // 0 + assertEq(VanityAddressLib.score(address(0x0400000000000000000000000000000000000000)), 11); // 10 * 1 + 1 = 11 + assertEq(VanityAddressLib.score(address(0x0044000000000000000000000000000000004444)), 46); // 10 * 2 + 6 + 20 = 46 + assertEq(VanityAddressLib.score(address(0x4444000000000000000000000000000000004444)), 88); // 40 + 20 + 20 + 8 = 88 + assertEq(VanityAddressLib.score(address(0x0044440000000000000000000000000000000044)), 86); // 10 * 2 + 40 + 20 + 6 = 86 + assertEq(VanityAddressLib.score(address(0x0000444400000000000000000000000000004444)), 128); // 10 * 4 + 40 + 20 + 20 + 8 = 128 + assertEq(VanityAddressLib.score(address(0x0040444444444444444444444444444444444444)), 77); // 10 * 2 + 37 + 20 = 77 + assertEq(VanityAddressLib.score(address(0x0000000000000000000000000000000000000444)), 373); // 10 * 37 + 3 = 373 + assertEq(VanityAddressLib.score(address(0x0000000000000000000000000000000044444444)), 388); // 10 * 32 + 40 + 20 + 8 = 388 + assertEq(VanityAddressLib.score(address(0x0000000000000000000000000000000000454444)), 365); // 10 * 34 + 20 + 5 = 365 + assertEq(VanityAddressLib.score(address(0x0000000000000000000000000000000000000044)), 382); // 10 * 38 + 2 = 382 + assertEq(VanityAddressLib.score(address(0x0000000000000000000000000000000000000004)), 391); // 10 * 39 + 1 = 391 + assertEq(VanityAddressLib.score(address(0x0000000000000000000000000000000000444444)), 406); // 10 * 34 + 40 + 20 + 6 = 406 + assertEq(VanityAddressLib.score(address(0x0000000000000000000000000000000000044444)), 415); // 10 * 35 + 40 + 20 + 5 = 415 + assertEq(VanityAddressLib.score(address(0x0000000000000000000000000000000000444455)), 404); // 10 * 34 + 40 + 20 + 4 = 404 + assertEq(VanityAddressLib.score(address(0x0000000000000000000000000000000000044445)), 414); // 10 * 35 + 40 + 20 + 4 = 414 + assertEq(VanityAddressLib.score(address(0x0000000000000000000000000000000000004444)), 444); // 10 * 36 + 40 + 20 + 20 + 4 = 444 + } + + function test_betterThan() public pure { + address addr1 = address(0x0011111111111111111111111111111111111111); // 0 points + address addr2 = address(0x4000111111111111111111111111111111111111); // 1 points + address addr3 = address(0x0000411111111111111111111111111111111111); // 10 * 4 + 1 = 41 points + address addr4 = address(0x0000441111111111111111111111111111111111); // 10 * 4 + 2 = 42 points + address addr5 = address(0x0000440011111111111111111111111111111111); // 10 * 4 + 2 = 42 points + assertTrue(VanityAddressLib.betterThan(addr2, addr1)); // 1 > 0 + assertTrue(VanityAddressLib.betterThan(addr3, addr2)); // 41 > 1 + assertTrue(VanityAddressLib.betterThan(addr3, addr1)); // 41 > 0 + assertTrue(VanityAddressLib.betterThan(addr4, addr3)); // 42 > 41 + assertTrue(VanityAddressLib.betterThan(addr4, addr2)); // 42 > 1 + assertTrue(VanityAddressLib.betterThan(addr4, addr1)); // 42 > 0 + assertFalse(VanityAddressLib.betterThan(addr5, addr4)); // 42 == 42 + assertEq(VanityAddressLib.score(addr5), VanityAddressLib.score(addr4)); // 42 == 42 + assertTrue(VanityAddressLib.betterThan(addr5, addr3)); // 42 > 41 + assertTrue(VanityAddressLib.betterThan(addr5, addr2)); // 42 > 1 + assertTrue(VanityAddressLib.betterThan(addr5, addr1)); // 42 > 0 + + address addr6 = address(0x0000000000000000000000000000000000004444); + address addr7 = address(0x0000000000000000000000000000000000000082); + assertTrue(VanityAddressLib.betterThan(addr6, addr7)); // 10 * 36 + 40 + 20 + 20 + 4 = 444 > 0 + } +} diff --git a/test/mocks/MockBadSubscribers.sol b/test/mocks/MockBadSubscribers.sol index 28f151823..d9c99cbbe 100644 --- a/test/mocks/MockBadSubscribers.sol +++ b/test/mocks/MockBadSubscribers.sol @@ -1,9 +1,10 @@ -// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {ISubscriber} from "../../src/interfaces/ISubscriber.sol"; import {PositionManager} from "../../src/PositionManager.sol"; import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {PositionInfo} from "../../src/libraries/PositionInfoLibrary.sol"; /// @notice A subscriber contract that returns values from the subscriber entrypoints contract MockReturnDataSubscriber is ISubscriber { @@ -12,7 +13,6 @@ contract MockReturnDataSubscriber is ISubscriber { uint256 public notifySubscribeCount; uint256 public notifyUnsubscribeCount; uint256 public notifyModifyLiquidityCount; - uint256 public notifyTransferCount; error NotAuthorizedNotifer(address sender); @@ -48,8 +48,10 @@ contract MockReturnDataSubscriber is ISubscriber { notifyModifyLiquidityCount++; } - function notifyTransfer(uint256, address, address) external onlyByPosm { - notifyTransferCount++; + function notifyBurn(uint256 tokenId, address owner, PositionInfo info, uint256 liquidity, BalanceDelta feesAccrued) + external + { + return; } function setReturnDataSize(uint256 _value) external { @@ -90,8 +92,10 @@ contract MockRevertSubscriber is ISubscriber { revert TestRevert("notifyModifyLiquidity"); } - function notifyTransfer(uint256, address, address) external view onlyByPosm { - revert TestRevert("notifyTransfer"); + function notifyBurn(uint256 tokenId, address owner, PositionInfo info, uint256 liquidity, BalanceDelta feesAccrued) + external + { + return; } function setRevert(bool _shouldRevert) external { diff --git a/test/mocks/MockBaseActionsRouter.sol b/test/mocks/MockBaseActionsRouter.sol index b7630297e..53b92a24a 100644 --- a/test/mocks/MockBaseActionsRouter.sol +++ b/test/mocks/MockBaseActionsRouter.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; diff --git a/test/mocks/MockCalldataDecoder.sol b/test/mocks/MockCalldataDecoder.sol index e25d8ec71..151b3c247 100644 --- a/test/mocks/MockCalldataDecoder.sol +++ b/test/mocks/MockCalldataDecoder.sol @@ -22,6 +22,16 @@ contract MockCalldataDecoder { bytes hookData; } + struct MintFromDeltasParams { + PoolKey poolKey; + int24 tickLower; + int24 tickUpper; + uint128 amount0Max; + uint128 amount1Max; + address owner; + bytes hookData; + } + function decodeActionsRouterParams(bytes calldata params) external pure @@ -136,4 +146,45 @@ contract MockCalldataDecoder { { return params.decodeCurrencyAddressAndUint256(); } + + function decodeIncreaseLiquidityFromDeltasParams(bytes calldata params) + external + pure + returns (uint256 tokenId, uint128 amount0Max, uint128 amount1Max, bytes calldata hookData) + { + return params.decodeIncreaseLiquidityFromDeltasParams(); + } + + function decodeMintFromDeltasParams(bytes calldata params) + external + pure + returns (MintFromDeltasParams memory mintParams) + { + ( + PoolKey memory poolKey, + int24 tickLower, + int24 tickUpper, + uint128 amount0Max, + uint128 amount1Max, + address owner, + bytes memory hookData + ) = params.decodeMintFromDeltasParams(); + return MintFromDeltasParams({ + poolKey: poolKey, + tickLower: tickLower, + tickUpper: tickUpper, + amount0Max: amount0Max, + amount1Max: amount1Max, + owner: owner, + hookData: hookData + }); + } + + function decodeUint256(bytes calldata params) external pure returns (uint256) { + return params.decodeUint256(); + } + + function decodeCurrencyUint256AndBool(bytes calldata params) external pure returns (Currency, uint256, bool) { + return params.decodeCurrencyUint256AndBool(); + } } diff --git a/test/mocks/MockDeltaResolver.sol b/test/mocks/MockDeltaResolver.sol index 327f5e1c9..6de67fbb7 100644 --- a/test/mocks/MockDeltaResolver.sol +++ b/test/mocks/MockDeltaResolver.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; diff --git a/test/mocks/MockERC721Permit.sol b/test/mocks/MockERC721Permit.sol index bf3792084..0cd2a7789 100644 --- a/test/mocks/MockERC721Permit.sol +++ b/test/mocks/MockERC721Permit.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {ERC721Permit_v4} from "../../src/base/ERC721Permit_v4.sol"; diff --git a/test/mocks/MockFeeOnTransfer.sol b/test/mocks/MockFeeOnTransfer.sol new file mode 100644 index 000000000..7488a5d87 --- /dev/null +++ b/test/mocks/MockFeeOnTransfer.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {MockERC20} from "solmate/src/test/utils/mocks/MockERC20.sol"; +import {IPositionManager} from "../../src/interfaces/IPositionManager.sol"; +import {BipsLibrary} from "../../src/libraries/BipsLibrary.sol"; + +contract MockFOT is MockERC20 { + using BipsLibrary for uint256; + + IPositionManager immutable posm; + + uint256 public bips; + + constructor(IPositionManager _posm) MockERC20("FOT Token", "FOT", 18) { + posm = _posm; + } + + function setFee(uint256 amountInBips) public { + bips = amountInBips; + } + + function transferFrom(address from, address to, uint256 amount) public override returns (bool) { + uint256 allowed = allowance[from][msg.sender]; + + if (allowed != type(uint256).max) allowance[from][msg.sender] = allowed - amount; + + balanceOf[from] -= amount; + + // bips% fee on the recipient + uint256 amountAfterFee = amount - amount.calculatePortion(bips); + + // Cannot overflow because the sum of all user + // balances can't exceed the max uint256 value. + unchecked { + balanceOf[to] += amountAfterFee; + } + + emit Transfer(from, to, amount); + + return true; + } +} diff --git a/test/mocks/MockMulticall.sol b/test/mocks/MockMulticall.sol index 38ddaa09e..1bae4d1f0 100644 --- a/test/mocks/MockMulticall.sol +++ b/test/mocks/MockMulticall.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "../../src/base/Multicall_v4.sol"; diff --git a/test/mocks/MockReenterHook.sol b/test/mocks/MockReenterHook.sol new file mode 100644 index 000000000..e6d6f2db5 --- /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/mocks/MockSafeCallback.sol b/test/mocks/MockSafeCallback.sol index 232fbe3c5..4739a1461 100644 --- a/test/mocks/MockSafeCallback.sol +++ b/test/mocks/MockSafeCallback.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; diff --git a/test/mocks/MockSubscriber.sol b/test/mocks/MockSubscriber.sol index 1e1fda151..64a167abb 100644 --- a/test/mocks/MockSubscriber.sol +++ b/test/mocks/MockSubscriber.sol @@ -1,9 +1,10 @@ -// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {ISubscriber} from "../../src/interfaces/ISubscriber.sol"; import {PositionManager} from "../../src/PositionManager.sol"; import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {PositionInfo} from "../../src/libraries/PositionInfoLibrary.sol"; /// @notice A subscriber contract that ingests updates from the v4 position manager contract MockSubscriber is ISubscriber { @@ -12,7 +13,7 @@ contract MockSubscriber is ISubscriber { uint256 public notifySubscribeCount; uint256 public notifyUnsubscribeCount; uint256 public notifyModifyLiquidityCount; - uint256 public notifyTransferCount; + uint256 public notifyBurnCount; int256 public liquidityChange; BalanceDelta public feesAccrued; @@ -46,7 +47,10 @@ contract MockSubscriber is ISubscriber { feesAccrued = _feesAccrued; } - function notifyTransfer(uint256, address, address) external onlyByPosm { - notifyTransferCount++; + function notifyBurn(uint256 tokenId, address owner, PositionInfo info, uint256 liquidity, BalanceDelta feesAccrued) + external + onlyByPosm + { + notifyBurnCount++; } } diff --git a/test/mocks/MockUnorderedNonce.sol b/test/mocks/MockUnorderedNonce.sol index 8f3cfc57a..338b8f2d2 100644 --- a/test/mocks/MockUnorderedNonce.sol +++ b/test/mocks/MockUnorderedNonce.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {UnorderedNonce} from "../../src/base/UnorderedNonce.sol"; diff --git a/test/mocks/ReentrantToken.sol b/test/mocks/ReentrantToken.sol index 63cc71ee3..522d76fba 100644 --- a/test/mocks/ReentrantToken.sol +++ b/test/mocks/ReentrantToken.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {MockERC20} from "solmate/src/test/utils/mocks/MockERC20.sol"; diff --git a/test/position-managers/PositionManager.gas.t.sol b/test/position-managers/PositionManager.gas.t.sol index cf0777483..bf27604f9 100644 --- a/test/position-managers/PositionManager.gas.t.sol +++ b/test/position-managers/PositionManager.gas.t.sol @@ -75,6 +75,10 @@ contract PosMGasTest is Test, PosmTestSetup, GasSnapshot { sub = new MockSubscriber(lpm); } + function test_bytecodeSize_positionManager() public { + snapSize("positionManager bytecode size", address(lpm)); + } + function test_gas_mint_withClose() public { Plan memory planner = Planner.init().add( Actions.MINT_POSITION, diff --git a/test/position-managers/PositionManager.modifyLiquidities.t.sol b/test/position-managers/PositionManager.modifyLiquidities.t.sol index e71c121fe..664758616 100644 --- a/test/position-managers/PositionManager.modifyLiquidities.t.sol +++ b/test/position-managers/PositionManager.modifyLiquidities.t.sol @@ -2,6 +2,9 @@ pragma solidity ^0.8.24; import "forge-std/Test.sol"; +import {IERC20} from "forge-std/interfaces/IERC20.sol"; + +import {CustomRevert} from "@uniswap/v4-core/src/libraries/CustomRevert.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,27 +16,47 @@ 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 {Pool} from "@uniswap/v4-core/src/libraries/Pool.sol"; + +import {MockERC20} from "solmate/src/test/utils/mocks/MockERC20.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"; import {PositionConfig} from "../shared/PositionConfig.sol"; +import {BipsLibrary} from "../../src/libraries/BipsLibrary.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 {MockFOT} from "../mocks/MockFeeOnTransfer.sol"; contract PositionManagerModifyLiquiditiesTest is Test, PosmTestSetup, LiquidityFuzzers { using StateLibrary for IPoolManager; using PoolIdLibrary for PoolKey; + using Planner for Plan; + using BipsLibrary for uint256; PoolId poolId; address alice; uint256 alicePK; address bob; + PoolKey fotKey; + PositionConfig config; + PositionConfig wethConfig; + PositionConfig nativeConfig; + PositionConfig fotConfig; + + MockERC20 fotToken; function setUp() public { (alice, alicePK) = makeAddrAndKey("ALICE"); @@ -54,8 +77,28 @@ contract PositionManagerModifyLiquiditiesTest is Test, PosmTestSetup, LiquidityF seedBalance(address(hookModifyLiquidities)); (key, poolId) = initPool(currency0, currency1, IHooks(hookModifyLiquidities), 3000, SQRT_PRICE_1_1); + wethKey = initPoolUnsorted(Currency.wrap(address(_WETH9)), 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); + + fotToken = new MockFOT(lpm); + approvePosmCurrency(Currency.wrap(address(fotToken))); + seedToken(fotToken, address(this)); + fotKey = initPoolUnsorted(Currency.wrap(address(fotToken)), currency1, IHooks(address(0)), 3000, SQRT_PRICE_1_1); } /// @dev minting liquidity without approval is allowable @@ -211,9 +254,11 @@ contract PositionManagerModifyLiquiditiesTest is Test, PosmTestSetup, LiquidityF // should revert because hook is not approved vm.expectRevert( abi.encodeWithSelector( - Hooks.Wrap__FailedHookCall.selector, + CustomRevert.WrappedError.selector, address(hookModifyLiquidities), - abi.encodeWithSelector(IPositionManager.NotApproved.selector, address(hookModifyLiquidities)) + IHooks.beforeSwap.selector, + abi.encodeWithSelector(IPositionManager.NotApproved.selector, address(hookModifyLiquidities)), + abi.encodeWithSelector(Hooks.HookCallFailed.selector) ) ); swap(key, true, -1e18, calls); @@ -232,9 +277,11 @@ contract PositionManagerModifyLiquiditiesTest is Test, PosmTestSetup, LiquidityF // should revert because hook is not approved vm.expectRevert( abi.encodeWithSelector( - Hooks.Wrap__FailedHookCall.selector, + CustomRevert.WrappedError.selector, address(hookModifyLiquidities), - abi.encodeWithSelector(IPositionManager.NotApproved.selector, address(hookModifyLiquidities)) + IHooks.beforeSwap.selector, + abi.encodeWithSelector(IPositionManager.NotApproved.selector, address(hookModifyLiquidities)), + abi.encodeWithSelector(Hooks.HookCallFailed.selector) ) ); swap(key, true, -1e18, calls); @@ -257,9 +304,11 @@ contract PositionManagerModifyLiquiditiesTest is Test, PosmTestSetup, LiquidityF // should revert because hook is not approved vm.expectRevert( abi.encodeWithSelector( - Hooks.Wrap__FailedHookCall.selector, + CustomRevert.WrappedError.selector, address(hookModifyLiquidities), - abi.encodeWithSelector(IPositionManager.NotApproved.selector, address(hookModifyLiquidities)) + IHooks.beforeSwap.selector, + abi.encodeWithSelector(IPositionManager.NotApproved.selector, address(hookModifyLiquidities)), + abi.encodeWithSelector(Hooks.HookCallFailed.selector) ) ); swap(key, true, -1e18, calls); @@ -278,9 +327,11 @@ contract PositionManagerModifyLiquiditiesTest is Test, PosmTestSetup, LiquidityF // should revert because hook is not approved vm.expectRevert( abi.encodeWithSelector( - Hooks.Wrap__FailedHookCall.selector, + CustomRevert.WrappedError.selector, address(hookModifyLiquidities), - abi.encodeWithSelector(IPositionManager.NotApproved.selector, address(hookModifyLiquidities)) + IHooks.beforeSwap.selector, + abi.encodeWithSelector(IPositionManager.NotApproved.selector, address(hookModifyLiquidities)), + abi.encodeWithSelector(Hooks.HookCallFailed.selector) ) ); swap(key, true, -1e18, calls); @@ -301,11 +352,616 @@ contract PositionManagerModifyLiquiditiesTest is Test, PosmTestSetup, LiquidityF // should revert because hook is re-entering modifyLiquiditiesWithoutUnlock vm.expectRevert( abi.encodeWithSelector( - Hooks.Wrap__FailedHookCall.selector, + CustomRevert.WrappedError.selector, address(hookModifyLiquidities), - abi.encodeWithSelector(ReentrancyLock.ContractLocked.selector) + IHooks.beforeAddLiquidity.selector, + abi.encodeWithSelector(ReentrancyLock.ContractLocked.selector), + abi.encodeWithSelector(Hooks.HookCallFailed.selector) ) ); 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); + } + + function test_mintFromDeltas_fot() public { + // Use a 1% fee. + MockFOT(address(fotToken)).setFee(100); + uint256 tokenId = lpm.nextTokenId(); + + uint256 fotBalanceBefore = Currency.wrap(address(fotToken)).balanceOf(address(this)); + + uint256 amountAfterTransfer = 990e18; + uint256 amountToSendFot = 1000e18; + + (uint256 amount0, uint256 amount1) = fotKey.currency0 == Currency.wrap(address(fotToken)) + ? (amountToSendFot, amountAfterTransfer) + : (amountAfterTransfer, amountToSendFot); + + // Calculcate the expected liquidity from the amounts after the transfer. They are the same for both currencies. + uint256 expectedLiquidity = LiquidityAmounts.getLiquidityForAmounts( + SQRT_PRICE_1_1, + TickMath.getSqrtPriceAtTick(LIQUIDITY_PARAMS.tickLower), + TickMath.getSqrtPriceAtTick(LIQUIDITY_PARAMS.tickUpper), + amountAfterTransfer, + amountAfterTransfer + ); + + Plan memory planner = Planner.init(); + planner.add(Actions.SETTLE, abi.encode(fotKey.currency0, amount0, true)); + planner.add(Actions.SETTLE, abi.encode(fotKey.currency1, amount1, true)); + planner.add( + Actions.MINT_POSITION_FROM_DELTAS, + abi.encode( + fotKey, + LIQUIDITY_PARAMS.tickLower, + LIQUIDITY_PARAMS.tickUpper, + MAX_SLIPPAGE_INCREASE, + MAX_SLIPPAGE_INCREASE, + ActionConstants.MSG_SENDER, + ZERO_BYTES + ) + ); + + bytes memory plan = planner.encode(); + + lpm.modifyLiquidities(plan, _deadline); + + uint256 fotBalanceAfter = Currency.wrap(address(fotToken)).balanceOf(address(this)); + + assertEq(lpm.ownerOf(tokenId), address(this)); + assertEq(lpm.getPositionLiquidity(tokenId), expectedLiquidity); + assertEq(fotBalanceBefore - fotBalanceAfter, 1000e18); + } + + function test_increaseFromDeltas() public { + uint128 initialLiquidity = 1000e18; + uint256 tokenId = lpm.nextTokenId(); + fotConfig = PositionConfig({poolKey: fotKey, tickLower: -120, tickUpper: 120}); + + mint(fotConfig, initialLiquidity, address(this), ZERO_BYTES); + + assertEq(lpm.ownerOf(tokenId), address(this)); + assertEq(lpm.getPositionLiquidity(tokenId), initialLiquidity); + + Plan memory planner = Planner.init(); + planner.add(Actions.SETTLE, abi.encode(fotKey.currency0, 10e18, true)); + planner.add(Actions.SETTLE, abi.encode(fotKey.currency1, 10e18, true)); + planner.add( + Actions.INCREASE_LIQUIDITY_FROM_DELTAS, + abi.encode(tokenId, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, ZERO_BYTES) + ); + + bytes memory actions = planner.encode(); + + lpm.modifyLiquidities(actions, _deadline); + + uint128 newLiquidity = LiquidityAmounts.getLiquidityForAmounts( + SQRT_PRICE_1_1, + TickMath.getSqrtPriceAtTick(fotConfig.tickLower), + TickMath.getSqrtPriceAtTick(fotConfig.tickUpper), + 10e18, + 10e18 + ); + + assertEq(lpm.getPositionLiquidity(tokenId), initialLiquidity + newLiquidity); + } + + function test_increaseFromDeltas_fot() public { + uint128 initialLiquidity = 1000e18; + uint256 tokenId = lpm.nextTokenId(); + fotConfig = PositionConfig({poolKey: fotKey, tickLower: -120, tickUpper: 120}); + + mint(fotConfig, initialLiquidity, address(this), ZERO_BYTES); + + assertEq(lpm.ownerOf(tokenId), address(this)); + assertEq(lpm.getPositionLiquidity(tokenId), initialLiquidity); + + // Use a 1% fee. + MockFOT(address(fotToken)).setFee(100); + + // Set the fee on transfer amount 1% higher. + (uint256 amount0, uint256 amount1) = + fotKey.currency0 == Currency.wrap(address(fotToken)) ? (100e18, 99e18) : (99e19, 100e18); + + Plan memory planner = Planner.init(); + planner.add(Actions.SETTLE, abi.encode(fotKey.currency0, amount0, true)); + planner.add(Actions.SETTLE, abi.encode(fotKey.currency1, amount1, true)); + planner.add( + Actions.INCREASE_LIQUIDITY_FROM_DELTAS, + abi.encode(tokenId, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, ZERO_BYTES) + ); + + bytes memory actions = planner.encode(); + + lpm.modifyLiquidities(actions, _deadline); + + (uint256 amount0AfterTransfer, uint256 amount1AfterTransfer) = + fotKey.currency0 == Currency.wrap(address(fotToken)) ? (99e18, 100e18) : (100e18, 99e19); + + uint128 newLiquidity = LiquidityAmounts.getLiquidityForAmounts( + SQRT_PRICE_1_1, + TickMath.getSqrtPriceAtTick(fotConfig.tickLower), + TickMath.getSqrtPriceAtTick(fotConfig.tickUpper), + amount0AfterTransfer, + amount1AfterTransfer + ); + + assertEq(lpm.getPositionLiquidity(tokenId), initialLiquidity + newLiquidity); + } + + function test_fuzz_mintFromDeltas_burn_fot( + uint256 bips, + uint256 amount0, + uint256 amount1, + int24 tickLower, + int24 tickUpper + ) public { + bips = bound(bips, 1, 10_000); + MockFOT(address(fotToken)).setFee(bips); + + tickLower = int24( + bound( + tickLower, + fotKey.tickSpacing * (TickMath.MIN_TICK / fotKey.tickSpacing), + fotKey.tickSpacing * (TickMath.MAX_TICK / fotKey.tickSpacing) + ) + ); + tickUpper = int24( + bound( + tickUpper, + fotKey.tickSpacing * (TickMath.MIN_TICK / fotKey.tickSpacing), + fotKey.tickSpacing * (TickMath.MAX_TICK / fotKey.tickSpacing) + ) + ); + + tickLower = fotKey.tickSpacing * (tickLower / fotKey.tickSpacing); + tickUpper = fotKey.tickSpacing * (tickUpper / fotKey.tickSpacing); + vm.assume(tickUpper > tickLower); + + (uint160 sqrtPriceX96,,,) = manager.getSlot0(fotKey.toId()); + uint128 maxLiquidityPerTick = Pool.tickSpacingToMaxLiquidityPerTick(fotKey.tickSpacing); + + (uint256 maxAmount0, uint256 maxAmount1) = LiquidityAmounts.getAmountsForLiquidity( + sqrtPriceX96, + TickMath.getSqrtPriceAtTick(tickLower), + TickMath.getSqrtPriceAtTick(tickUpper), + maxLiquidityPerTick + ); + + maxAmount0 = maxAmount0 == 0 ? 1 : maxAmount0 > STARTING_USER_BALANCE ? STARTING_USER_BALANCE : maxAmount0; + maxAmount1 = maxAmount1 == 0 ? 1 : maxAmount1 > STARTING_USER_BALANCE ? STARTING_USER_BALANCE : maxAmount1; + amount0 = bound(amount0, 1, maxAmount0); + amount1 = bound(amount1, 1, maxAmount1); + + uint256 tokenId = lpm.nextTokenId(); + + uint256 balance0 = fotKey.currency0.balanceOf(address(this)); + uint256 balance1 = fotKey.currency1.balanceOf(address(this)); + uint256 balance0PM = fotKey.currency0.balanceOf(address(manager)); + uint256 balance1PM = fotKey.currency1.balanceOf(address(manager)); + + Plan memory planner = Planner.init(); + planner.add(Actions.SETTLE, abi.encode(fotKey.currency0, amount0, true)); + planner.add(Actions.SETTLE, abi.encode(fotKey.currency1, amount1, true)); + planner.add( + Actions.MINT_POSITION_FROM_DELTAS, + abi.encode( + fotKey, + tickLower, + tickUpper, + MAX_SLIPPAGE_INCREASE, + MAX_SLIPPAGE_INCREASE, + ActionConstants.MSG_SENDER, + ZERO_BYTES + ) + ); + // take the excess of each currency + planner.add(Actions.TAKE_PAIR, abi.encode(fotKey.currency0, fotKey.currency1, ActionConstants.MSG_SENDER)); + + bytes memory actions = planner.encode(); + + bool currency0IsFOT = fotKey.currency0 == Currency.wrap(address(fotToken)); + bool positionIsEntirelyInOtherToken = currency0IsFOT + ? tickUpper <= TickMath.getTickAtSqrtPrice(sqrtPriceX96) + : tickLower > TickMath.getTickAtSqrtPrice(sqrtPriceX96); + + if (bips == 10000 && !positionIsEntirelyInOtherToken) { + vm.expectRevert(Position.CannotUpdateEmptyPosition.selector); + lpm.modifyLiquidities(actions, _deadline); + } else { + // MINT FROM DELTAS. + lpm.modifyLiquidities(actions, _deadline); + + uint256 balance0After = fotKey.currency0.balanceOf(address(this)); + uint256 balance1After = fotKey.currency1.balanceOf(address(this)); + uint256 balance0PMAfter = fotKey.currency0.balanceOf(address(manager)); + uint256 balance1PMAfter = fotKey.currency1.balanceOf(address(manager)); + + // Calculate the expected resulting balances used to create liquidity after the fee is applied. + uint256 amountInFOT = currency0IsFOT ? amount0 : amount1; + uint256 expectedFee = amountInFOT.calculatePortion(bips); + (uint256 expected0, uint256 expected1) = currency0IsFOT + ? (balance0 - balance0After - expectedFee, balance1 - balance1After) + : (balance0 - balance0After, balance1 - balance1After - expectedFee); + + assertEq(expected0, balance0PMAfter - balance0PM); + assertEq(expected1, balance1PMAfter - balance1PM); + + // the liquidity that was created is a diff of the balance change + uint128 expectedLiquidity = LiquidityAmounts.getLiquidityForAmounts( + sqrtPriceX96, + TickMath.getSqrtPriceAtTick(tickLower), + TickMath.getSqrtPriceAtTick(tickUpper), + expected0, + expected1 + ); + + assertEq(lpm.ownerOf(tokenId), address(this)); + assertEq(lpm.getPositionLiquidity(tokenId), expectedLiquidity); + + // BURN. + planner = Planner.init(); + // Note that the slippage does not include the fee from the transfer. + planner.add( + Actions.BURN_POSITION, + abi.encode(tokenId, expected0 == 0 ? 0 : expected0 - 1, expected1 == 0 ? 0 : expected1 - 1, ZERO_BYTES) + ); + + planner.add(Actions.TAKE_PAIR, abi.encode(fotKey.currency0, fotKey.currency1, ActionConstants.MSG_SENDER)); + + actions = planner.encode(); + + lpm.modifyLiquidities(actions, _deadline); + + assertEq(lpm.getPositionLiquidity(tokenId), 0); + } + } } diff --git a/test/position-managers/PositionManager.multicall.t.sol b/test/position-managers/PositionManager.multicall.t.sol index c70f68b39..087c21027 100644 --- a/test/position-managers/PositionManager.multicall.t.sol +++ b/test/position-managers/PositionManager.multicall.t.sol @@ -133,6 +133,47 @@ contract PositionManagerMulticallTest is Test, Permit2SignatureHelpers, PosmTest assertGt(result.amount1(), 0); } + function test_multicall_initializePool_twice_andMint_succeeds() public { + key = PoolKey({currency0: currency0, currency1: currency1, fee: 0, tickSpacing: 10, hooks: IHooks(address(0))}); + manager.initialize(key, SQRT_PRICE_1_1); + + // Use multicall to initialize the pool again. + bytes[] memory calls = new bytes[](2); + calls[0] = abi.encodeWithSelector(lpm.initializePool.selector, key, SQRT_PRICE_1_1); + + config = PositionConfig({ + poolKey: key, + tickLower: TickMath.minUsableTick(key.tickSpacing), + tickUpper: TickMath.maxUsableTick(key.tickSpacing) + }); + + Plan memory planner = Planner.init(); + planner.add( + Actions.MINT_POSITION, + abi.encode( + config.poolKey, + config.tickLower, + config.tickUpper, + 100e18, + MAX_SLIPPAGE_INCREASE, + MAX_SLIPPAGE_INCREASE, + ActionConstants.MSG_SENDER, + ZERO_BYTES + ) + ); + bytes memory actions = planner.finalizeModifyLiquidityWithClose(config.poolKey); + + calls[1] = abi.encodeWithSelector(IPositionManager.modifyLiquidities.selector, actions, _deadline); + + IMulticall_v4(address(lpm)).multicall(calls); + + // test swap, doesn't revert, showing the mint succeeded even after initialize reverted + int256 amountSpecified = -1e18; + BalanceDelta result = swap(key, true, amountSpecified, ZERO_BYTES); + assertEq(result.amount0(), amountSpecified); + assertGt(result.amount1(), 0); + } + function test_multicall_initializePool_mint_native() public { key = PoolKey({ currency0: CurrencyLibrary.ADDRESS_ZERO, @@ -227,26 +268,6 @@ contract PositionManagerMulticallTest is Test, Permit2SignatureHelpers, PosmTest lpm.multicall(calls); } - // create a pool where tickSpacing is negative - // core's TickSpacingTooSmall(int24) should bubble up through Multicall - function test_multicall_bubbleRevert_core_args() public { - int24 tickSpacing = -10; - key = PoolKey({ - currency0: currency0, - currency1: currency1, - fee: 0, - tickSpacing: tickSpacing, - hooks: IHooks(address(0)) - }); - - // Use multicall to initialize a pool - bytes[] memory calls = new bytes[](1); - calls[0] = abi.encodeWithSelector(PoolInitializer.initializePool.selector, key, SQRT_PRICE_1_1); - - vm.expectRevert(abi.encodeWithSelector(IPoolManager.TickSpacingTooSmall.selector, tickSpacing)); - lpm.multicall(calls); - } - function test_multicall_permitAndDecrease() public { config = PositionConfig({poolKey: key, tickLower: -60, tickUpper: 60}); uint256 liquidityAlice = 1e18; diff --git a/test/position-managers/PositionManager.notifier.t.sol b/test/position-managers/PositionManager.notifier.t.sol index 391446a84..de25bc412 100644 --- a/test/position-managers/PositionManager.notifier.t.sol +++ b/test/position-managers/PositionManager.notifier.t.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.24; import "forge-std/Test.sol"; +import {CustomRevert} from "@uniswap/v4-core/src/libraries/CustomRevert.sol"; import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; import {Position} from "@uniswap/v4-core/src/libraries/Position.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; @@ -9,6 +10,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 +22,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 +34,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 +55,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 } @@ -198,7 +215,7 @@ contract PositionManagerNotifierTest is Test, PosmTestSetup, GasSnapshot { assertEq(int256(sub.feesAccrued().amount1()), int256(feeRevenue1) - 1 wei); } - function test_notifyTransfer_withTransferFrom_succeeds() public { + function test_transferFrom_unsubscribes() public { uint256 tokenId = lpm.nextTokenId(); mint(config, 100e18, alice, ZERO_BYTES); @@ -214,10 +231,12 @@ contract PositionManagerNotifierTest is Test, PosmTestSetup, GasSnapshot { lpm.transferFrom(alice, bob, tokenId); - assertEq(sub.notifyTransferCount(), 1); + assertEq(sub.notifyUnsubscribeCount(), 1); + assertEq(lpm.positionInfo(tokenId).hasSubscriber(), false); + assertEq(address(lpm.subscriber(tokenId)), address(0)); } - function test_notifyTransfer_withTransferFrom_selfDestruct_revert() public { + function test_transferFrom_unsubscribes_selfDestruct() public { uint256 tokenId = lpm.nextTokenId(); mint(config, 100e18, alice, ZERO_BYTES); @@ -233,11 +252,14 @@ contract PositionManagerNotifierTest is Test, PosmTestSetup, GasSnapshot { // simulate selfdestruct by etching the bytecode to 0 vm.etch(address(sub), ZERO_BYTES); - vm.expectRevert(INotifier.NoCodeSubscriber.selector); + // unsubscribe happens anyway lpm.transferFrom(alice, bob, tokenId); + + assertEq(lpm.positionInfo(tokenId).hasSubscriber(), false); + assertEq(address(lpm.subscriber(tokenId)), address(0)); } - function test_notifyTransfer_withSafeTransferFrom_succeeds() public { + function test_safeTransferFrom_unsubscribes() public { uint256 tokenId = lpm.nextTokenId(); mint(config, 100e18, alice, ZERO_BYTES); @@ -253,10 +275,12 @@ contract PositionManagerNotifierTest is Test, PosmTestSetup, GasSnapshot { lpm.safeTransferFrom(alice, bob, tokenId); - assertEq(sub.notifyTransferCount(), 1); + assertEq(sub.notifyUnsubscribeCount(), 1); + assertEq(lpm.positionInfo(tokenId).hasSubscriber(), false); + assertEq(address(lpm.subscriber(tokenId)), address(0)); } - function test_notifyTransfer_withSafeTransferFrom_selfDestruct_revert() public { + function test_safeTransferFrom_unsubscribes_selfDestruct() public { uint256 tokenId = lpm.nextTokenId(); mint(config, 100e18, alice, ZERO_BYTES); @@ -272,11 +296,14 @@ contract PositionManagerNotifierTest is Test, PosmTestSetup, GasSnapshot { // simulate selfdestruct by etching the bytecode to 0 vm.etch(address(sub), ZERO_BYTES); - vm.expectRevert(INotifier.NoCodeSubscriber.selector); + // unsubscribe happens anyway lpm.safeTransferFrom(alice, bob, tokenId); + + assertEq(lpm.positionInfo(tokenId).hasSubscriber(), false); + assertEq(address(lpm.subscriber(tokenId)), address(0)); } - function test_notifyTransfer_withSafeTransferFromData_succeeds() public { + function test_safeTransferFrom_unsubscribes_withData() public { uint256 tokenId = lpm.nextTokenId(); mint(config, 100e18, alice, ZERO_BYTES); @@ -292,7 +319,9 @@ contract PositionManagerNotifierTest is Test, PosmTestSetup, GasSnapshot { lpm.safeTransferFrom(alice, bob, tokenId, ""); - assertEq(sub.notifyTransferCount(), 1); + assertEq(sub.notifyUnsubscribeCount(), 1); + assertEq(lpm.positionInfo(tokenId).hasSubscriber(), false); + assertEq(address(lpm.subscriber(tokenId)), address(0)); } function test_unsubscribe_succeeds() public { @@ -495,9 +524,11 @@ contract PositionManagerNotifierTest is Test, PosmTestSetup, GasSnapshot { vm.expectRevert( abi.encodeWithSelector( - INotifier.Wrap__SubscriptionReverted.selector, + CustomRevert.WrappedError.selector, address(revertSubscriber), - abi.encodeWithSelector(MockRevertSubscriber.TestRevert.selector, "notifySubscribe") + ISubscriber.notifySubscribe.selector, + abi.encodeWithSelector(MockRevertSubscriber.TestRevert.selector, "notifySubscribe"), + abi.encodeWithSelector(INotifier.SubscriptionReverted.selector) ) ); lpm.subscribe(tokenId, address(revertSubscriber), ZERO_BYTES); @@ -525,79 +556,18 @@ contract PositionManagerNotifierTest is Test, PosmTestSetup, GasSnapshot { bytes memory calls = plan.finalizeModifyLiquidityWithSettlePair(config.poolKey); vm.expectRevert( abi.encodeWithSelector( - INotifier.Wrap__ModifyLiquidityNotificationReverted.selector, + CustomRevert.WrappedError.selector, address(revertSubscriber), - abi.encodeWithSelector(MockRevertSubscriber.TestRevert.selector, "notifyModifyLiquidity") + ISubscriber.notifyModifyLiquidity.selector, + abi.encodeWithSelector(MockRevertSubscriber.TestRevert.selector, "notifyModifyLiquidity"), + abi.encodeWithSelector(INotifier.ModifyLiquidityNotificationReverted.selector) ) ); lpm.modifyLiquidities(calls, _deadline); } - function test_notifyTransfer_withTransferFrom_wraps_revert() public { - uint256 tokenId = lpm.nextTokenId(); - mint(config, 100e18, alice, ZERO_BYTES); - - // approve this contract to operate on alices liq - vm.startPrank(alice); - lpm.approve(address(this), tokenId); - vm.stopPrank(); - - lpm.subscribe(tokenId, address(revertSubscriber), ZERO_BYTES); - - vm.expectRevert( - abi.encodeWithSelector( - INotifier.Wrap__TransferNotificationReverted.selector, - address(revertSubscriber), - abi.encodeWithSelector(MockRevertSubscriber.TestRevert.selector, "notifyTransfer") - ) - ); - lpm.transferFrom(alice, bob, tokenId); - } - - function test_notifyTransfer_withSafeTransferFrom_wraps_revert() public { - uint256 tokenId = lpm.nextTokenId(); - mint(config, 100e18, alice, ZERO_BYTES); - - // approve this contract to operate on alices liq - vm.startPrank(alice); - lpm.approve(address(this), tokenId); - vm.stopPrank(); - - lpm.subscribe(tokenId, address(revertSubscriber), ZERO_BYTES); - - vm.expectRevert( - abi.encodeWithSelector( - INotifier.Wrap__TransferNotificationReverted.selector, - address(revertSubscriber), - abi.encodeWithSelector(MockRevertSubscriber.TestRevert.selector, "notifyTransfer") - ) - ); - lpm.safeTransferFrom(alice, bob, tokenId); - } - - function test_notifyTransfer_withSafeTransferFromData_wraps_revert() public { - uint256 tokenId = lpm.nextTokenId(); - mint(config, 100e18, alice, ZERO_BYTES); - - // approve this contract to operate on alices liq - vm.startPrank(alice); - lpm.approve(address(this), tokenId); - vm.stopPrank(); - - lpm.subscribe(tokenId, address(revertSubscriber), ZERO_BYTES); - - vm.expectRevert( - abi.encodeWithSelector( - INotifier.Wrap__TransferNotificationReverted.selector, - address(revertSubscriber), - abi.encodeWithSelector(MockRevertSubscriber.TestRevert.selector, "notifyTransfer") - ) - ); - lpm.safeTransferFrom(alice, bob, tokenId, ""); - } - - /// @notice burning a position will automatically notify unsubscribe - function test_burn_unsubscribe() public { + /// @notice burning a position will automatically notify burn + function test_notifyBurn_succeeds() public { uint256 tokenId = lpm.nextTokenId(); mint(config, 100e18, alice, ZERO_BYTES); @@ -613,12 +583,13 @@ contract PositionManagerNotifierTest is Test, PosmTestSetup, GasSnapshot { assertEq(lpm.positionInfo(tokenId).hasSubscriber(), true); assertEq(sub.notifyUnsubscribeCount(), 0); - // burn the position, causing an unsubscribe + // burn the position, causing a notifyBurn burn(tokenId, config, ZERO_BYTES); // position is now unsubscribed assertEq(lpm.positionInfo(tokenId).hasSubscriber(), false); - assertEq(sub.notifyUnsubscribeCount(), 1); + assertEq(sub.notifyUnsubscribeCount(), 0); + assertEq(sub.notifyBurnCount(), 1); } /// @notice Test that users cannot forcibly avoid unsubscribe logic via gas limits @@ -647,4 +618,75 @@ 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( + CustomRevert.WrappedError.selector, + address(reenterHook), + IHooks.beforeAddLiquidity.selector, + abi.encodeWithSelector(IPositionManager.PoolManagerMustBeLocked.selector), + abi.encodeWithSelector(Hooks.HookCallFailed.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( + CustomRevert.WrappedError.selector, + address(reenterHook), + IHooks.beforeAddLiquidity.selector, + abi.encodeWithSelector(IPositionManager.PoolManagerMustBeLocked.selector), + abi.encodeWithSelector(Hooks.HookCallFailed.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( + CustomRevert.WrappedError.selector, + address(reenterHook), + IHooks.beforeAddLiquidity.selector, + abi.encodeWithSelector(IPositionManager.PoolManagerMustBeLocked.selector), + abi.encodeWithSelector(Hooks.HookCallFailed.selector) + ) + ); + lpm.modifyLiquidities(actions, _deadline); + } } diff --git a/test/router/Payments.gas.t.sol b/test/router/Payments.gas.t.sol index 9717fdad0..6ee98bffc 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,10 +36,11 @@ 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)); + plan = plan.add(Actions.SETTLE, abi.encode(key0.currency0, amountIn, true)); + plan = plan.add(Actions.TAKE_ALL, abi.encode(key0.currency1, 0)); bytes memory data = plan.encode(); router.executeActions(data); @@ -49,7 +50,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 +67,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 8f96a4085..cb50c1728 100644 --- a/test/router/Payments.t.sol +++ b/test/router/Payments.t.sol @@ -3,13 +3,13 @@ pragma solidity ^0.8.19; import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; -import {BipsLibrary} from "@uniswap/v4-core/src/libraries/BipsLibrary.sol"; import {IV4Router} from "../../src/interfaces/IV4Router.sol"; import {RoutingTestHelpers} from "../shared/RoutingTestHelpers.sol"; import {Plan, Planner} from "../shared/Planner.sol"; import {Actions} from "../../src/libraries/Actions.sol"; import {ActionConstants} from "../../src/libraries/ActionConstants.sol"; +import {BipsLibrary} from "../../src/libraries/BipsLibrary.sol"; contract PaymentsTests is RoutingTestHelpers, GasSnapshot { using CurrencyLibrary for Currency; @@ -22,39 +22,10 @@ contract PaymentsTests is RoutingTestHelpers, GasSnapshot { plan = Planner.init(); } - function test_settleTakePair() public { - uint256 amountIn = 1 ether; - uint256 expectedAmountOut = 992054607780215625; - IV4Router.ExactInputSingleParams memory params = - IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, 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)); - - uint256 inputBalanceBefore = key0.currency0.balanceOfSelf(); - uint256 outputBalanceBefore = key0.currency1.balanceOfSelf(); - // router is empty before - assertEq(currency0.balanceOf(address(router)), 0); - assertEq(currency1.balanceOf(address(router)), 0); - - bytes memory data = plan.encode(); - router.executeActions(data); - - uint256 inputBalanceAfter = key0.currency0.balanceOfSelf(); - uint256 outputBalanceAfter = key0.currency1.balanceOfSelf(); - - // router is empty - assertEq(currency0.balanceOf(address(router)), 0); - assertEq(currency1.balanceOf(address(router)), 0); - // caller's balance changed by input and output amounts - assertEq(inputBalanceBefore - inputBalanceAfter, amountIn); - assertEq(outputBalanceAfter - outputBalanceBefore, expectedAmountOut); - } - 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 +40,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 +58,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 +76,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 +92,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 +106,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,12 +140,13 @@ 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, abi.encode(key0.currency0, amountIn, true)); // take 15 bips to Bob plan = plan.add(Actions.TAKE_PORTION, abi.encode(key0.currency1, bob, 15)); - plan = plan.add(Actions.SETTLE_TAKE_PAIR, abi.encode(key0.currency0, key0.currency1)); + plan = plan.add(Actions.TAKE_ALL, abi.encode(key0.currency1, 0)); uint256 inputBalanceBefore = key0.currency0.balanceOfSelf(); uint256 outputBalanceBefore = key0.currency1.balanceOfSelf(); @@ -205,12 +177,13 @@ 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)); + plan = plan.add(Actions.SETTLE, abi.encode(key0.currency0, amountIn, true)); // bips is larger than maximum bips plan = plan.add(Actions.TAKE_PORTION, abi.encode(key0.currency1, bob, BipsLibrary.BPS_DENOMINATOR + 1)); - plan = plan.add(Actions.SETTLE_TAKE_PAIR, abi.encode(key0.currency0, key0.currency1)); + plan = plan.add(Actions.TAKE_ALL, abi.encode(key0.currency1, 0)); bytes memory data = plan.encode(); diff --git a/test/router/V4Router.gas.t.sol b/test/router/V4Router.gas.t.sol index 8d3eac922..c49f96dcf 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 e32da0529..0c74e0046 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/script/DeployPoolManager.t.sol b/test/script/DeployPoolManager.t.sol index fbb7df439..19159c266 100644 --- a/test/script/DeployPoolManager.t.sol +++ b/test/script/DeployPoolManager.t.sol @@ -15,7 +15,7 @@ contract DeployPoolManagerTest is Test { function test_run_poolManager() public { IPoolManager manager = deployer.run(); // Foundry sets a default sender in scripts. - address defaultSender = 0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38; + address defaultSender = 0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f; // Deployer is the owner. assertEq(_getOwner(manager), defaultSender); } diff --git a/test/script/DeployPoolMofifyLiquidityTest.t.sol b/test/script/DeployPoolMofifyLiquidityTest.t.sol index e4c677108..6da02e012 100644 --- a/test/script/DeployPoolMofifyLiquidityTest.t.sol +++ b/test/script/DeployPoolMofifyLiquidityTest.t.sol @@ -14,7 +14,7 @@ contract DeployPoolModifyLiquidityTestTest is Test { IPoolManager manager; function setUp() public { - manager = new PoolManager(); + manager = new PoolManager(address(this)); deployer = new DeployPoolModifyLiquidityTest(); } diff --git a/test/script/DeployPoolSwapTest.t.sol b/test/script/DeployPoolSwapTest.t.sol index 2feb0aaad..de9ca350b 100644 --- a/test/script/DeployPoolSwapTest.t.sol +++ b/test/script/DeployPoolSwapTest.t.sol @@ -14,7 +14,7 @@ contract DeployPoolSwapTestTest is Test { IPoolManager manager; function setUp() public { - manager = new PoolManager(); + manager = new PoolManager(address(this)); deployer = new DeployPoolSwapTest(); } diff --git a/test/shared/Planner.sol b/test/shared/Planner.sol index 979f12151..0d5ca4fc1 100644 --- a/test/shared/Planner.sol +++ b/test/shared/Planner.sol @@ -86,7 +86,9 @@ library Planner { returns (bytes memory) { if (takeRecipient == ActionConstants.MSG_SENDER) { - plan = plan.add(Actions.SETTLE_TAKE_PAIR, abi.encode(inputCurrency, outputCurrency)); + // blindly settling and taking all, without slippage checks, isnt recommended in prod + plan = plan.add(Actions.SETTLE_ALL, abi.encode(inputCurrency, type(uint256).max)); + plan = plan.add(Actions.TAKE_ALL, abi.encode(outputCurrency, 0)); } else { plan = plan.add(Actions.SETTLE, abi.encode(inputCurrency, ActionConstants.OPEN_DELTA, true)); plan = plan.add(Actions.TAKE, abi.encode(outputCurrency, takeRecipient, ActionConstants.OPEN_DELTA)); diff --git a/test/shared/PositionConfig.sol b/test/shared/PositionConfig.sol index a2ab832bd..1d0a5a5ad 100644 --- a/test/shared/PositionConfig.sol +++ b/test/shared/PositionConfig.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; diff --git a/test/shared/PosmTestSetup.sol b/test/shared/PosmTestSetup.sol index 0a79edd17..bce86b97b 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,26 @@ 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 seedToken(MockERC20 token, address to) internal { + token.mint(to, STARTING_USER_BALANCE); + } + + function initPoolUnsorted(Currency currencyA, Currency currencyB, IHooks hooks, uint24 fee, uint160 sqrtPriceX96) + internal + returns (PoolKey memory poolKey) + { + (Currency _currency0, Currency _currency1) = + SortTokens.sort(MockERC20(Currency.unwrap(currencyA)), MockERC20(Currency.unwrap(currencyB))); + + (poolKey,) = 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);