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 99bfcda62..88081af81 100644 --- a/.forge-snapshots/PositionManager_burn_empty.snap +++ b/.forge-snapshots/PositionManager_burn_empty.snap @@ -1 +1 @@ -50413 \ 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 99bfcda62..88081af81 100644 --- a/.forge-snapshots/PositionManager_burn_empty_native.snap +++ b/.forge-snapshots/PositionManager_burn_empty_native.snap @@ -1 +1 @@ -50413 \ 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 23b4ea0bf..b8ad8f5e7 100644 --- a/.forge-snapshots/PositionManager_burn_nonEmpty_native_withClose.snap +++ b/.forge-snapshots/PositionManager_burn_nonEmpty_native_withClose.snap @@ -1 +1 @@ -125541 \ 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 932be33b5..5f7fe3b44 100644 --- a/.forge-snapshots/PositionManager_burn_nonEmpty_native_withTakePair.snap +++ b/.forge-snapshots/PositionManager_burn_nonEmpty_native_withTakePair.snap @@ -1 +1 @@ -124988 \ 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 4f3b1f22f..f821b49cb 100644 --- a/.forge-snapshots/PositionManager_burn_nonEmpty_withClose.snap +++ b/.forge-snapshots/PositionManager_burn_nonEmpty_withClose.snap @@ -1 +1 @@ -132394 \ 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 af112ed01..46630b5d9 100644 --- a/.forge-snapshots/PositionManager_burn_nonEmpty_withTakePair.snap +++ b/.forge-snapshots/PositionManager_burn_nonEmpty_withTakePair.snap @@ -1 +1 @@ -131841 \ 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 a81bea649..86f965291 100644 --- a/.forge-snapshots/PositionManager_collect_native.snap +++ b/.forge-snapshots/PositionManager_collect_native.snap @@ -1 +1 @@ -146241 \ 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 6e9f06c48..e2b8005b9 100644 --- a/.forge-snapshots/PositionManager_collect_sameRange.snap +++ b/.forge-snapshots/PositionManager_collect_sameRange.snap @@ -1 +1 @@ -154807 \ 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 6e9f06c48..e2b8005b9 100644 --- a/.forge-snapshots/PositionManager_collect_withClose.snap +++ b/.forge-snapshots/PositionManager_collect_withClose.snap @@ -1 +1 @@ -154807 \ 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 292a8b059..ccb34392b 100644 --- a/.forge-snapshots/PositionManager_collect_withTakePair.snap +++ b/.forge-snapshots/PositionManager_collect_withTakePair.snap @@ -1 +1 @@ -154128 \ 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 720602f56..c2b5120a1 100644 --- a/.forge-snapshots/PositionManager_decreaseLiquidity_native.snap +++ b/.forge-snapshots/PositionManager_decreaseLiquidity_native.snap @@ -1 +1 @@ -111938 \ 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 3da01e1a0..bdea68d3c 100644 --- a/.forge-snapshots/PositionManager_decreaseLiquidity_withClose.snap +++ b/.forge-snapshots/PositionManager_decreaseLiquidity_withClose.snap @@ -1 +1 @@ -119688 \ 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 667f6401e..8c20e971e 100644 --- a/.forge-snapshots/PositionManager_decreaseLiquidity_withTakePair.snap +++ b/.forge-snapshots/PositionManager_decreaseLiquidity_withTakePair.snap @@ -1 +1 @@ -119009 \ 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 560c56538..dbd7a9ac1 100644 --- a/.forge-snapshots/PositionManager_decrease_burnEmpty.snap +++ b/.forge-snapshots/PositionManager_decrease_burnEmpty.snap @@ -1 +1 @@ -135191 \ 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 4d3693468..4622f7b48 100644 --- a/.forge-snapshots/PositionManager_decrease_burnEmpty_native.snap +++ b/.forge-snapshots/PositionManager_decrease_burnEmpty_native.snap @@ -1 +1 @@ -128338 \ 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 8ac56af5f..105b30011 100644 --- a/.forge-snapshots/PositionManager_decrease_sameRange_allLiquidity.snap +++ b/.forge-snapshots/PositionManager_decrease_sameRange_allLiquidity.snap @@ -1 +1 @@ -132375 \ 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 3a6b84eae..a34b498cd 100644 --- a/.forge-snapshots/PositionManager_decrease_take_take.snap +++ b/.forge-snapshots/PositionManager_decrease_take_take.snap @@ -1 +1 @@ -120264 \ 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 c052a1440..0c144b4d2 100644 --- a/.forge-snapshots/PositionManager_increaseLiquidity_erc20_withClose.snap +++ b/.forge-snapshots/PositionManager_increaseLiquidity_erc20_withClose.snap @@ -1 +1 @@ -158992 \ 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 5f62225d3..5b88c6213 100644 --- a/.forge-snapshots/PositionManager_increaseLiquidity_erc20_withSettlePair.snap +++ b/.forge-snapshots/PositionManager_increaseLiquidity_erc20_withSettlePair.snap @@ -1 +1 @@ -157932 \ 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 afef57e7a..dd90e5a55 100644 --- a/.forge-snapshots/PositionManager_increaseLiquidity_native.snap +++ b/.forge-snapshots/PositionManager_increaseLiquidity_native.snap @@ -1 +1 @@ -140819 \ 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 5f3bb3049..6312e8ddf 100644 --- a/.forge-snapshots/PositionManager_increase_autocompoundExactUnclaimedFees.snap +++ b/.forge-snapshots/PositionManager_increase_autocompoundExactUnclaimedFees.snap @@ -1 +1 @@ -136318 \ 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 32b4822f4..cf908d5cf 100644 --- a/.forge-snapshots/PositionManager_increase_autocompoundExcessFeesCredit.snap +++ b/.forge-snapshots/PositionManager_increase_autocompoundExcessFeesCredit.snap @@ -1 +1 @@ -177299 \ 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 54aff2196..c9779d027 100644 --- a/.forge-snapshots/PositionManager_increase_autocompound_clearExcess.snap +++ b/.forge-snapshots/PositionManager_increase_autocompound_clearExcess.snap @@ -1 +1 @@ -147975 \ 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 41bb5af10..ad9b48fff 100644 --- a/.forge-snapshots/PositionManager_mint_native.snap +++ b/.forge-snapshots/PositionManager_mint_native.snap @@ -1 +1 @@ -364680 \ 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 f989c5b41..6d4460fb9 100644 --- a/.forge-snapshots/PositionManager_mint_nativeWithSweep_withClose.snap +++ b/.forge-snapshots/PositionManager_mint_nativeWithSweep_withClose.snap @@ -1 +1 @@ -373203 \ 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 a6c6e9f36..351997f8d 100644 --- a/.forge-snapshots/PositionManager_mint_nativeWithSweep_withSettlePair.snap +++ b/.forge-snapshots/PositionManager_mint_nativeWithSweep_withSettlePair.snap @@ -1 +1 @@ -372426 \ 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 490f92901..dcb8d5ae5 100644 --- a/.forge-snapshots/PositionManager_mint_onSameTickLower.snap +++ b/.forge-snapshots/PositionManager_mint_onSameTickLower.snap @@ -1 +1 @@ -317528 \ 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 8a0285485..3a7c7c34a 100644 --- a/.forge-snapshots/PositionManager_mint_onSameTickUpper.snap +++ b/.forge-snapshots/PositionManager_mint_onSameTickUpper.snap @@ -1 +1 @@ -318198 \ 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 6428314a0..7d8a26862 100644 --- a/.forge-snapshots/PositionManager_mint_sameRange.snap +++ b/.forge-snapshots/PositionManager_mint_sameRange.snap @@ -1 +1 @@ -243767 \ 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 7a992c28e..4d62f1da1 100644 --- a/.forge-snapshots/PositionManager_mint_settleWithBalance_sweep.snap +++ b/.forge-snapshots/PositionManager_mint_settleWithBalance_sweep.snap @@ -1 +1 @@ -418947 \ 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 e6cbe01bf..fcd47aaf3 100644 --- a/.forge-snapshots/PositionManager_mint_warmedPool_differentRange.snap +++ b/.forge-snapshots/PositionManager_mint_warmedPool_differentRange.snap @@ -1 +1 @@ -323559 \ 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 8ff9088db..59610f85c 100644 --- a/.forge-snapshots/PositionManager_mint_withClose.snap +++ b/.forge-snapshots/PositionManager_mint_withClose.snap @@ -1 +1 @@ -420081 \ 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 fb3a929ea..b3a6480c2 100644 --- a/.forge-snapshots/PositionManager_mint_withSettlePair.snap +++ b/.forge-snapshots/PositionManager_mint_withSettlePair.snap @@ -1 +1 @@ -419139 \ 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 4e5f499fd..19380f4f4 100644 --- a/.forge-snapshots/PositionManager_multicall_initialize_mint.snap +++ b/.forge-snapshots/PositionManager_multicall_initialize_mint.snap @@ -1 +1 @@ -464241 \ 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 new file mode 100644 index 000000000..60d759c40 --- /dev/null +++ b/.forge-snapshots/positionDescriptor bytecode size.snap @@ -0,0 +1 @@ +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 381668e8d..30dff3ef4 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 18b223cab..b619b6718 160000 --- a/lib/v4-core +++ b/lib/v4-core @@ -1 +1 @@ -Subproject commit 18b223cab19dc778d9d287a82d29fee3e99162b0 +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 3ce242504..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"; @@ -8,18 +8,31 @@ import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {StateView} from "../src/lens/StateView.sol"; 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 {} - function run(address poolManager, address permit2, uint256 unsubscribeGasLimit) - public - returns (PositionManager posm) - { + function run( + address poolManager, + address permit2, + uint256 unsubscribeGasLimit, + address wrappedNative, + string memory nativeCurrencyLabel + ) public returns (PositionDescriptor positionDescriptor, PositionManager posm) { vm.startBroadcast(); + positionDescriptor = new PositionDescriptor(IPoolManager(poolManager), wrappedNative, nativeCurrencyLabel); + console2.log("PositionDescriptor", address(positionDescriptor)); + posm = new PositionManager{salt: hex"03"}( - IPoolManager(poolManager), IAllowanceTransfer(permit2), unsubscribeGasLimit + IPoolManager(poolManager), + IAllowanceTransfer(permit2), + unsubscribeGasLimit, + 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 new file mode 100644 index 000000000..021cbd5e5 --- /dev/null +++ b/src/PositionDescriptor.sol @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; +import {IPositionManager} from "./interfaces/IPositionManager.sol"; +import {IPositionDescriptor} from "./interfaces/IPositionDescriptor.sol"; +import {PositionInfo, PositionInfoLibrary} from "./libraries/PositionInfoLibrary.sol"; +import {Descriptor} from "./libraries/Descriptor.sol"; +import {CurrencyRatioSortOrder} from "./libraries/CurrencyRatioSortOrder.sol"; +import {SafeCurrencyMetadata} from "./libraries/SafeCurrencyMetadata.sol"; + +/// @title Describes NFT token positions +/// @notice Produces a string containing the data URI for a JSON metadata string +contract PositionDescriptor is IPositionDescriptor { + using StateLibrary for IPoolManager; + using PoolIdLibrary for PoolKey; + using CurrencyLibrary for Currency; + using PositionInfoLibrary for PositionInfo; + + error InvalidTokenId(uint256 tokenId); + + // mainnet addresses + address private constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F; + address private constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; + address private constant USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7; + address private constant TBTC = 0x8dAEBADE922dF735c38C80C7eBD708Af50815fAa; + address private constant WBTC = 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599; + + address public immutable wrappedNative; + string public nativeCurrencyLabel; + + IPoolManager public immutable poolManager; + + constructor(IPoolManager _poolManager, address _wrappedNative, string memory _nativeCurrencyLabel) { + poolManager = _poolManager; + wrappedNative = _wrappedNative; + nativeCurrencyLabel = _nativeCurrencyLabel; + } + + /// @inheritdoc IPositionDescriptor + function tokenURI(IPositionManager positionManager, uint256 tokenId) + external + view + override + returns (string memory) + { + (PoolKey memory poolKey, PositionInfo positionInfo) = positionManager.getPoolAndPositionInfo(tokenId); + if (positionInfo.poolId() == 0) { + revert InvalidTokenId(tokenId); + } + (, 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(currency0, currency1); + + // If not flipped, quote currency is currency1, base currency is currency0 + // If flipped, quote currency is currency0, base currency is currency1 + address quoteCurrency = !_flipRatio ? currency1 : currency0; + address baseCurrency = !_flipRatio ? currency0 : currency1; + + return Descriptor.constructTokenURI( + Descriptor.ConstructTokenURIParams({ + tokenId: tokenId, + quoteCurrency: quoteCurrency, + baseCurrency: baseCurrency, + quoteCurrencySymbol: SafeCurrencyMetadata.currencySymbol(quoteCurrency, nativeCurrencyLabel), + baseCurrencySymbol: SafeCurrencyMetadata.currencySymbol(baseCurrency, nativeCurrencyLabel), + quoteCurrencyDecimals: SafeCurrencyMetadata.currencyDecimals(quoteCurrency), + baseCurrencyDecimals: SafeCurrencyMetadata.currencyDecimals(baseCurrency), + flipRatio: _flipRatio, + tickLower: positionInfo.tickLower(), + tickUpper: positionInfo.tickUpper(), + tickCurrent: tick, + tickSpacing: poolKey.tickSpacing, + fee: poolKey.fee, + poolManager: address(poolManager), + hooks: address(poolKey.hooks) + }) + ); + } + + /// @notice Returns true if currency0 has higher priority than currency1 + /// @param currency0 The first currency address + /// @param currency1 The second currency address + /// @return flipRatio True if currency0 has higher priority than currency1 + function flipRatio(address currency0, address currency1) public view returns (bool) { + return currencyRatioPriority(currency0) > currencyRatioPriority(currency1); + } + + /// @notice Returns the priority of a currency. + /// For certain currencies on mainnet, the smaller the currency, the higher the priority + /// @param currency The currency address + /// @return priority The priority of the currency + function currencyRatioPriority(address currency) public view returns (int256) { + // Currencies in order of priority on mainnet: USDC, USDT, DAI, (ETH, WETH), TBTC, WBTC + // wrapped native is different address on different chains. passed in constructor + + // native currency + if (currency == address(0) || currency == wrappedNative) { + return CurrencyRatioSortOrder.DENOMINATOR; + } + if (block.chainid == 1) { + if (currency == USDC) { + return CurrencyRatioSortOrder.NUMERATOR_MOST; + } else if (currency == USDT) { + return CurrencyRatioSortOrder.NUMERATOR_MORE; + } else if (currency == DAI) { + return CurrencyRatioSortOrder.NUMERATOR; + } else if (currency == TBTC) { + return CurrencyRatioSortOrder.DENOMINATOR_MORE; + } else if (currency == WBTC) { + return CurrencyRatioSortOrder.DENOMINATOR_MOST; + } + } + return 0; + } +} diff --git a/src/PositionManager.sol b/src/PositionManager.sol index 52c019232..f9dbd2e38 100644 --- a/src/PositionManager.sol +++ b/src/PositionManager.sol @@ -1,17 +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 {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"; @@ -25,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 @@ -101,7 +106,8 @@ contract PositionManager is ReentrancyLock, BaseActionsRouter, Notifier, - Permit2Forwarder + Permit2Forwarder, + NativeWrapper { using PoolIdLibrary for PoolKey; using StateLibrary for IPoolManager; @@ -116,15 +122,26 @@ contract PositionManager is /// @dev The ID of the next token that will be minted. Skips 0 uint256 public nextTokenId = 1; + IPositionDescriptor public immutable tokenDescriptor; + mapping(uint256 tokenId => PositionInfo info) public positionInfo; mapping(bytes25 poolId => PoolKey poolKey) public poolKeys; - constructor(IPoolManager _poolManager, IAllowanceTransfer _permit2, uint256 _unsubscribeGasLimit) + constructor( + IPoolManager _poolManager, + IAllowanceTransfer _permit2, + uint256 _unsubscribeGasLimit, + IPositionDescriptor _tokenDescriptor, + IWETH9 _weth9 + ) BaseActionsRouter(_poolManager) Permit2Forwarder(_permit2) - ERC721Permit_v4("Uniswap V4 Positions NFT", "UNI-V4-POSM") + ERC721Permit_v4("Uniswap v4 Positions NFT", "UNI-V4-POSM") Notifier(_unsubscribeGasLimit) - {} + NativeWrapper(_weth9) + { + tokenDescriptor = _tokenDescriptor; + } /// @notice Reverts if the deadline has passed /// @param deadline The timestamp at which the call is no longer valid, passed in by the caller @@ -143,6 +160,16 @@ 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); + } + /// @inheritdoc IPositionManager function modifyLiquidities(bytes calldata unlockData, uint256 deadline) external @@ -174,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(); @@ -192,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) = @@ -228,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); @@ -250,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, @@ -302,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 @@ -311,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 { @@ -349,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) { @@ -373,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, @@ -417,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 b33d8d904..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,9 +15,10 @@ 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 +/// @notice Abstract contract that contains all internal logic needed for routing through Uniswap v4 pools /// @dev the entry point to executing actions in this contract is calling `BaseActionsRouter._executeActions` /// An inheriting contract should call _executeActions at the point that they wish actions to be executed abstract contract V4Router is IV4Router, BaseActionsRouter, DeltaResolver { @@ -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 b700db89d..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"; @@ -95,9 +95,4 @@ abstract contract ERC721Permit_v4 is ERC721, IERC721Permit_v4, EIP712_v4, Unorde return spender == ownerOf(tokenId) || getApproved[tokenId] == spender || isApprovedForAll[ownerOf(tokenId)][spender]; } - - // TODO: to be implemented after audits - function tokenURI(uint256) public pure override returns (string memory) { - return "https://example.com"; - } } 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 1f42ab1ff..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,14 +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 - /// @param hookData the optional data passed to the hook's initialize functions - function initializePool(PoolKey calldata key, uint160 sqrtPriceX96, bytes calldata hookData) - external - payable - returns (int24) - { - return poolManager.initialize(key, sqrtPriceX96, hookData); + /// @return tick The current tick of the pool + function initializePool(PoolKey calldata key, uint160 sqrtPriceX96) external payable returns (int24) { + 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 ca2eb9858..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"; @@ -56,16 +56,12 @@ abstract contract BaseHook is IHooks, SafeCallback { } /// @inheritdoc IHooks - function beforeInitialize(address, PoolKey calldata, uint160, bytes calldata) external virtual returns (bytes4) { + function beforeInitialize(address, PoolKey calldata, uint160) external virtual returns (bytes4) { revert HookNotImplemented(); } /// @inheritdoc IHooks - function afterInitialize(address, PoolKey calldata, uint160, int24, bytes calldata) - external - virtual - returns (bytes4) - { + function afterInitialize(address, PoolKey calldata, uint160, int24) external virtual returns (bytes4) { revert HookNotImplemented(); } 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 new file mode 100644 index 000000000..a1736ceab --- /dev/null +++ b/src/interfaces/IPositionDescriptor.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "./IPositionManager.sol"; + +/// @title Describes position NFT tokens via URI +interface IPositionDescriptor { + /// @notice Produces the URI describing a particular token ID + /// @dev Note this URI may be a data: URI with the JSON contents directly inlined + /// @param positionManager The position manager for which to describe the token + /// @param tokenId The ID of the token for which to produce a description, which may not be valid + /// @return The URI of the ERC721-compliant metadata + function tokenURI(IPositionManager positionManager, uint256 tokenId) external view returns (string memory); +} 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 new file mode 100644 index 000000000..3add136f8 --- /dev/null +++ b/src/libraries/AddressStringUtil.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/// @title AddressStringUtil +/// @notice provides utility functions for converting addresses to strings +/// @dev Reference: https://github.com/Uniswap/solidity-lib/blob/master/contracts/libraries/AddressStringUtil.sol +library AddressStringUtil { + error InvalidAddressLength(uint256 len); + + /// @notice Converts an address to the uppercase hex string, extracting only len bytes (up to 20, multiple of 2) + /// @param addr the address to convert + /// @param len the number of bytes to extract + /// @return the hex string + function toAsciiString(address addr, uint256 len) internal pure returns (string memory) { + if (!(len % 2 == 0 && len > 0 && len <= 40)) { + revert InvalidAddressLength(len); + } + + bytes memory s = new bytes(len); + uint256 addrNum = uint256(uint160(addr)); + for (uint256 i = 0; i < len / 2; i++) { + // shift right and truncate all but the least significant byte to extract the byte at position 19-i + uint8 b = uint8(addrNum >> (8 * (19 - i))); + // first hex character is the most significant 4 bits + uint8 hi = b >> 4; + // second hex character is the least significant 4 bits + uint8 lo = b - (hi << 4); + s[2 * i] = char(hi); + s[2 * i + 1] = char(lo); + } + return string(s); + } + + /// @notice Converts a value into is corresponding ASCII character for the hex representation + // hi and lo are only 4 bits and between 0 and 16 + // uses upper case for the characters + /// @param b the value to convert + /// @return c the ASCII character + function char(uint8 b) private pure returns (bytes1 c) { + if (b < 10) { + return bytes1(b + 0x30); + } else { + return bytes1(b + 0x37); + } + } +} 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 new file mode 100644 index 000000000..c02d1084f --- /dev/null +++ b/src/libraries/CurrencyRatioSortOrder.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/// @title CurrencyRatioSortOrder +/// @notice Provides constants for sorting currencies when displaying price ratios +/// Currencies given larger values will be in the numerator of the price ratio +/// @dev Reference: https://github.com/Uniswap/v3-periphery/blob/main/contracts/libraries/TokenRatioSortOrder.sol +library CurrencyRatioSortOrder { + int256 constant NUMERATOR_MOST = 300; + int256 constant NUMERATOR_MORE = 200; + int256 constant NUMERATOR = 100; + + int256 constant DENOMINATOR_MOST = -300; + int256 constant DENOMINATOR_MORE = -200; + int256 constant DENOMINATOR = -100; +} diff --git a/src/libraries/Descriptor.sol b/src/libraries/Descriptor.sol new file mode 100644 index 000000000..327937c8d --- /dev/null +++ b/src/libraries/Descriptor.sol @@ -0,0 +1,528 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +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"; +import {Strings} from "openzeppelin-contracts/contracts/utils/Strings.sol"; +import {Base64} from "openzeppelin-contracts/contracts/utils/Base64.sol"; +import {SVG} from "./SVG.sol"; +import {HexStrings} from "./HexStrings.sol"; + +/// @title Descriptor +/// @notice Describes NFT token positions +/// @dev Reference: https://github.com/Uniswap/v3-periphery/blob/main/contracts/libraries/NFTDescriptor.sol +library Descriptor { + using TickMath for int24; + using Strings for uint256; + using HexStrings for uint256; + using LPFeeLibrary for uint24; + + uint256 constant sqrt10X128 = 1076067327063303206878105757264492625226; + + struct ConstructTokenURIParams { + uint256 tokenId; + address quoteCurrency; + address baseCurrency; + string quoteCurrencySymbol; + string baseCurrencySymbol; + uint8 quoteCurrencyDecimals; + uint8 baseCurrencyDecimals; + bool flipRatio; + int24 tickLower; + int24 tickUpper; + int24 tickCurrent; + int24 tickSpacing; + uint24 fee; + address poolManager; + address hooks; + } + + /// @notice Constructs the token URI for a Uniswap v4 NFT + /// @param params Parameters needed to construct the token URI + /// @return The token URI as a string + function constructTokenURI(ConstructTokenURIParams memory params) internal pure returns (string memory) { + string memory name = generateName(params, feeToPercentString(params.fee)); + string memory descriptionPartOne = generateDescriptionPartOne( + escapeSpecialCharacters(params.quoteCurrencySymbol), + escapeSpecialCharacters(params.baseCurrencySymbol), + addressToString(params.poolManager) + ); + string memory descriptionPartTwo = generateDescriptionPartTwo( + params.tokenId.toString(), + 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))); + + return string( + abi.encodePacked( + "data:application/json;base64,", + Base64.encode( + bytes( + abi.encodePacked( + '{"name":"', + name, + '", "description":"', + descriptionPartOne, + descriptionPartTwo, + '", "image": "', + "data:image/svg+xml;base64,", + image, + '"}' + ) + ) + ) + ) + ); + } + + /// @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 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 (isSpecialCharacter(symbolBytes[i])) { + specialCharCount++; + } + } + 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, form feeds, new lines, carriage returns, or tabs + if (isSpecialCharacter(symbolBytes[i])) { + escapedBytes[index++] = "\\"; + } + // copy each byte from original string to the new array + escapedBytes[index++] = symbolBytes[i]; + } + return string(escapedBytes); + } + return symbol; + } + + /// @notice Generates the first part of the description for a Uniswap v4 NFT + /// @param quoteCurrencySymbol The symbol of the quote currency + /// @param baseCurrencySymbol The symbol of the base currency + /// @param poolManager The address of the pool manager + /// @return The first part of the description + function generateDescriptionPartOne( + string memory quoteCurrencySymbol, + string memory baseCurrencySymbol, + string memory poolManager + ) private pure returns (string memory) { + // displays quote currency first, then base currency + return string( + abi.encodePacked( + "This NFT represents a liquidity position in a Uniswap v4 ", + quoteCurrencySymbol, + "-", + baseCurrencySymbol, + " pool. ", + "The owner of this NFT can modify or redeem the position.\\n", + "\\nPool Manager Address: ", + poolManager, + "\\n", + quoteCurrencySymbol + ) + ); + } + + /// @notice Generates the second part of the description for a Uniswap v4 NFTs + /// @param tokenId The token ID + /// @param baseCurrencySymbol The symbol of the base currency + /// @param quoteCurrency The address of the quote currency + /// @param baseCurrency The address of the base currency + /// @param hooks The address of the hooks contract + /// @param feeTier The fee tier of the pool + /// @return The second part of the description + function generateDescriptionPartTwo( + string memory tokenId, + string memory baseCurrencySymbol, + string memory quoteCurrency, + string memory baseCurrency, + string memory hooks, + string memory feeTier + ) private pure returns (string memory) { + return string( + abi.encodePacked( + " Address: ", + quoteCurrency, + "\\n", + baseCurrencySymbol, + " Address: ", + baseCurrency, + "\\nHook Address: ", + hooks, + "\\nFee Tier: ", + feeTier, + "\\nToken ID: ", + tokenId, + "\\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." + ) + ); + } + + /// @notice Generates the name for a Uniswap v4 NFT + /// @param params Parameters needed to generate the name + /// @param feeTier The fee tier of the pool + /// @return The name of the NFT + function generateName(ConstructTokenURIParams memory params, string memory feeTier) + private + pure + returns (string memory) + { + // image shows in terms of price, ie quoteCurrency/baseCurrency + return string( + abi.encodePacked( + "Uniswap - ", + feeTier, + " - ", + escapeSpecialCharacters(params.quoteCurrencySymbol), + "/", + escapeSpecialCharacters(params.baseCurrencySymbol), + " - ", + tickToDecimalString( + !params.flipRatio ? params.tickLower : params.tickUpper, + params.tickSpacing, + params.baseCurrencyDecimals, + params.quoteCurrencyDecimals, + params.flipRatio + ), + "<>", + tickToDecimalString( + !params.flipRatio ? params.tickUpper : params.tickLower, + params.tickSpacing, + params.baseCurrencyDecimals, + params.quoteCurrencyDecimals, + params.flipRatio + ) + ) + ); + } + + struct DecimalStringParams { + // significant figures of decimal + uint256 sigfigs; + // length of decimal string + uint8 bufferLength; + // ending index for significant figures (funtion works backwards when copying sigfigs) + uint8 sigfigIndex; + // index of decimal place (0 if no decimal) + uint8 decimalIndex; + // start index for trailing/leading 0's for very small/large numbers + uint8 zerosStartIndex; + // end index for trailing/leading 0's for very small/large numbers + uint8 zerosEndIndex; + // true if decimal number is less than one + bool isLessThanOne; + // true if string should include "%" + bool isPercent; + } + + function generateDecimalString(DecimalStringParams memory params) private pure returns (string memory) { + bytes memory buffer = new bytes(params.bufferLength); + if (params.isPercent) { + buffer[buffer.length - 1] = "%"; + } + if (params.isLessThanOne) { + buffer[0] = "0"; + buffer[1] = "."; + } + + // add leading/trailing 0's + for (uint256 zerosCursor = params.zerosStartIndex; zerosCursor < params.zerosEndIndex + 1; zerosCursor++) { + // converts the ASCII code for 0 (which is 48) into a bytes1 to store in the buffer + buffer[zerosCursor] = bytes1(uint8(48)); + } + // add sigfigs + while (params.sigfigs > 0) { + if (params.decimalIndex > 0 && params.sigfigIndex == params.decimalIndex) { + buffer[params.sigfigIndex--] = "."; + } + buffer[params.sigfigIndex] = bytes1(uint8(48 + (params.sigfigs % 10))); + // can overflow when sigfigIndex = 0 + unchecked { + params.sigfigIndex--; + } + params.sigfigs /= 10; + } + return string(buffer); + } + + /// @notice Gets the price (quote/base) at a specific tick in decimal form + /// MIN or MAX are returned if tick is at the bottom or top of the price curve + /// @param tick The tick (either tickLower or tickUpper) + /// @param tickSpacing The tick spacing of the pool + /// @param baseCurrencyDecimals The decimals of the base currency + /// @param quoteCurrencyDecimals The decimals of the quote currency + /// @param flipRatio True if the ratio was flipped + /// @return The ratio value as a string + function tickToDecimalString( + int24 tick, + int24 tickSpacing, + uint8 baseCurrencyDecimals, + uint8 quoteCurrencyDecimals, + bool flipRatio + ) internal pure returns (string memory) { + if (tick == (TickMath.MIN_TICK / tickSpacing) * tickSpacing) { + return !flipRatio ? "MIN" : "MAX"; + } else if (tick == (TickMath.MAX_TICK / tickSpacing) * tickSpacing) { + return !flipRatio ? "MAX" : "MIN"; + } else { + uint160 sqrtRatioX96 = TickMath.getSqrtPriceAtTick(tick); + if (flipRatio) { + sqrtRatioX96 = uint160(uint256(1 << 192) / sqrtRatioX96); + } + return fixedPointToDecimalString(sqrtRatioX96, baseCurrencyDecimals, quoteCurrencyDecimals); + } + } + + function sigfigsRounded(uint256 value, uint8 digits) private pure returns (uint256, bool) { + bool extraDigit; + if (digits > 5) { + value = value / (10 ** (digits - 5)); + } + bool roundUp = value % 10 > 4; + value = value / 10; + if (roundUp) { + value = value + 1; + } + // 99999 -> 100000 gives an extra sigfig + if (value == 100000) { + value /= 10; + extraDigit = true; + } + return (value, extraDigit); + } + + /// @notice Adjusts the sqrt price for different currencies with different decimals + /// @param sqrtRatioX96 The sqrt price at a specific tick + /// @param baseCurrencyDecimals The decimals of the base currency + /// @param quoteCurrencyDecimals The decimals of the quote currency + /// @return adjustedSqrtRatioX96 The adjusted sqrt price + function adjustForDecimalPrecision(uint160 sqrtRatioX96, uint8 baseCurrencyDecimals, uint8 quoteCurrencyDecimals) + private + pure + returns (uint256 adjustedSqrtRatioX96) + { + uint256 difference = abs(int256(uint256(baseCurrencyDecimals)) - (int256(uint256(quoteCurrencyDecimals)))); + if (difference > 0 && difference <= 18) { + if (baseCurrencyDecimals > quoteCurrencyDecimals) { + adjustedSqrtRatioX96 = sqrtRatioX96 * (10 ** (difference / 2)); + if (difference % 2 == 1) { + adjustedSqrtRatioX96 = FullMath.mulDiv(adjustedSqrtRatioX96, sqrt10X128, 1 << 128); + } + } else { + adjustedSqrtRatioX96 = sqrtRatioX96 / (10 ** (difference / 2)); + if (difference % 2 == 1) { + adjustedSqrtRatioX96 = FullMath.mulDiv(adjustedSqrtRatioX96, 1 << 128, sqrt10X128); + } + } + } else { + adjustedSqrtRatioX96 = uint256(sqrtRatioX96); + } + } + + /// @notice Absolute value of a signed integer + /// @param x The signed integer + /// @return The absolute value of x + function abs(int256 x) private pure returns (uint256) { + return uint256(x >= 0 ? x : -x); + } + + function fixedPointToDecimalString(uint160 sqrtRatioX96, uint8 baseCurrencyDecimals, uint8 quoteCurrencyDecimals) + internal + pure + returns (string memory) + { + uint256 adjustedSqrtRatioX96 = + adjustForDecimalPrecision(sqrtRatioX96, baseCurrencyDecimals, quoteCurrencyDecimals); + uint256 value = FullMath.mulDiv(adjustedSqrtRatioX96, adjustedSqrtRatioX96, 1 << 64); + + bool priceBelow1 = adjustedSqrtRatioX96 < 2 ** 96; + if (priceBelow1) { + // 10 ** 43 is precision needed to retreive 5 sigfigs of smallest possible price + 1 for rounding + value = FullMath.mulDiv(value, 10 ** 44, 1 << 128); + } else { + // leave precision for 4 decimal places + 1 place for rounding + value = FullMath.mulDiv(value, 10 ** 5, 1 << 128); + } + + // get digit count + uint256 temp = value; + uint8 digits; + while (temp != 0) { + digits++; + temp /= 10; + } + // don't count extra digit kept for rounding + digits = digits - 1; + + // address rounding + (uint256 sigfigs, bool extraDigit) = sigfigsRounded(value, digits); + if (extraDigit) { + digits++; + } + + DecimalStringParams memory params; + if (priceBelow1) { + // 7 bytes ( "0." and 5 sigfigs) + leading 0's bytes + params.bufferLength = uint8(uint8(7) + (uint8(43) - digits)); + params.zerosStartIndex = 2; + params.zerosEndIndex = uint8(uint256(43) - digits + 1); + params.sigfigIndex = uint8(params.bufferLength - 1); + } else if (digits >= 9) { + // no decimal in price string + params.bufferLength = uint8(digits - 4); + params.zerosStartIndex = 5; + params.zerosEndIndex = uint8(params.bufferLength - 1); + params.sigfigIndex = 4; + } else { + // 5 sigfigs surround decimal + params.bufferLength = 6; + params.sigfigIndex = 5; + params.decimalIndex = uint8(digits - 5 + 1); + } + params.sigfigs = sigfigs; + params.isLessThanOne = priceBelow1; + params.isPercent = false; + + return generateDecimalString(params); + } + + /// @notice Converts fee amount in pips to decimal string with percent sign + /// @param fee fee amount + /// @return fee as a decimal string with percent sign + function feeToPercentString(uint24 fee) internal pure returns (string memory) { + if (fee.isDynamicFee()) { + return "Dynamic"; + } + if (fee == 0) { + return "0%"; + } + uint24 temp = fee; + uint256 digits; + uint8 numSigfigs; + // iterates over each digit of fee by dividing temp by 10 in each iteration until temp becomes 0 + // calculates number of digits and number of significant figures (non-zero digits) + while (temp != 0) { + if (numSigfigs > 0) { + // count all digits preceding least significant figure + numSigfigs++; + } else if (temp % 10 != 0) { + numSigfigs++; + } + digits++; + temp /= 10; + } + + DecimalStringParams memory params; + uint256 nZeros; + if (digits >= 5) { + // represents fee greater than or equal to 1% + // if decimal > 1 (5th digit is the ones place) + uint256 decimalPlace = digits - numSigfigs >= 4 ? 0 : 1; + nZeros = digits - 5 < numSigfigs - 1 ? 0 : digits - 5 - (numSigfigs - 1); + params.zerosStartIndex = numSigfigs; + params.zerosEndIndex = uint8(params.zerosStartIndex + nZeros - 1); + params.sigfigIndex = uint8(params.zerosStartIndex - 1 + decimalPlace); + params.bufferLength = uint8(nZeros + numSigfigs + 1 + decimalPlace); + } else { + // represents fee less than 1% + // else if decimal < 1 + nZeros = 5 - digits; // number of zeros, inlcuding the zero before decimal + params.zerosStartIndex = 2; // leading zeros will start after the decimal point + params.zerosEndIndex = uint8(nZeros + params.zerosStartIndex - 1); // end index for leading zeros + params.bufferLength = uint8(nZeros + numSigfigs + 2); // total length of string buffer, including "0." and "%" + params.sigfigIndex = uint8(params.bufferLength - 2); // index of starting signficant figure + params.isLessThanOne = true; + } + params.sigfigs = uint256(fee) / (10 ** (digits - numSigfigs)); // the signficant figures of the fee + params.isPercent = true; + params.decimalIndex = digits > 4 ? uint8(digits - 4) : 0; // based on total number of digits in the fee + + return generateDecimalString(params); + } + + function addressToString(address addr) internal pure returns (string memory) { + return (uint256(uint160(addr))).toHexString(20); + } + + /// @notice Generates the SVG image for a Uniswap v4 NFT + /// @param params Parameters needed to generate the SVG image + /// @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(params.quoteCurrency), + baseCurrency: addressToString(params.baseCurrency), + hooks: params.hooks, + quoteCurrencySymbol: params.quoteCurrencySymbol, + baseCurrencySymbol: params.baseCurrencySymbol, + feeTier: feeToPercentString(params.fee), + tickLower: params.tickLower, + tickUpper: params.tickUpper, + tickSpacing: params.tickSpacing, + overRange: overRange(params.tickLower, params.tickUpper, params.tickCurrent), + tokenId: params.tokenId, + 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); + } + + /// @notice Checks if the current price is within your position range, above, or below + /// @param tickLower The lower tick + /// @param tickUpper The upper tick + /// @param tickCurrent The current tick + /// @return 0 if the current price is within the position range, -1 if below, 1 if above + function overRange(int24 tickLower, int24 tickUpper, int24 tickCurrent) private pure returns (int8) { + if (tickCurrent < tickLower) { + return -1; + } else if (tickCurrent > tickUpper) { + return 1; + } else { + return 0; + } + } + + 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 + returns (string memory) + { + return ((n - inMn) * (outMx - outMn) / (inMx - inMn) + outMn).toString(); + } + + function currencyToColorHex(uint256 currency, uint256 offset) internal pure returns (string memory str) { + return string((currency >> offset).toHexStringNoPrefix(3)); + } + + function getCircleCoord(uint256 currency, uint256 offset, uint256 tokenId) internal pure returns (uint256) { + return (sliceCurrencyHex(currency, offset) * tokenId) % 255; + } + + function sliceCurrencyHex(uint256 currency, uint256 offset) internal pure returns (uint256) { + return uint256(uint8(currency >> offset)); + } +} 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 new file mode 100644 index 000000000..714a9c8ef --- /dev/null +++ b/src/libraries/HexStrings.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/// @title HexStrings +/// @notice Provides function for converting numbers to hexadecimal strings +/// @dev Reference: https://github.com/Uniswap/v3-periphery/blob/main/contracts/libraries/HexStrings.sol +library HexStrings { + bytes16 internal constant ALPHABET = "0123456789abcdef"; + + /// @notice Convert a number to a hex string without the '0x' prefix with a fixed length + /// @param value The number to convert + /// @param length The length of the output string, starting from the last character of the string + /// @return The hex string + function toHexStringNoPrefix(uint256 value, uint256 length) internal pure returns (string memory) { + bytes memory buffer = new bytes(2 * length); + for (uint256 i = buffer.length; i > 0; i--) { + buffer[i - 1] = ALPHABET[value & 0xf]; + value >>= 4; + } + return string(buffer); + } +} 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 new file mode 100644 index 000000000..03c4236cb --- /dev/null +++ b/src/libraries/SVG.sol @@ -0,0 +1,471 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +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"; +import {Base64} from "openzeppelin-contracts/contracts/utils/Base64.sol"; + +/// @title SVG +/// @notice Provides a function for generating an SVG associated with a Uniswap NFT +/// @dev Reference: https://github.com/Uniswap/v3-periphery/blob/main/contracts/libraries/NFTSVG.sol +library SVG { + using Strings for uint256; + + // SVG path commands for the curve that represent the steepness of the position + // defined using the Cubic Bezier Curve syntax + // curve1 is the smallest (linear) curve, curve8 is the largest curve + string constant curve1 = "M1 1C41 41 105 105 145 145"; + string constant curve2 = "M1 1C33 49 97 113 145 145"; + string constant curve3 = "M1 1C33 57 89 113 145 145"; + string constant curve4 = "M1 1C25 65 81 121 145 145"; + string constant curve5 = "M1 1C17 73 73 129 145 145"; + string constant curve6 = "M1 1C9 81 65 137 145 145"; + string constant curve7 = "M1 1C1 89 57.5 145 145 145"; + string constant curve8 = "M1 1C1 97 49 145 145 145"; + + struct SVGParams { + string quoteCurrency; + string baseCurrency; + address hooks; + string quoteCurrencySymbol; + string baseCurrencySymbol; + string feeTier; + int24 tickLower; + int24 tickUpper; + int24 tickSpacing; + int8 overRange; + uint256 tokenId; + string color0; + string color1; + string color2; + string color3; + string x1; + string y1; + string x2; + string y2; + string x3; + string y3; + } + + /// @notice Generate the SVG associated with a Uniswap v4 NFT + /// @param params The SVGParams struct containing the parameters for the SVG + /// @return svg The SVG string associated with the NFT + function generateSVG(SVGParams memory params) internal pure returns (string memory svg) { + return string( + abi.encodePacked( + generateSVGDefs(params), + generateSVGBorderText( + params.quoteCurrency, params.baseCurrency, params.quoteCurrencySymbol, params.baseCurrencySymbol + ), + generateSVGCardMantle(params.quoteCurrencySymbol, params.baseCurrencySymbol, params.feeTier), + generageSvgCurve(params.tickLower, params.tickUpper, params.tickSpacing, params.overRange), + generateSVGPositionDataAndLocationCurve( + params.tokenId.toString(), params.hooks, params.tickLower, params.tickUpper + ), + generateSVGRareSparkle(params.tokenId, params.hooks), + "" + ) + ); + } + + /// @notice Generate the SVG defs that create the color scheme for the SVG + /// @param params The SVGParams struct containing the parameters to generate the SVG defs + /// @return svg The SVG defs string + function generateSVGDefs(SVGParams memory params) private pure returns (string memory svg) { + svg = string( + abi.encodePacked( + '", + "", + '" + ) + ) + ), + '"/>" + ) + ) + ), + '"/>" + ) + ) + ), + '" />', + '" + ) + ) + ), + '" /> ', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + ' ', + '', + '', + '' + ) + ); + } + + /// @notice Generate the SVG for the moving border text displaying the quote and base currency addresses with their symbols + /// @param quoteCurrency The quote currency + /// @param baseCurrency The base currency + /// @param quoteCurrencySymbol The quote currency symbol + /// @param baseCurrencySymbol The base currency symbol + /// @return svg The SVG for the border NFT's border text + function generateSVGBorderText( + string memory quoteCurrency, + string memory baseCurrency, + string memory quoteCurrencySymbol, + string memory baseCurrencySymbol + ) private pure returns (string memory svg) { + svg = string( + abi.encodePacked( + '', + '', + baseCurrency, + unicode" • ", + baseCurrencySymbol, + ' ', + ' ', + baseCurrency, + unicode" • ", + baseCurrencySymbol, + ' ', + '', + quoteCurrency, + unicode" • ", + quoteCurrencySymbol, + ' ', + quoteCurrency, + unicode" • ", + quoteCurrencySymbol, + ' ' + ) + ); + } + + /// @notice Generate the SVG for the card mantle displaying the quote and base currency symbols and fee tier + /// @param quoteCurrencySymbol The quote currency symbol + /// @param baseCurrencySymbol The base currency symbol + /// @param feeTier The fee tier + /// @return svg The SVG for the card mantle + function generateSVGCardMantle( + string memory quoteCurrencySymbol, + string memory baseCurrencySymbol, + string memory feeTier + ) private pure returns (string memory svg) { + svg = string( + abi.encodePacked( + ' ', + quoteCurrencySymbol, + "/", + baseCurrencySymbol, + '', + feeTier, + "", + '' + ) + ); + } + + /// @notice Generate the SVG for the curve that represents the position. Fade up (top is faded) if current price is above your position range, fade down (bottom is faded) if current price is below your position range + /// Circles are generated at the ends of the curve if the position is in range, or at one end of the curve it is on if not in range + /// @param tickLower The lower tick + /// @param tickUpper The upper tick + /// @param tickSpacing The tick spacing + /// @param overRange Whether the current tick is in range, over range, or under range + /// @return svg The SVG for the curve + function generageSvgCurve(int24 tickLower, int24 tickUpper, int24 tickSpacing, int8 overRange) + private + pure + returns (string memory svg) + { + string memory fade = overRange == 1 ? "#fade-up" : overRange == -1 ? "#fade-down" : "#none"; + string memory curve = getCurve(tickLower, tickUpper, tickSpacing); + svg = string( + abi.encodePacked( + '' + '' '', + '', + '', + '', + generateSVGCurveCircle(overRange) + ) + ); + } + + /// @notice Get the curve based on the tick range + /// The smaller the tick range, the smaller/more linear the curve + /// @param tickLower The lower tick + /// @param tickUpper The upper tick + /// @param tickSpacing The tick spacing + /// @return curve The curve path + function getCurve(int24 tickLower, int24 tickUpper, int24 tickSpacing) + internal + pure + returns (string memory curve) + { + int24 tickRange = (tickUpper - tickLower) / tickSpacing; + if (tickRange <= 4) { + curve = curve1; + } else if (tickRange <= 8) { + curve = curve2; + } else if (tickRange <= 16) { + curve = curve3; + } else if (tickRange <= 32) { + curve = curve4; + } else if (tickRange <= 64) { + curve = curve5; + } else if (tickRange <= 128) { + curve = curve6; + } else if (tickRange <= 256) { + curve = curve7; + } else { + curve = curve8; + } + } + + /// @notice Generate the SVG for the circles on the curve + /// @param overRange 0 if the current tick is in range, 1 if the current tick is over range, -1 if the current tick is under range + /// @return svg The SVG for the circles + function generateSVGCurveCircle(int8 overRange) internal pure returns (string memory svg) { + string memory curvex1 = "73"; + string memory curvey1 = "190"; + string memory curvex2 = "217"; + string memory curvey2 = "334"; + /// If the position is over or under range, generate one circle at the end of the curve on the side of the range it is on with a larger circle around it + if (overRange == 1 || overRange == -1) { + svg = string( + abi.encodePacked( + '' + ) + ); + } else { + /// If the position is in range, generate two circles at the ends of the curve + svg = string( + abi.encodePacked( + '', + '' + ) + ); + } + } + + /// @notice Generate the SVG for the position data (token ID, hooks address, min tick, max tick) and the location curve (where your position falls on the curve) + /// @param tokenId The token ID + /// @param hook The hooks address + /// @param tickLower The lower tick + /// @param tickUpper The upper tick + /// @return svg The SVG for the position data and location curve + function generateSVGPositionDataAndLocationCurve( + string memory tokenId, + address hook, + int24 tickLower, + int24 tickUpper + ) private pure returns (string memory svg) { + string memory hookStr = (uint256(uint160(hook))).toHexString(20); + string memory tickLowerStr = tickToString(tickLower); + string memory tickUpperStr = tickToString(tickUpper); + uint256 str1length = bytes(tokenId).length + 4; + 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; + (string memory xCoord, string memory yCoord) = rangeLocation(tickLower, tickUpper); + svg = string( + abi.encodePacked( + ' ', + '', + 'ID: ', + tokenId, + "", + ' ', + '', + 'Hook: ', + hookSlice, + "", + ' ', + '', + 'Min Tick: ', + tickLowerStr, + "", + ' ', + '', + 'Max Tick: ', + tickUpperStr, + "" '', + '', + '', + '' + ) + ); + } + + function substring(string memory str, uint256 startIndex, uint256 endIndex) internal pure returns (string memory) { + bytes memory strBytes = bytes(str); + bytes memory result = new bytes(endIndex - startIndex); + for (uint256 i = startIndex; i < endIndex; i++) { + result[i - startIndex] = strBytes[i]; + } + return string(result); + } + + function tickToString(int24 tick) private pure returns (string memory) { + string memory sign = ""; + if (tick < 0) { + tick = tick * -1; + sign = "-"; + } + return string(abi.encodePacked(sign, uint256(uint24(tick)).toString())); + } + + /// @notice Get the location of where your position falls on the curve + /// @param tickLower The lower tick + /// @param tickUpper The upper tick + /// @return The x and y coordinates of the location of the liquidity + function rangeLocation(int24 tickLower, int24 tickUpper) internal pure returns (string memory, string memory) { + int24 midPoint = (tickLower + tickUpper) / 2; + if (midPoint < -125_000) { + return ("8", "7"); + } else if (midPoint < -75_000) { + return ("8", "10.5"); + } else if (midPoint < -25_000) { + return ("8", "14.25"); + } else if (midPoint < -5_000) { + return ("10", "18"); + } else if (midPoint < 0) { + return ("11", "21"); + } else if (midPoint < 5_000) { + return ("13", "23"); + } else if (midPoint < 25_000) { + return ("15", "25"); + } else if (midPoint < 75_000) { + return ("18", "26"); + } else if (midPoint < 125_000) { + return ("21", "27"); + } else { + return ("24", "27"); + } + } + + /// @notice Generates the SVG for a rare sparkle if the NFT is rare. Else, returns an empty string + /// @param tokenId The token ID + /// @param hooks The hooks address + /// @return svg The SVG for the rare sparkle + function generateSVGRareSparkle(uint256 tokenId, address hooks) private pure returns (string memory svg) { + if (isRare(tokenId, hooks)) { + svg = string( + abi.encodePacked( + '', + '', + '' + ) + ); + } else { + svg = ""; + } + } + + /// @notice Determines if an NFT is rare based on the token ID and hooks address + /// @param tokenId The token ID + /// @param hooks The hooks address + /// @return Whether the NFT is rare or not + function isRare(uint256 tokenId, address hooks) internal pure returns (bool) { + bytes32 h = keccak256(abi.encodePacked(tokenId, hooks)); + return uint256(h) < type(uint256).max / (1 + BitMath.mostSignificantBit(tokenId) * 2); + } +} diff --git a/src/libraries/SafeCurrencyMetadata.sol b/src/libraries/SafeCurrencyMetadata.sol new file mode 100644 index 000000000..e1a4c059c --- /dev/null +++ b/src/libraries/SafeCurrencyMetadata.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IERC20Metadata} from "openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.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 { + uint8 constant MAX_SYMBOL_LENGTH = 12; + + /// @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 currency symbol + function currencySymbol(address currency, string memory nativeLabel) internal view returns (string memory) { + if (currency == address(0)) { + return nativeLabel; + } + string memory symbol = callAndParseStringReturn(currency, IERC20Metadata.symbol.selector); + if (bytes(symbol).length == 0) { + // fallback to 6 uppercase hex of address + 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 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.staticcall(abi.encodeCall(IERC20Metadata.decimals, ())); + if (!success) { + return 0; + } + if (data.length == 32) { + uint256 decimals = abi.decode(data, (uint256)); + if (decimals <= type(uint8).max) { + return uint8(decimals); + } + } + return 0; + } + + function bytes32ToString(bytes32 x) private pure returns (string memory) { + bytes memory bytesString = new bytes(32); + uint256 charCount = 0; + for (uint256 j = 0; j < 32; j++) { + bytes1 char = x[j]; + if (char != 0) { + bytesString[charCount] = char; + charCount++; + } + } + bytes memory bytesStringTrimmed = new bytes(charCount); + for (uint256 j = 0; j < charCount; j++) { + bytesStringTrimmed[j] = bytesString[j]; + } + return string(bytesStringTrimmed); + } + + /// @notice produces a symbol from the address - the first 6 hex of the address string in upper case + /// @param currencyAddress the address of the currency + /// @return the symbol + function addressToSymbol(address currencyAddress) private pure returns (string memory) { + return AddressStringUtil.toAsciiString(currencyAddress, 6); + } + + /// @notice calls an external view contract method that returns a symbol, and parses the output into a string + /// @param currencyAddress the address of the currency + /// @param selector the selector of the symbol method + /// @return the symbol + function callAndParseStringReturn(address currencyAddress, bytes4 selector) private view returns (string memory) { + (bool success, bytes memory data) = currencyAddress.staticcall(abi.encodeWithSelector(selector)); + // if not implemented, return empty string + if (!success) { + return ""; + } + // bytes32 data always has length 32 + if (data.length == 32) { + bytes32 decoded = abi.decode(data, (bytes32)); + return bytes32ToString(decoded); + } else if (data.length > 64) { + return abi.decode(data, (string)); + } + 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 new file mode 100644 index 000000000..4b326814c --- /dev/null +++ b/test/PositionDescriptor.t.sol @@ -0,0 +1,368 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; +import {PositionDescriptor} from "../src/PositionDescriptor.sol"; +import {CurrencyRatioSortOrder} from "../src/libraries/CurrencyRatioSortOrder.sol"; +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; +import {LiquidityAmounts} from "@uniswap/v4-core/test/utils/LiquidityAmounts.sol"; +import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; +import {PositionConfig} from "./shared/PositionConfig.sol"; +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; + address public USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; + address public USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7; + address public TBTC = 0x8dAEBADE922dF735c38C80C7eBD708Af50815fAa; + address public WBTC = 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599; + string public nativeCurrencyLabel = "ETH"; + + struct Token { + string description; + string image; + string name; + } + + function setUp() public { + deployFreshManager(); + (currency0, currency1) = deployAndMint2Currencies(); + (key,) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_PRICE_1_1); + deployAndApprovePosm(manager); + } + + function test_bytecodeSize_positionDescriptor() public { + snapSize("positionDescriptor bytecode size", address(positionDescriptor)); + } + + function test_setup_succeeds() public view { + assertEq(address(positionDescriptor.poolManager()), address(manager)); + assertEq(positionDescriptor.wrappedNative(), WETH9); + assertEq(positionDescriptor.nativeCurrencyLabel(), nativeCurrencyLabel); + } + + function test_currencyRatioPriority_mainnet_succeeds() public { + vm.chainId(1); + assertEq(positionDescriptor.currencyRatioPriority(WETH9), CurrencyRatioSortOrder.DENOMINATOR); + assertEq(positionDescriptor.currencyRatioPriority(address(0)), CurrencyRatioSortOrder.DENOMINATOR); + assertEq(positionDescriptor.currencyRatioPriority(USDC), CurrencyRatioSortOrder.NUMERATOR_MOST); + assertEq(positionDescriptor.currencyRatioPriority(USDT), CurrencyRatioSortOrder.NUMERATOR_MORE); + assertEq(positionDescriptor.currencyRatioPriority(DAI), CurrencyRatioSortOrder.NUMERATOR); + assertEq(positionDescriptor.currencyRatioPriority(TBTC), CurrencyRatioSortOrder.DENOMINATOR_MORE); + assertEq(positionDescriptor.currencyRatioPriority(WBTC), CurrencyRatioSortOrder.DENOMINATOR_MOST); + assertEq(positionDescriptor.currencyRatioPriority(makeAddr("ALICE")), 0); + } + + function test_currencyRatioPriority_notMainnet_succeeds() public { + assertEq(positionDescriptor.currencyRatioPriority(WETH9), CurrencyRatioSortOrder.DENOMINATOR); + assertEq(positionDescriptor.currencyRatioPriority(address(0)), CurrencyRatioSortOrder.DENOMINATOR); + assertEq(positionDescriptor.currencyRatioPriority(USDC), 0); + assertEq(positionDescriptor.currencyRatioPriority(USDT), 0); + assertEq(positionDescriptor.currencyRatioPriority(DAI), 0); + assertEq(positionDescriptor.currencyRatioPriority(TBTC), 0); + assertEq(positionDescriptor.currencyRatioPriority(WBTC), 0); + assertEq(positionDescriptor.currencyRatioPriority(makeAddr("ALICE")), 0); + } + + function test_flipRatio_succeeds() public { + vm.chainId(1); + // bc price = token1/token0 + assertTrue(positionDescriptor.flipRatio(USDC, WETH9)); + assertFalse(positionDescriptor.flipRatio(DAI, USDC)); + assertFalse(positionDescriptor.flipRatio(WBTC, WETH9)); + assertFalse(positionDescriptor.flipRatio(WBTC, USDC)); + assertFalse(positionDescriptor.flipRatio(WBTC, DAI)); + } + + function test_tokenURI_succeeds() public { + int24 tickLower = int24(key.tickSpacing); + int24 tickUpper = int24(key.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: key, tickLower: tickLower, tickUpper: tickUpper}); + uint256 tokenId = lpm.nextTokenId(); + mint(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(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, + 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_tokenURI_revertsWithInvalidTokenId() public { + int24 tickLower = int24(key.tickSpacing); + int24 tickUpper = int24(key.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: key, tickLower: tickLower, tickUpper: tickUpper}); + uint256 tokenId = lpm.nextTokenId(); + mint(config, liquidityToAdd, ActionConstants.MSG_SENDER, ZERO_BYTES); + + vm.expectRevert(abi.encodeWithSelector(PositionDescriptor.InvalidTokenId.selector, tokenId + 1)); + + 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/StateViewTest.t.sol b/test/StateViewTest.t.sol index 392d70484..c243f12fa 100644 --- a/test/StateViewTest.t.sol +++ b/test/StateViewTest.t.sol @@ -38,7 +38,7 @@ contract StateViewTest is Test, Deployers, Fuzzers, GasSnapshot { // Create the pool key = PoolKey(currency0, currency1, 3000, 60, IHooks(address(0x0))); poolId = key.toId(); - manager.initialize(key, SQRT_PRICE_1_1, ZERO_BYTES); + manager.initialize(key, SQRT_PRICE_1_1); state = new StateView(manager); } 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 8de8e4616..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); @@ -452,7 +452,7 @@ contract QuoterTest is Test, Deployers, GasSnapshot { } function setupPool(PoolKey memory poolKey) internal { - manager.initialize(poolKey, SQRT_PRICE_1_1, ZERO_BYTES); + manager.initialize(poolKey, SQRT_PRICE_1_1); MockERC20(Currency.unwrap(poolKey.currency0)).approve(address(positionManager), type(uint256).max); MockERC20(Currency.unwrap(poolKey.currency1)).approve(address(positionManager), type(uint256).max); positionManager.modifyLiquidity( @@ -468,7 +468,7 @@ contract QuoterTest is Test, Deployers, GasSnapshot { } function setupPoolMultiplePositions(PoolKey memory poolKey) internal { - manager.initialize(poolKey, SQRT_PRICE_1_1, ZERO_BYTES); + manager.initialize(poolKey, SQRT_PRICE_1_1); MockERC20(Currency.unwrap(poolKey.currency0)).approve(address(positionManager), type(uint256).max); MockERC20(Currency.unwrap(poolKey.currency1)).approve(address(positionManager), type(uint256).max); positionManager.modifyLiquidity( @@ -501,7 +501,7 @@ contract QuoterTest is Test, Deployers, GasSnapshot { PoolId poolId = poolKey.toId(); (uint160 sqrtPriceX96,,,) = manager.getSlot0(poolId); if (sqrtPriceX96 == 0) { - manager.initialize(poolKey, SQRT_PRICE_1_1, ZERO_BYTES); + manager.initialize(poolKey, SQRT_PRICE_1_1); } MockERC20(Currency.unwrap(poolKey.currency0)).approve(address(positionManager), type(uint256).max); @@ -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/base64.sol b/test/base64.sol new file mode 100644 index 000000000..811a8f09b --- /dev/null +++ b/test/base64.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +/// @title Base64 +/// @author Brecht Devos - +/// @notice Provides functions for decoding base64 +library Base64 { + bytes internal constant TABLE_DECODE = hex"0000000000000000000000000000000000000000000000000000000000000000" + hex"00000000000000000000003e0000003f3435363738393a3b3c3d000000000000" + hex"00000102030405060708090a0b0c0d0e0f101112131415161718190000000000" + hex"001a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132330000000000"; + + function decode(string memory _data) internal pure returns (bytes memory) { + bytes memory data = bytes(_data); + + if (data.length == 0) return new bytes(0); + require(data.length % 4 == 0, "invalid base64 decoder input"); + + // load the table into memory + bytes memory table = TABLE_DECODE; + + // every 4 characters represent 3 bytes + uint256 decodedLen = (data.length / 4) * 3; + + // add some extra buffer at the end required for the writing + bytes memory result = new bytes(decodedLen + 32); + + assembly { + // padding with '=' + let lastBytes := mload(add(data, mload(data))) + if eq(and(lastBytes, 0xFF), 0x3d) { + decodedLen := sub(decodedLen, 1) + if eq(and(lastBytes, 0xFFFF), 0x3d3d) { decodedLen := sub(decodedLen, 1) } + } + + // set the actual output length + mstore(result, decodedLen) + + // prepare the lookup table + let tablePtr := add(table, 1) + + // input ptr + let dataPtr := data + let endPtr := add(dataPtr, mload(data)) + + // result ptr, jump over length + let resultPtr := add(result, 32) + + // run over the input, 4 characters at a time + for {} lt(dataPtr, endPtr) {} { + // read 4 characters + dataPtr := add(dataPtr, 4) + let input := mload(dataPtr) + + // write 3 bytes + let output := + add( + add( + shl(18, and(mload(add(tablePtr, and(shr(24, input), 0xFF))), 0xFF)), + shl(12, and(mload(add(tablePtr, and(shr(16, input), 0xFF))), 0xFF)) + ), + add( + shl(6, and(mload(add(tablePtr, and(shr(8, input), 0xFF))), 0xFF)), + and(mload(add(tablePtr, and(input, 0xFF))), 0xFF) + ) + ) + mstore(resultPtr, shl(232, output)) + resultPtr := add(resultPtr, 3) + } + } + + return result; + } +} 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 new file mode 100644 index 000000000..2f3d5fd80 --- /dev/null +++ b/test/libraries/Descriptor.t.sol @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {Descriptor} from "../../src/libraries/Descriptor.sol"; +import {Test} from "forge-std/Test.sol"; +import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; + +contract DescriptorTest is Test { + function test_feeToPercentString_succeeds() public pure { + assertEq(Descriptor.feeToPercentString(0x800000), "Dynamic"); + assertEq(Descriptor.feeToPercentString(0), "0%"); + assertEq(Descriptor.feeToPercentString(1), "0.0001%"); + assertEq(Descriptor.feeToPercentString(30), "0.003%"); + assertEq(Descriptor.feeToPercentString(33), "0.0033%"); + assertEq(Descriptor.feeToPercentString(500), "0.05%"); + assertEq(Descriptor.feeToPercentString(2500), "0.25%"); + assertEq(Descriptor.feeToPercentString(3000), "0.3%"); + assertEq(Descriptor.feeToPercentString(10000), "1%"); + assertEq(Descriptor.feeToPercentString(17000), "1.7%"); + assertEq(Descriptor.feeToPercentString(100000), "10%"); + assertEq(Descriptor.feeToPercentString(150000), "15%"); + assertEq(Descriptor.feeToPercentString(102000), "10.2%"); + assertEq(Descriptor.feeToPercentString(1000000), "100%"); + assertEq(Descriptor.feeToPercentString(1005000), "100.5%"); + assertEq(Descriptor.feeToPercentString(10000000), "1000%"); + assertEq(Descriptor.feeToPercentString(12300000), "1230%"); + } + + function test_addressToString_succeeds() public pure { + assertEq(Descriptor.addressToString(address(0)), "0x0000000000000000000000000000000000000000"); + assertEq(Descriptor.addressToString(address(1)), "0x0000000000000000000000000000000000000001"); + assertEq( + Descriptor.addressToString(0x1111111111111111111111111111111111111111), + "0x1111111111111111111111111111111111111111" + ); + assertEq( + Descriptor.addressToString(0x1234AbcdEf1234abcDef1234aBCdEF1234ABCDEF), + "0x1234abcdef1234abcdef1234abcdef1234abcdef" + ); + } + + 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 { + int24 tickSpacing = 10; + int24 minTick = (TickMath.MIN_TICK / tickSpacing) * tickSpacing; + int24 maxTick = (TickMath.MAX_TICK / tickSpacing) * tickSpacing; + assertEq(Descriptor.tickToDecimalString(minTick, tickSpacing, 18, 18, false), "MIN"); + assertEq(Descriptor.tickToDecimalString(maxTick, tickSpacing, 18, 18, false), "MAX"); + assertEq(Descriptor.tickToDecimalString(1, tickSpacing, 18, 18, false), "1.0001"); + int24 otherMinTick = (TickMath.MIN_TICK / 60) * 60; + assertEq( + Descriptor.tickToDecimalString(otherMinTick, tickSpacing, 18, 18, false), + "0.0000000000000000000000000000000000000029387" + ); + } + + function test_tickToDecimalString_withTickSpacing60() public pure { + int24 tickSpacing = 60; + int24 minTick = (TickMath.MIN_TICK / tickSpacing) * tickSpacing; + int24 maxTick = (TickMath.MAX_TICK / tickSpacing) * tickSpacing; + assertEq(Descriptor.tickToDecimalString(minTick, tickSpacing, 18, 18, false), "MIN"); + assertEq(Descriptor.tickToDecimalString(maxTick, tickSpacing, 18, 18, false), "MAX"); + assertEq(Descriptor.tickToDecimalString(-1, tickSpacing, 18, 18, false), "0.99990"); + int24 otherMinTick = (TickMath.MIN_TICK / 200) * 200; + assertEq( + Descriptor.tickToDecimalString(otherMinTick, tickSpacing, 18, 18, false), + "0.0000000000000000000000000000000000000029387" + ); + } + + function test_tickToDecimalString_withTickSpacing200() public pure { + int24 tickSpacing = 200; + int24 minTick = (TickMath.MIN_TICK / tickSpacing) * tickSpacing; + int24 maxTick = (TickMath.MAX_TICK / tickSpacing) * tickSpacing; + assertEq(Descriptor.tickToDecimalString(minTick, tickSpacing, 18, 18, false), "MIN"); + assertEq(Descriptor.tickToDecimalString(maxTick, tickSpacing, 18, 18, false), "MAX"); + assertEq(Descriptor.tickToDecimalString(0, tickSpacing, 18, 18, false), "1.0000"); + int24 otherMinTick = (TickMath.MIN_TICK / 60) * 60; + assertEq( + Descriptor.tickToDecimalString(otherMinTick, tickSpacing, 18, 18, false), + "0.0000000000000000000000000000000000000029387" + ); + } + + function test_tickToDecimalString_ratio_returnsInverseMediumNumbers() public pure { + int24 tickSpacing = 200; + assertEq(Descriptor.tickToDecimalString(10, tickSpacing, 18, 18, false), "1.0010"); + assertEq(Descriptor.tickToDecimalString(10, tickSpacing, 18, 18, true), "0.99900"); + } + + function test_tickToDecimalString_ratio_returnsInverseLargeNumbers() public pure { + int24 tickSpacing = 200; + assertEq(Descriptor.tickToDecimalString(487272, tickSpacing, 18, 18, false), "1448400000000000000000"); + assertEq(Descriptor.tickToDecimalString(487272, tickSpacing, 18, 18, true), "0.00000000000000000000069041"); + } + + function test_tickToDecimalString_ratio_returnsInverseSmallNumbers() public pure { + int24 tickSpacing = 200; + assertEq(Descriptor.tickToDecimalString(-387272, tickSpacing, 18, 18, false), "0.000000000000000015200"); + assertEq(Descriptor.tickToDecimalString(-387272, tickSpacing, 18, 18, true), "65791000000000000"); + } + + function test_tickToDecimalString_differentDecimals() public pure { + int24 tickSpacing = 200; + assertEq(Descriptor.tickToDecimalString(1000, tickSpacing, 18, 18, true), "0.90484"); + assertEq(Descriptor.tickToDecimalString(1000, tickSpacing, 18, 10, true), "90484000"); + assertEq(Descriptor.tickToDecimalString(1000, tickSpacing, 10, 18, true), "0.0000000090484"); + } + + function test_fixedPointToDecimalString() public pure { + assertEq( + Descriptor.fixedPointToDecimalString(1457647476727839560029885420909913413788472405159, 18, 18), + "338490000000000000000000000000000000000" + ); + assertEq( + Descriptor.fixedPointToDecimalString(4025149349925610116743993887520032712, 18, 18), "2581100000000000" + ); + assertEq(Descriptor.fixedPointToDecimalString(3329657202331788924044422905302854, 18, 18), "1766200000"); + assertEq(Descriptor.fixedPointToDecimalString(16241966553695418990605751641065, 18, 18), "42026"); + assertEq(Descriptor.fixedPointToDecimalString(2754475062069337566441091812235, 18, 18), "1208.7"); + assertEq(Descriptor.fixedPointToDecimalString(871041495427277622831427623669, 18, 18), "120.87"); + assertEq(Descriptor.fixedPointToDecimalString(275447506206933756644109181223, 18, 18), "12.087"); + + assertEq(Descriptor.fixedPointToDecimalString(88028870788706913884596530851, 18, 18), "1.2345"); + assertEq(Descriptor.fixedPointToDecimalString(79228162514264337593543950336, 18, 18), "1.0000"); + assertEq(Descriptor.fixedPointToDecimalString(27837173154497669652482281089, 18, 18), "0.12345"); + assertEq(Descriptor.fixedPointToDecimalString(1559426812423768092342, 18, 18), "0.00000000000000038741"); + assertEq(Descriptor.fixedPointToDecimalString(74532606916587, 18, 18), "0.00000000000000000000000000000088498"); + assertEq( + Descriptor.fixedPointToDecimalString(4947797163, 18, 18), "0.0000000000000000000000000000000000000029387" + ); + + assertEq(Descriptor.fixedPointToDecimalString(79228162514264337593543950336, 18, 16), "100.00"); + assertEq(Descriptor.fixedPointToDecimalString(250541448375047931186413801569, 18, 17), "100.00"); + assertEq(Descriptor.fixedPointToDecimalString(79228162514264337593543950336, 24, 5), "1.0000"); + + assertEq(Descriptor.fixedPointToDecimalString(79228162514264337593543950336, 10, 18), "0.000000010000"); + assertEq(Descriptor.fixedPointToDecimalString(79228162514264337593543950336, 7, 18), "0.000000000010000"); + } +} diff --git a/test/libraries/SVG.t.sol b/test/libraries/SVG.t.sol new file mode 100644 index 000000000..915557cbb --- /dev/null +++ b/test/libraries/SVG.t.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {SVG} from "../../src/libraries/SVG.sol"; +import {Test} from "forge-std/Test.sol"; + +contract DescriptorTest is Test { + function test_rangeLocation_succeeds() public pure { + (string memory x, string memory y) = SVG.rangeLocation(-887_272, -887_100); + assertEq(x, "8"); + assertEq(y, "7"); + (x, y) = SVG.rangeLocation(-100_000, -90_000); + assertEq(x, "8"); + assertEq(y, "10.5"); + (x, y) = SVG.rangeLocation(-50_000, -20_000); + assertEq(x, "8"); + assertEq(y, "14.25"); + (x, y) = SVG.rangeLocation(-10_000, -5_000); + assertEq(x, "10"); + assertEq(y, "18"); + (x, y) = SVG.rangeLocation(-5_000, -4_000); + assertEq(x, "11"); + assertEq(y, "21"); + (x, y) = SVG.rangeLocation(4_000, 5_000); + assertEq(x, "13"); + assertEq(y, "23"); + (x, y) = SVG.rangeLocation(10_000, 15_000); + assertEq(x, "15"); + assertEq(y, "25"); + (x, y) = SVG.rangeLocation(25_000, 50_000); + assertEq(x, "18"); + assertEq(y, "26"); + (x, y) = SVG.rangeLocation(100_000, 125_000); + assertEq(x, "21"); + assertEq(y, "27"); + (x, y) = SVG.rangeLocation(200_000, 100_000); + assertEq(x, "24"); + assertEq(y, "27"); + (x, y) = SVG.rangeLocation(887_272, 887_272); + assertEq(x, "24"); + assertEq(y, "27"); + } + + function test_isRare_succeeds() public pure { + bool result = SVG.isRare(1, 0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB); + assertTrue(result); + 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 6056a638f..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"; @@ -12,4 +12,8 @@ contract MockERC721Permit is ERC721Permit_v4 { tokenId = ++lastTokenId; _mint(msg.sender, tokenId); } + + function tokenURI(uint256) public pure override returns (string memory) { + return "mock"; + } } 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/Execute.t.sol b/test/position-managers/Execute.t.sol index 42fb1a7b6..dd99d86b1 100644 --- a/test/position-managers/Execute.t.sol +++ b/test/position-managers/Execute.t.sol @@ -47,7 +47,7 @@ contract ExecuteTest is Test, PosmTestSetup, LiquidityFuzzers { // This is needed to receive return deltas from modifyLiquidity calls. deployPosmHookSavesDelta(); - (key, poolId) = initPool(currency0, currency1, IHooks(address(hook)), 3000, SQRT_PRICE_1_1, ZERO_BYTES); + (key, poolId) = initPool(currency0, currency1, IHooks(address(hook)), 3000, SQRT_PRICE_1_1); // Requires currency0 and currency1 to be set in base Deployers contract. deployAndApprovePosm(manager); diff --git a/test/position-managers/FeeCollection.t.sol b/test/position-managers/FeeCollection.t.sol index 837e2a82d..c25085365 100644 --- a/test/position-managers/FeeCollection.t.sol +++ b/test/position-managers/FeeCollection.t.sol @@ -40,7 +40,7 @@ contract FeeCollectionTest is Test, PosmTestSetup, LiquidityFuzzers { // This is needed to receive return deltas from modifyLiquidity calls. deployPosmHookSavesDelta(); - (key, poolId) = initPool(currency0, currency1, IHooks(hook), 3000, SQRT_PRICE_1_1, ZERO_BYTES); + (key, poolId) = initPool(currency0, currency1, IHooks(hook), 3000, SQRT_PRICE_1_1); FEE_WAD = uint256(key.fee).mulDivDown(FixedPointMathLib.WAD, 1_000_000); // Requires currency0 and currency1 to be set in base Deployers contract. diff --git a/test/position-managers/IncreaseLiquidity.t.sol b/test/position-managers/IncreaseLiquidity.t.sol index ba0ec2f88..a0264d2b1 100644 --- a/test/position-managers/IncreaseLiquidity.t.sol +++ b/test/position-managers/IncreaseLiquidity.t.sol @@ -57,7 +57,7 @@ contract IncreaseLiquidityTest is Test, PosmTestSetup, Fuzzers { // This is needed to receive return deltas from modifyLiquidity calls. deployPosmHookSavesDelta(); - (key, poolId) = initPool(currency0, currency1, IHooks(hook), 3000, SQRT_PRICE_1_1, ZERO_BYTES); + (key, poolId) = initPool(currency0, currency1, IHooks(hook), 3000, SQRT_PRICE_1_1); FEE_WAD = uint256(key.fee).mulDivDown(FixedPointMathLib.WAD, 1_000_000); // Requires currency0 and currency1 to be set in base Deployers contract. diff --git a/test/position-managers/NativeToken.t.sol b/test/position-managers/NativeToken.t.sol index fcad290cd..c79f74dea 100644 --- a/test/position-managers/NativeToken.t.sol +++ b/test/position-managers/NativeToken.t.sol @@ -54,7 +54,7 @@ contract PositionManagerTest is Test, PosmTestSetup, LiquidityFuzzers { deployPosmHookSavesDelta(); currency0 = CurrencyLibrary.ADDRESS_ZERO; - (nativeKey, poolId) = initPool(currency0, currency1, IHooks(hook), 3000, SQRT_PRICE_1_1, ZERO_BYTES); + (nativeKey, poolId) = initPool(currency0, currency1, IHooks(hook), 3000, SQRT_PRICE_1_1); deployPosm(manager); // currency0 is the native token so only execute approvals for currency1. diff --git a/test/position-managers/Permit.t.sol b/test/position-managers/Permit.t.sol index ea3a9c6d9..0ce9a5b9a 100644 --- a/test/position-managers/Permit.t.sol +++ b/test/position-managers/Permit.t.sol @@ -43,7 +43,7 @@ contract PermitTest is Test, PosmTestSetup { deployFreshManagerAndRouters(); deployMintAndApprove2Currencies(); - (key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_PRICE_1_1, ZERO_BYTES); + (key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_PRICE_1_1); // Requires currency0 and currency1 to be set in base Deployers contract. deployAndApprovePosm(manager); @@ -64,7 +64,7 @@ contract PermitTest is Test, PosmTestSetup { keccak256( abi.encode( keccak256("EIP712Domain(string name,uint256 chainId,address verifyingContract)"), - keccak256("Uniswap V4 Positions NFT"), // storage is private on EIP712.sol so we need to hardcode these + keccak256("Uniswap v4 Positions NFT"), // storage is private on EIP712.sol so we need to hardcode these block.chainid, address(lpm) ) diff --git a/test/position-managers/PositionManager.gas.t.sol b/test/position-managers/PositionManager.gas.t.sol index 0fd658ead..bf27604f9 100644 --- a/test/position-managers/PositionManager.gas.t.sol +++ b/test/position-managers/PositionManager.gas.t.sol @@ -53,8 +53,8 @@ contract PosMGasTest is Test, PosmTestSetup, GasSnapshot { deployFreshManagerAndRouters(); deployMintAndApprove2Currencies(); - (key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_PRICE_1_1, ZERO_BYTES); - (nativeKey,) = initPool(CurrencyLibrary.ADDRESS_ZERO, currency1, IHooks(hook), 3000, SQRT_PRICE_1_1, ZERO_BYTES); + (key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_PRICE_1_1); + (nativeKey,) = initPool(CurrencyLibrary.ADDRESS_ZERO, currency1, IHooks(hook), 3000, SQRT_PRICE_1_1); FEE_WAD = uint256(key.fee).mulDivDown(FixedPointMathLib.WAD, 1_000_000); // Requires currency0 and currency1 to be set in base Deployers contract. @@ -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, @@ -393,7 +397,7 @@ contract PosMGasTest is Test, PosmTestSetup, GasSnapshot { // Use multicall to initialize a pool and mint liquidity bytes[] memory calls = new bytes[](2); - calls[0] = abi.encodeWithSelector(lpm.initializePool.selector, key, SQRT_PRICE_1_1, ZERO_BYTES); + calls[0] = abi.encodeWithSelector(lpm.initializePool.selector, key, SQRT_PRICE_1_1); config = PositionConfig({ poolKey: key, diff --git a/test/position-managers/PositionManager.modifyLiquidities.t.sol b/test/position-managers/PositionManager.modifyLiquidities.t.sol index 9b135e4f3..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"); @@ -53,9 +76,29 @@ contract PositionManagerModifyLiquiditiesTest is Test, PosmTestSetup, LiquidityF deployPosmHookModifyLiquidities(); seedBalance(address(hookModifyLiquidities)); - (key, poolId) = initPool(currency0, currency1, IHooks(hookModifyLiquidities), 3000, SQRT_PRICE_1_1, ZERO_BYTES); + (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 e5347fb43..087c21027 100644 --- a/test/position-managers/PositionManager.multicall.t.sol +++ b/test/position-managers/PositionManager.multicall.t.sol @@ -71,7 +71,7 @@ contract PositionManagerMulticallTest is Test, Permit2SignatureHelpers, PosmTest deployFreshManagerAndRouters(); deployMintAndApprove2Currencies(); - (key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_PRICE_1_1, ZERO_BYTES); + (key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_PRICE_1_1); // Requires currency0 and currency1 to be set in base Deployers contract. deployAndApprovePosm(manager); @@ -98,7 +98,7 @@ contract PositionManagerMulticallTest is Test, Permit2SignatureHelpers, PosmTest // Use multicall to initialize a pool and mint liquidity bytes[] memory calls = new bytes[](2); - calls[0] = abi.encodeWithSelector(lpm.initializePool.selector, key, SQRT_PRICE_1_1, ZERO_BYTES); + calls[0] = abi.encodeWithSelector(lpm.initializePool.selector, key, SQRT_PRICE_1_1); config = PositionConfig({ poolKey: key, @@ -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, @@ -144,7 +185,7 @@ contract PositionManagerMulticallTest is Test, Permit2SignatureHelpers, PosmTest // Use multicall to initialize a pool and mint liquidity bytes[] memory calls = new bytes[](2); - calls[0] = abi.encodeWithSelector(lpm.initializePool.selector, key, SQRT_PRICE_1_1, ZERO_BYTES); + calls[0] = abi.encodeWithSelector(lpm.initializePool.selector, key, SQRT_PRICE_1_1); config = PositionConfig({ poolKey: key, @@ -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, ZERO_BYTES); - - 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 5e61891bd..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,15 +34,18 @@ 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(); - (key,) = initPool(currency0, currency1, IHooks(hook), 3000, SQRT_PRICE_1_1, ZERO_BYTES); + (key,) = initPool(currency0, currency1, IHooks(hook), 3000, SQRT_PRICE_1_1); // Requires currency0 and currency1 to be set in base Deployers contract. deployAndApprovePosm(manager); @@ -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/position-managers/PositionManager.t.sol b/test/position-managers/PositionManager.t.sol index c0c0b96dd..7db64e9b9 100644 --- a/test/position-managers/PositionManager.t.sol +++ b/test/position-managers/PositionManager.t.sol @@ -52,7 +52,7 @@ contract PositionManagerTest is Test, PosmTestSetup, LiquidityFuzzers { // This is needed to receive return deltas from modifyLiquidity calls. deployPosmHookSavesDelta(); - (key, poolId) = initPool(currency0, currency1, IHooks(hook), 3000, SQRT_PRICE_1_1, ZERO_BYTES); + (key, poolId) = initPool(currency0, currency1, IHooks(hook), 3000, SQRT_PRICE_1_1); // Requires currency0 and currency1 to be set in base Deployers contract. deployAndApprovePosm(manager); @@ -92,10 +92,11 @@ contract PositionManagerTest is Test, PosmTestSetup, LiquidityFuzzers { // Set up approvals for the reentrant token approvePosmCurrency(reentrantToken); - (key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_PRICE_1_1, ZERO_BYTES); + (key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_PRICE_1_1); // Try to add liquidity at that range, but the token reenters posm - PositionConfig memory config = PositionConfig({poolKey: key, tickLower: 0, tickUpper: 60}); + PositionConfig memory config = + PositionConfig({poolKey: key, tickLower: -int24(key.tickSpacing), tickUpper: int24(key.tickSpacing)}); bytes memory calls = getMintEncoded(config, 1e18, ActionConstants.MSG_SENDER, ""); // Permit2.transferFrom does not bubble the ContractLocked error and instead reverts with its own error @@ -880,7 +881,7 @@ contract PositionManagerTest is Test, PosmTestSetup, LiquidityFuzzers { function test_initialize() public { // initialize a new pool and add liquidity key = PoolKey({currency0: currency0, currency1: currency1, fee: 0, tickSpacing: 10, hooks: IHooks(address(0))}); - lpm.initializePool(key, SQRT_PRICE_1_1, ZERO_BYTES); + lpm.initializePool(key, SQRT_PRICE_1_1); (uint160 sqrtPriceX96, int24 tick, uint24 protocolFee, uint24 lpFee) = manager.getSlot0(key.toId()); assertEq(sqrtPriceX96, SQRT_PRICE_1_1); @@ -895,7 +896,7 @@ contract PositionManagerTest is Test, PosmTestSetup, LiquidityFuzzers { fee = uint24(bound(fee, 0, LPFeeLibrary.MAX_LP_FEE)); key = PoolKey({currency0: currency0, currency1: currency1, fee: fee, tickSpacing: 10, hooks: IHooks(address(0))}); - lpm.initializePool(key, sqrtPrice, ZERO_BYTES); + lpm.initializePool(key, sqrtPrice); (uint160 sqrtPriceX96, int24 tick, uint24 protocolFee, uint24 lpFee) = manager.getSlot0(key.toId()); assertEq(sqrtPriceX96, sqrtPrice); 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 81c4f9bf5..bce86b97b 100644 --- a/test/shared/PosmTestSetup.sol +++ b/test/shared/PosmTestSetup.sol @@ -15,15 +15,24 @@ import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol" import {DeployPermit2} from "permit2/test/utils/DeployPermit2.sol"; 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 { uint256 constant STARTING_USER_BALANCE = 10_000_000 ether; IAllowanceTransfer permit2; + 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( @@ -33,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); @@ -57,7 +68,8 @@ contract PosmTestSetup is Test, Deployers, DeployPermit2, LiquidityOperations { function deployPosm(IPoolManager poolManager) internal { // We use deployPermit2() to prevent having to use via-ir in this repository. permit2 = IAllowanceTransfer(deployPermit2()); - lpm = new PositionManager(poolManager, permit2, 100_000); + positionDescriptor = new PositionDescriptor(poolManager, 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, "ETH"); + lpm = new PositionManager(poolManager, permit2, 100_000, positionDescriptor, _WETH9); } function seedBalance(address to) internal { @@ -85,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); diff --git a/test/shared/RoutingTestHelpers.sol b/test/shared/RoutingTestHelpers.sol index 3bde34caa..67b8f6011 100644 --- a/test/shared/RoutingTestHelpers.sol +++ b/test/shared/RoutingTestHelpers.sol @@ -76,7 +76,7 @@ contract RoutingTestHelpers is Test, Deployers { if (Currency.unwrap(currencyA) > Currency.unwrap(currencyB)) (currencyA, currencyB) = (currencyB, currencyA); _key = PoolKey(currencyA, currencyB, 3000, 60, IHooks(hookAddr)); - manager.initialize(_key, SQRT_PRICE_1_1, ZERO_BYTES); + manager.initialize(_key, SQRT_PRICE_1_1); MockERC20(Currency.unwrap(currencyA)).approve(address(positionManager), type(uint256).max); MockERC20(Currency.unwrap(currencyB)).approve(address(positionManager), type(uint256).max); positionManager.modifyLiquidity(_key, IPoolManager.ModifyLiquidityParams(-887220, 887220, 200 ether, 0), "0x"); @@ -88,7 +88,7 @@ contract RoutingTestHelpers is Test, Deployers { { _key = PoolKey(CurrencyLibrary.ADDRESS_ZERO, currency, 3000, 60, IHooks(hookAddr)); - manager.initialize(_key, SQRT_PRICE_1_1, ZERO_BYTES); + manager.initialize(_key, SQRT_PRICE_1_1); MockERC20(Currency.unwrap(currency)).approve(address(positionManager), type(uint256).max); positionManager.modifyLiquidity{value: 200 ether}( _key, IPoolManager.ModifyLiquidityParams(-887220, 887220, 200 ether, 0), "0x"