diff --git a/.env b/.env deleted file mode 100644 index 7859e840..00000000 --- a/.env +++ /dev/null @@ -1,7 +0,0 @@ -FOUNDRY_FUZZ_SEED=0x4444 - -if [[ "$OSTYPE" == "linux-gnu"* ]]; then - export FOUNDRY_SOLC="./lib/v4-core/bin/solc-static-linux" -elif [[ "$OSTYPE" == "darwin"* ]]; then - export FOUNDRY_SOLC="./lib/v4-core/bin/solc-mac" -fi diff --git a/.forge-snapshots/BaseActionsRouter_mock10commands.snap b/.forge-snapshots/BaseActionsRouter_mock10commands.snap new file mode 100644 index 00000000..34a072bb --- /dev/null +++ b/.forge-snapshots/BaseActionsRouter_mock10commands.snap @@ -0,0 +1 @@ +61756 \ No newline at end of file diff --git a/.forge-snapshots/FullOracleObserve0After5Seconds.snap b/.forge-snapshots/FullOracleObserve0After5Seconds.snap deleted file mode 100644 index f5b9e8bf..00000000 --- a/.forge-snapshots/FullOracleObserve0After5Seconds.snap +++ /dev/null @@ -1 +0,0 @@ -1912 \ No newline at end of file diff --git a/.forge-snapshots/FullOracleObserve200By13.snap b/.forge-snapshots/FullOracleObserve200By13.snap deleted file mode 100644 index b47b8dc4..00000000 --- a/.forge-snapshots/FullOracleObserve200By13.snap +++ /dev/null @@ -1 +0,0 @@ -20210 \ No newline at end of file diff --git a/.forge-snapshots/FullOracleObserve200By13Plus5.snap b/.forge-snapshots/FullOracleObserve200By13Plus5.snap deleted file mode 100644 index 46616951..00000000 --- a/.forge-snapshots/FullOracleObserve200By13Plus5.snap +++ /dev/null @@ -1 +0,0 @@ -20443 \ No newline at end of file diff --git a/.forge-snapshots/FullOracleObserve5After5Seconds.snap b/.forge-snapshots/FullOracleObserve5After5Seconds.snap deleted file mode 100644 index dba60802..00000000 --- a/.forge-snapshots/FullOracleObserve5After5Seconds.snap +++ /dev/null @@ -1 +0,0 @@ -2024 \ No newline at end of file diff --git a/.forge-snapshots/FullOracleObserveOldest.snap b/.forge-snapshots/FullOracleObserveOldest.snap deleted file mode 100644 index c90bb2fe..00000000 --- a/.forge-snapshots/FullOracleObserveOldest.snap +++ /dev/null @@ -1 +0,0 @@ -19279 \ No newline at end of file diff --git a/.forge-snapshots/FullOracleObserveOldestAfter5Seconds.snap b/.forge-snapshots/FullOracleObserveOldestAfter5Seconds.snap deleted file mode 100644 index 1d23504b..00000000 --- a/.forge-snapshots/FullOracleObserveOldestAfter5Seconds.snap +++ /dev/null @@ -1 +0,0 @@ -19555 \ No newline at end of file diff --git a/.forge-snapshots/FullOracleObserveZero.snap b/.forge-snapshots/FullOracleObserveZero.snap deleted file mode 100644 index 3559f242..00000000 --- a/.forge-snapshots/FullOracleObserveZero.snap +++ /dev/null @@ -1 +0,0 @@ -1477 \ No newline at end of file diff --git a/.forge-snapshots/MIDDLEWARE_REMOVE-deltas-protected.snap b/.forge-snapshots/MIDDLEWARE_REMOVE-deltas-protected.snap new file mode 100644 index 00000000..2557522c --- /dev/null +++ b/.forge-snapshots/MIDDLEWARE_REMOVE-deltas-protected.snap @@ -0,0 +1 @@ +138303 \ No newline at end of file diff --git a/.forge-snapshots/MIDDLEWARE_REMOVE-deltas-vanilla.snap b/.forge-snapshots/MIDDLEWARE_REMOVE-deltas-vanilla.snap new file mode 100644 index 00000000..9f50530b --- /dev/null +++ b/.forge-snapshots/MIDDLEWARE_REMOVE-deltas-vanilla.snap @@ -0,0 +1 @@ +124851 \ No newline at end of file diff --git a/.forge-snapshots/MIDDLEWARE_REMOVE-fee-protected.snap b/.forge-snapshots/MIDDLEWARE_REMOVE-fee-protected.snap new file mode 100644 index 00000000..0f4ae3b9 --- /dev/null +++ b/.forge-snapshots/MIDDLEWARE_REMOVE-fee-protected.snap @@ -0,0 +1 @@ +153543 \ No newline at end of file diff --git a/.forge-snapshots/MIDDLEWARE_REMOVE-fee-vanilla.snap b/.forge-snapshots/MIDDLEWARE_REMOVE-fee-vanilla.snap new file mode 100644 index 00000000..015afa77 --- /dev/null +++ b/.forge-snapshots/MIDDLEWARE_REMOVE-fee-vanilla.snap @@ -0,0 +1 @@ +140103 \ No newline at end of file diff --git a/.forge-snapshots/MIDDLEWARE_REMOVE-override.snap b/.forge-snapshots/MIDDLEWARE_REMOVE-override.snap new file mode 100644 index 00000000..44729915 --- /dev/null +++ b/.forge-snapshots/MIDDLEWARE_REMOVE-override.snap @@ -0,0 +1 @@ +133820 \ No newline at end of file diff --git a/.forge-snapshots/MIDDLEWARE_REMOVE-protected.snap b/.forge-snapshots/MIDDLEWARE_REMOVE-protected.snap new file mode 100644 index 00000000..79afd43e --- /dev/null +++ b/.forge-snapshots/MIDDLEWARE_REMOVE-protected.snap @@ -0,0 +1 @@ +135757 \ No newline at end of file diff --git a/.forge-snapshots/MIDDLEWARE_REMOVE-vanilla.snap b/.forge-snapshots/MIDDLEWARE_REMOVE-vanilla.snap new file mode 100644 index 00000000..9520d0bd --- /dev/null +++ b/.forge-snapshots/MIDDLEWARE_REMOVE-vanilla.snap @@ -0,0 +1 @@ +124822 \ No newline at end of file diff --git a/.forge-snapshots/OracleGrow10Slots.snap b/.forge-snapshots/OracleGrow10Slots.snap deleted file mode 100644 index 3dada479..00000000 --- a/.forge-snapshots/OracleGrow10Slots.snap +++ /dev/null @@ -1 +0,0 @@ -232960 \ No newline at end of file diff --git a/.forge-snapshots/OracleGrow10SlotsCardinalityGreater.snap b/.forge-snapshots/OracleGrow10SlotsCardinalityGreater.snap deleted file mode 100644 index f623cfa5..00000000 --- a/.forge-snapshots/OracleGrow10SlotsCardinalityGreater.snap +++ /dev/null @@ -1 +0,0 @@ -223649 \ No newline at end of file diff --git a/.forge-snapshots/OracleGrow1Slot.snap b/.forge-snapshots/OracleGrow1Slot.snap deleted file mode 100644 index 137baa16..00000000 --- a/.forge-snapshots/OracleGrow1Slot.snap +++ /dev/null @@ -1 +0,0 @@ -32845 \ No newline at end of file diff --git a/.forge-snapshots/OracleGrow1SlotCardinalityGreater.snap b/.forge-snapshots/OracleGrow1SlotCardinalityGreater.snap deleted file mode 100644 index e6dc42ce..00000000 --- a/.forge-snapshots/OracleGrow1SlotCardinalityGreater.snap +++ /dev/null @@ -1 +0,0 @@ -23545 \ No newline at end of file diff --git a/.forge-snapshots/OracleInitialize.snap b/.forge-snapshots/OracleInitialize.snap deleted file mode 100644 index e4e9e6b2..00000000 --- a/.forge-snapshots/OracleInitialize.snap +++ /dev/null @@ -1 +0,0 @@ -51310 \ No newline at end of file diff --git a/.forge-snapshots/OracleObserveBetweenOldestAndOldestPlusOne.snap b/.forge-snapshots/OracleObserveBetweenOldestAndOldestPlusOne.snap deleted file mode 100644 index 5996d53e..00000000 --- a/.forge-snapshots/OracleObserveBetweenOldestAndOldestPlusOne.snap +++ /dev/null @@ -1 +0,0 @@ -5368 \ No newline at end of file diff --git a/.forge-snapshots/OracleObserveCurrentTime.snap b/.forge-snapshots/OracleObserveCurrentTime.snap deleted file mode 100644 index 3559f242..00000000 --- a/.forge-snapshots/OracleObserveCurrentTime.snap +++ /dev/null @@ -1 +0,0 @@ -1477 \ No newline at end of file diff --git a/.forge-snapshots/OracleObserveCurrentTimeCounterfactual.snap b/.forge-snapshots/OracleObserveCurrentTimeCounterfactual.snap deleted file mode 100644 index 3559f242..00000000 --- a/.forge-snapshots/OracleObserveCurrentTimeCounterfactual.snap +++ /dev/null @@ -1 +0,0 @@ -1477 \ No newline at end of file diff --git a/.forge-snapshots/OracleObserveLast20Seconds.snap b/.forge-snapshots/OracleObserveLast20Seconds.snap deleted file mode 100644 index 24efe8f4..00000000 --- a/.forge-snapshots/OracleObserveLast20Seconds.snap +++ /dev/null @@ -1 +0,0 @@ -73037 \ No newline at end of file diff --git a/.forge-snapshots/OracleObserveLatestEqual.snap b/.forge-snapshots/OracleObserveLatestEqual.snap deleted file mode 100644 index 3559f242..00000000 --- a/.forge-snapshots/OracleObserveLatestEqual.snap +++ /dev/null @@ -1 +0,0 @@ -1477 \ No newline at end of file diff --git a/.forge-snapshots/OracleObserveLatestTransform.snap b/.forge-snapshots/OracleObserveLatestTransform.snap deleted file mode 100644 index f5b9e8bf..00000000 --- a/.forge-snapshots/OracleObserveLatestTransform.snap +++ /dev/null @@ -1 +0,0 @@ -1912 \ No newline at end of file diff --git a/.forge-snapshots/OracleObserveMiddle.snap b/.forge-snapshots/OracleObserveMiddle.snap deleted file mode 100644 index 76e5b53e..00000000 --- a/.forge-snapshots/OracleObserveMiddle.snap +++ /dev/null @@ -1 +0,0 @@ -5541 \ No newline at end of file diff --git a/.forge-snapshots/OracleObserveOldest.snap b/.forge-snapshots/OracleObserveOldest.snap deleted file mode 100644 index f124ce2d..00000000 --- a/.forge-snapshots/OracleObserveOldest.snap +++ /dev/null @@ -1 +0,0 @@ -5092 \ No newline at end of file diff --git a/.forge-snapshots/OracleObserveSinceMostRecent.snap b/.forge-snapshots/OracleObserveSinceMostRecent.snap deleted file mode 100644 index 9dab3404..00000000 --- a/.forge-snapshots/OracleObserveSinceMostRecent.snap +++ /dev/null @@ -1 +0,0 @@ -2522 \ No newline at end of file diff --git a/.forge-snapshots/Payments_swap_settleFromCaller_takeAll.snap b/.forge-snapshots/Payments_swap_settleFromCaller_takeAll.snap new file mode 100644 index 00000000..422a77b8 --- /dev/null +++ b/.forge-snapshots/Payments_swap_settleFromCaller_takeAll.snap @@ -0,0 +1 @@ +134171 \ No newline at end of file diff --git a/.forge-snapshots/Payments_swap_settleFromRouter_takeAll.snap b/.forge-snapshots/Payments_swap_settleFromRouter_takeAll.snap new file mode 100644 index 00000000..dcc72d1c --- /dev/null +++ b/.forge-snapshots/Payments_swap_settleFromRouter_takeAll.snap @@ -0,0 +1 @@ +126966 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_burn_empty.snap b/.forge-snapshots/PositionManager_burn_empty.snap new file mode 100644 index 00000000..4c184642 --- /dev/null +++ b/.forge-snapshots/PositionManager_burn_empty.snap @@ -0,0 +1 @@ +47059 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_burn_empty_native.snap b/.forge-snapshots/PositionManager_burn_empty_native.snap new file mode 100644 index 00000000..9230f513 --- /dev/null +++ b/.forge-snapshots/PositionManager_burn_empty_native.snap @@ -0,0 +1 @@ +46876 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_burn_nonEmpty.snap b/.forge-snapshots/PositionManager_burn_nonEmpty.snap new file mode 100644 index 00000000..c15f4f2d --- /dev/null +++ b/.forge-snapshots/PositionManager_burn_nonEmpty.snap @@ -0,0 +1 @@ +129852 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_burn_nonEmpty_native.snap b/.forge-snapshots/PositionManager_burn_nonEmpty_native.snap new file mode 100644 index 00000000..d2ae1c7d --- /dev/null +++ b/.forge-snapshots/PositionManager_burn_nonEmpty_native.snap @@ -0,0 +1 @@ +122773 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_collect.snap b/.forge-snapshots/PositionManager_collect.snap new file mode 100644 index 00000000..0cf00f2a --- /dev/null +++ b/.forge-snapshots/PositionManager_collect.snap @@ -0,0 +1 @@ +149984 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_collect_native.snap b/.forge-snapshots/PositionManager_collect_native.snap new file mode 100644 index 00000000..1b627db8 --- /dev/null +++ b/.forge-snapshots/PositionManager_collect_native.snap @@ -0,0 +1 @@ +141136 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_collect_sameRange.snap b/.forge-snapshots/PositionManager_collect_sameRange.snap new file mode 100644 index 00000000..0cf00f2a --- /dev/null +++ b/.forge-snapshots/PositionManager_collect_sameRange.snap @@ -0,0 +1 @@ +149984 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_decreaseLiquidity.snap b/.forge-snapshots/PositionManager_decreaseLiquidity.snap new file mode 100644 index 00000000..c30fc9de --- /dev/null +++ b/.forge-snapshots/PositionManager_decreaseLiquidity.snap @@ -0,0 +1 @@ +115527 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_decreaseLiquidity_native.snap b/.forge-snapshots/PositionManager_decreaseLiquidity_native.snap new file mode 100644 index 00000000..3c0e3daf --- /dev/null +++ b/.forge-snapshots/PositionManager_decreaseLiquidity_native.snap @@ -0,0 +1 @@ +108384 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_decrease_burnEmpty.snap b/.forge-snapshots/PositionManager_decrease_burnEmpty.snap new file mode 100644 index 00000000..58fe1844 --- /dev/null +++ b/.forge-snapshots/PositionManager_decrease_burnEmpty.snap @@ -0,0 +1 @@ +133885 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_decrease_burnEmpty_native.snap b/.forge-snapshots/PositionManager_decrease_burnEmpty_native.snap new file mode 100644 index 00000000..23aac82f --- /dev/null +++ b/.forge-snapshots/PositionManager_decrease_burnEmpty_native.snap @@ -0,0 +1 @@ +126624 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_decrease_sameRange_allLiquidity.snap b/.forge-snapshots/PositionManager_decrease_sameRange_allLiquidity.snap new file mode 100644 index 00000000..48ed6687 --- /dev/null +++ b/.forge-snapshots/PositionManager_decrease_sameRange_allLiquidity.snap @@ -0,0 +1 @@ +128243 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_increaseLiquidity_erc20.snap b/.forge-snapshots/PositionManager_increaseLiquidity_erc20.snap new file mode 100644 index 00000000..26723396 --- /dev/null +++ b/.forge-snapshots/PositionManager_increaseLiquidity_erc20.snap @@ -0,0 +1 @@ +152100 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_increaseLiquidity_native.snap b/.forge-snapshots/PositionManager_increaseLiquidity_native.snap new file mode 100644 index 00000000..35295eaf --- /dev/null +++ b/.forge-snapshots/PositionManager_increaseLiquidity_native.snap @@ -0,0 +1 @@ +133900 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_increase_autocompoundExactUnclaimedFees.snap b/.forge-snapshots/PositionManager_increase_autocompoundExactUnclaimedFees.snap new file mode 100644 index 00000000..04c138b3 --- /dev/null +++ b/.forge-snapshots/PositionManager_increase_autocompoundExactUnclaimedFees.snap @@ -0,0 +1 @@ +130065 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_increase_autocompoundExcessFeesCredit.snap b/.forge-snapshots/PositionManager_increase_autocompoundExcessFeesCredit.snap new file mode 100644 index 00000000..e55d6257 --- /dev/null +++ b/.forge-snapshots/PositionManager_increase_autocompoundExcessFeesCredit.snap @@ -0,0 +1 @@ +170759 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_increase_autocompound_clearExcess.snap b/.forge-snapshots/PositionManager_increase_autocompound_clearExcess.snap new file mode 100644 index 00000000..375f518b --- /dev/null +++ b/.forge-snapshots/PositionManager_increase_autocompound_clearExcess.snap @@ -0,0 +1 @@ +140581 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint.snap b/.forge-snapshots/PositionManager_mint.snap new file mode 100644 index 00000000..7e0f6688 --- /dev/null +++ b/.forge-snapshots/PositionManager_mint.snap @@ -0,0 +1 @@ +372012 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_native.snap b/.forge-snapshots/PositionManager_mint_native.snap new file mode 100644 index 00000000..e708094b --- /dev/null +++ b/.forge-snapshots/PositionManager_mint_native.snap @@ -0,0 +1 @@ +336712 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_nativeWithSweep.snap b/.forge-snapshots/PositionManager_mint_nativeWithSweep.snap new file mode 100644 index 00000000..15689f38 --- /dev/null +++ b/.forge-snapshots/PositionManager_mint_nativeWithSweep.snap @@ -0,0 +1 @@ +345244 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_onSameTickLower.snap b/.forge-snapshots/PositionManager_mint_onSameTickLower.snap new file mode 100644 index 00000000..90fd8fec --- /dev/null +++ b/.forge-snapshots/PositionManager_mint_onSameTickLower.snap @@ -0,0 +1 @@ +314694 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_onSameTickUpper.snap b/.forge-snapshots/PositionManager_mint_onSameTickUpper.snap new file mode 100644 index 00000000..9b6845af --- /dev/null +++ b/.forge-snapshots/PositionManager_mint_onSameTickUpper.snap @@ -0,0 +1 @@ +315336 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_sameRange.snap b/.forge-snapshots/PositionManager_mint_sameRange.snap new file mode 100644 index 00000000..597983b5 --- /dev/null +++ b/.forge-snapshots/PositionManager_mint_sameRange.snap @@ -0,0 +1 @@ +240918 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_settleWithBalance_sweep.snap b/.forge-snapshots/PositionManager_mint_settleWithBalance_sweep.snap new file mode 100644 index 00000000..a6459af0 --- /dev/null +++ b/.forge-snapshots/PositionManager_mint_settleWithBalance_sweep.snap @@ -0,0 +1 @@ +370018 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_warmedPool_differentRange.snap b/.forge-snapshots/PositionManager_mint_warmedPool_differentRange.snap new file mode 100644 index 00000000..af4a8aa0 --- /dev/null +++ b/.forge-snapshots/PositionManager_mint_warmedPool_differentRange.snap @@ -0,0 +1 @@ +320712 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_multicall_initialize_mint.snap b/.forge-snapshots/PositionManager_multicall_initialize_mint.snap new file mode 100644 index 00000000..28856ebd --- /dev/null +++ b/.forge-snapshots/PositionManager_multicall_initialize_mint.snap @@ -0,0 +1 @@ +416388 \ No newline at end of file diff --git a/.forge-snapshots/StateView_extsload_getFeeGrowthGlobals.snap b/.forge-snapshots/StateView_extsload_getFeeGrowthGlobals.snap new file mode 100644 index 00000000..0cd7e117 --- /dev/null +++ b/.forge-snapshots/StateView_extsload_getFeeGrowthGlobals.snap @@ -0,0 +1 @@ +2398 \ No newline at end of file diff --git a/.forge-snapshots/StateView_extsload_getFeeGrowthInside.snap b/.forge-snapshots/StateView_extsload_getFeeGrowthInside.snap new file mode 100644 index 00000000..d10ca668 --- /dev/null +++ b/.forge-snapshots/StateView_extsload_getFeeGrowthInside.snap @@ -0,0 +1 @@ +8543 \ No newline at end of file diff --git a/.forge-snapshots/StateView_extsload_getLiquidity.snap b/.forge-snapshots/StateView_extsload_getLiquidity.snap new file mode 100644 index 00000000..5303ac12 --- /dev/null +++ b/.forge-snapshots/StateView_extsload_getLiquidity.snap @@ -0,0 +1 @@ +1509 \ No newline at end of file diff --git a/.forge-snapshots/StateView_extsload_getPositionInfo.snap b/.forge-snapshots/StateView_extsload_getPositionInfo.snap new file mode 100644 index 00000000..9d57e9ad --- /dev/null +++ b/.forge-snapshots/StateView_extsload_getPositionInfo.snap @@ -0,0 +1 @@ +2927 \ No newline at end of file diff --git a/.forge-snapshots/StateView_extsload_getPositionLiquidity.snap b/.forge-snapshots/StateView_extsload_getPositionLiquidity.snap new file mode 100644 index 00000000..280f1a09 --- /dev/null +++ b/.forge-snapshots/StateView_extsload_getPositionLiquidity.snap @@ -0,0 +1 @@ +1746 \ No newline at end of file diff --git a/.forge-snapshots/StateView_extsload_getSlot0.snap b/.forge-snapshots/StateView_extsload_getSlot0.snap new file mode 100644 index 00000000..38ca8416 --- /dev/null +++ b/.forge-snapshots/StateView_extsload_getSlot0.snap @@ -0,0 +1 @@ +1606 \ No newline at end of file diff --git a/.forge-snapshots/StateView_extsload_getTickBitmap.snap b/.forge-snapshots/StateView_extsload_getTickBitmap.snap new file mode 100644 index 00000000..dfd4d9fc --- /dev/null +++ b/.forge-snapshots/StateView_extsload_getTickBitmap.snap @@ -0,0 +1 @@ +1704 \ No newline at end of file diff --git a/.forge-snapshots/StateView_extsload_getTickFeeGrowthOutside.snap b/.forge-snapshots/StateView_extsload_getTickFeeGrowthOutside.snap new file mode 100644 index 00000000..f26febc1 --- /dev/null +++ b/.forge-snapshots/StateView_extsload_getTickFeeGrowthOutside.snap @@ -0,0 +1 @@ +2756 \ No newline at end of file diff --git a/.forge-snapshots/StateView_extsload_getTickInfo.snap b/.forge-snapshots/StateView_extsload_getTickInfo.snap new file mode 100644 index 00000000..90a81289 --- /dev/null +++ b/.forge-snapshots/StateView_extsload_getTickInfo.snap @@ -0,0 +1 @@ +3090 \ No newline at end of file diff --git a/.forge-snapshots/StateView_extsload_getTickLiquidity.snap b/.forge-snapshots/StateView_extsload_getTickLiquidity.snap new file mode 100644 index 00000000..ff353461 --- /dev/null +++ b/.forge-snapshots/StateView_extsload_getTickLiquidity.snap @@ -0,0 +1 @@ +1901 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_Bytecode.snap b/.forge-snapshots/V4Router_Bytecode.snap new file mode 100644 index 00000000..c6e2c74d --- /dev/null +++ b/.forge-snapshots/V4Router_Bytecode.snap @@ -0,0 +1 @@ +6942 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactIn1Hop_nativeIn.snap b/.forge-snapshots/V4Router_ExactIn1Hop_nativeIn.snap new file mode 100644 index 00000000..ec0b0f29 --- /dev/null +++ b/.forge-snapshots/V4Router_ExactIn1Hop_nativeIn.snap @@ -0,0 +1 @@ +120501 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactIn1Hop_nativeOut.snap b/.forge-snapshots/V4Router_ExactIn1Hop_nativeOut.snap new file mode 100644 index 00000000..75bad55d --- /dev/null +++ b/.forge-snapshots/V4Router_ExactIn1Hop_nativeOut.snap @@ -0,0 +1 @@ +119696 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactIn1Hop_oneForZero.snap b/.forge-snapshots/V4Router_ExactIn1Hop_oneForZero.snap new file mode 100644 index 00000000..d83695c7 --- /dev/null +++ b/.forge-snapshots/V4Router_ExactIn1Hop_oneForZero.snap @@ -0,0 +1 @@ +128568 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactIn1Hop_zeroForOne.snap b/.forge-snapshots/V4Router_ExactIn1Hop_zeroForOne.snap new file mode 100644 index 00000000..82eb5f4d --- /dev/null +++ b/.forge-snapshots/V4Router_ExactIn1Hop_zeroForOne.snap @@ -0,0 +1 @@ +135398 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactIn2Hops.snap b/.forge-snapshots/V4Router_ExactIn2Hops.snap new file mode 100644 index 00000000..b02cff01 --- /dev/null +++ b/.forge-snapshots/V4Router_ExactIn2Hops.snap @@ -0,0 +1 @@ +186897 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactIn2Hops_nativeIn.snap b/.forge-snapshots/V4Router_ExactIn2Hops_nativeIn.snap new file mode 100644 index 00000000..6f17f2bb --- /dev/null +++ b/.forge-snapshots/V4Router_ExactIn2Hops_nativeIn.snap @@ -0,0 +1 @@ +178832 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactIn3Hops.snap b/.forge-snapshots/V4Router_ExactIn3Hops.snap new file mode 100644 index 00000000..de05bf83 --- /dev/null +++ b/.forge-snapshots/V4Router_ExactIn3Hops.snap @@ -0,0 +1 @@ +238421 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactIn3Hops_nativeIn.snap b/.forge-snapshots/V4Router_ExactIn3Hops_nativeIn.snap new file mode 100644 index 00000000..5665d2e3 --- /dev/null +++ b/.forge-snapshots/V4Router_ExactIn3Hops_nativeIn.snap @@ -0,0 +1 @@ +230380 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactInputSingle.snap b/.forge-snapshots/V4Router_ExactInputSingle.snap new file mode 100644 index 00000000..422a77b8 --- /dev/null +++ b/.forge-snapshots/V4Router_ExactInputSingle.snap @@ -0,0 +1 @@ +134171 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactInputSingle_nativeIn.snap b/.forge-snapshots/V4Router_ExactInputSingle_nativeIn.snap new file mode 100644 index 00000000..146c27b9 --- /dev/null +++ b/.forge-snapshots/V4Router_ExactInputSingle_nativeIn.snap @@ -0,0 +1 @@ +119274 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactInputSingle_nativeOut.snap b/.forge-snapshots/V4Router_ExactInputSingle_nativeOut.snap new file mode 100644 index 00000000..52615b42 --- /dev/null +++ b/.forge-snapshots/V4Router_ExactInputSingle_nativeOut.snap @@ -0,0 +1 @@ +118447 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactOut1Hop_nativeIn_sweepETH.snap b/.forge-snapshots/V4Router_ExactOut1Hop_nativeIn_sweepETH.snap new file mode 100644 index 00000000..57bde469 --- /dev/null +++ b/.forge-snapshots/V4Router_ExactOut1Hop_nativeIn_sweepETH.snap @@ -0,0 +1 @@ +126298 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactOut1Hop_nativeOut.snap b/.forge-snapshots/V4Router_ExactOut1Hop_nativeOut.snap new file mode 100644 index 00000000..019035c6 --- /dev/null +++ b/.forge-snapshots/V4Router_ExactOut1Hop_nativeOut.snap @@ -0,0 +1 @@ +120531 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactOut1Hop_oneForZero.snap b/.forge-snapshots/V4Router_ExactOut1Hop_oneForZero.snap new file mode 100644 index 00000000..d40b084a --- /dev/null +++ b/.forge-snapshots/V4Router_ExactOut1Hop_oneForZero.snap @@ -0,0 +1 @@ +129403 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactOut1Hop_zeroForOne.snap b/.forge-snapshots/V4Router_ExactOut1Hop_zeroForOne.snap new file mode 100644 index 00000000..715f8dca --- /dev/null +++ b/.forge-snapshots/V4Router_ExactOut1Hop_zeroForOne.snap @@ -0,0 +1 @@ +134204 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactOut2Hops.snap b/.forge-snapshots/V4Router_ExactOut2Hops.snap new file mode 100644 index 00000000..abc303ec --- /dev/null +++ b/.forge-snapshots/V4Router_ExactOut2Hops.snap @@ -0,0 +1 @@ +186306 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactOut2Hops_nativeIn.snap b/.forge-snapshots/V4Router_ExactOut2Hops_nativeIn.snap new file mode 100644 index 00000000..4b39aaa9 --- /dev/null +++ b/.forge-snapshots/V4Router_ExactOut2Hops_nativeIn.snap @@ -0,0 +1 @@ +183201 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactOut3Hops.snap b/.forge-snapshots/V4Router_ExactOut3Hops.snap new file mode 100644 index 00000000..f111bdc0 --- /dev/null +++ b/.forge-snapshots/V4Router_ExactOut3Hops.snap @@ -0,0 +1 @@ +238448 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactOut3Hops_nativeIn.snap b/.forge-snapshots/V4Router_ExactOut3Hops_nativeIn.snap new file mode 100644 index 00000000..d6cabe72 --- /dev/null +++ b/.forge-snapshots/V4Router_ExactOut3Hops_nativeIn.snap @@ -0,0 +1 @@ +235367 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactOut3Hops_nativeOut.snap b/.forge-snapshots/V4Router_ExactOut3Hops_nativeOut.snap new file mode 100644 index 00000000..30c1022c --- /dev/null +++ b/.forge-snapshots/V4Router_ExactOut3Hops_nativeOut.snap @@ -0,0 +1 @@ +229600 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactOutputSingle.snap b/.forge-snapshots/V4Router_ExactOutputSingle.snap new file mode 100644 index 00000000..c97de2ba --- /dev/null +++ b/.forge-snapshots/V4Router_ExactOutputSingle.snap @@ -0,0 +1 @@ +132975 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactOutputSingle_nativeIn_sweepETH.snap b/.forge-snapshots/V4Router_ExactOutputSingle_nativeIn_sweepETH.snap new file mode 100644 index 00000000..5c208ff2 --- /dev/null +++ b/.forge-snapshots/V4Router_ExactOutputSingle_nativeIn_sweepETH.snap @@ -0,0 +1 @@ +125069 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactOutputSingle_nativeOut.snap b/.forge-snapshots/V4Router_ExactOutputSingle_nativeOut.snap new file mode 100644 index 00000000..0465d75c --- /dev/null +++ b/.forge-snapshots/V4Router_ExactOutputSingle_nativeOut.snap @@ -0,0 +1 @@ +119360 \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/FEATURE_IMPROVEMENT.yml b/.github/ISSUE_TEMPLATE/FEATURE_IMPROVEMENT.yml index 8857b8a8..e5f0b7fa 100644 --- a/.github/ISSUE_TEMPLATE/FEATURE_IMPROVEMENT.yml +++ b/.github/ISSUE_TEMPLATE/FEATURE_IMPROVEMENT.yml @@ -13,6 +13,9 @@ body: description: Which area of code does your idea improve? multiple: true options: + - Position Manager + - Position Manager, documentation + - Position Manager, tests - Pool Interaction, Hooks - Pool Interaction, Swaps - Pool Interaction, Positions diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 280df88b..fb5820e5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,7 +20,7 @@ jobs: uses: foundry-rs/foundry-toolchain@v1 with: version: nightly - + - name: Run tests run: forge test -vvv env: diff --git a/.gitmodules b/.gitmodules index d2dc450b..9d6618d5 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,15 +1,6 @@ -[submodule "lib/forge-std"] - path = lib/forge-std - url = https://github.com/foundry-rs/forge-std -[submodule "lib/openzeppelin-contracts"] - path = lib/openzeppelin-contracts - url = https://github.com/OpenZeppelin/openzeppelin-contracts -[submodule "lib/forge-gas-snapshot"] - path = lib/forge-gas-snapshot - url = https://github.com/marktoda/forge-gas-snapshot [submodule "lib/v4-core"] path = lib/v4-core url = https://github.com/Uniswap/v4-core -[submodule "lib/solmate"] - path = lib/solmate - url = https://github.com/transmissions11/solmate +[submodule "lib/permit2"] + path = lib/permit2 + url = https://github.com/Uniswap/permit2 diff --git a/README.md b/README.md index b3355a10..1ea94bea 100644 --- a/README.md +++ b/README.md @@ -6,26 +6,6 @@ Uniswap v4 is a new automated market maker protocol that provides extensibility If you’re interested in contributing please see the [contribution guidelines](https://github.com/Uniswap/v4-periphery/blob/main/CONTRIBUTING.md)! -## Repository Structure - -```solidity -contracts/ -----hooks/ - ----examples/ - | GeomeanOracle.sol - | LimitOrder.sol - | TWAMM.sol - | VolatilityOracle.sol -----libraries/ - | Oracle.sol -BaseHook.sol -test/ -``` - -To showcase the power of hooks, this repository provides some interesting examples in the `/hooks/examples/` folder. Note that none of the contracts in this repository are fully production-ready, and the final design for some of the example hooks could look different. - -Eventually, some hooks that have been audited and are considered production-ready will be placed in the root `hooks` folder. Not all hooks will be safe or valuable to users. This repository will maintain a limited set of hook contracts. Even a well-designed and audited hook contract may not be accepted in this repo. - ## Local Deployment and Usage To utilize the contracts and deploy to a local testnet, you can install the code in your repo with forge: @@ -38,7 +18,7 @@ If you are building hooks, it may be useful to inherit from the `BaseHook` contr ```solidity -import {BaseHook} from 'v4-periphery/contracts/BaseHook.sol'; +import {BaseHook} from 'v4-periphery/src/base/hooks/BaseHook.sol'; contract CoolHook is BaseHook { // Override the hook callbacks you want on your hook @@ -46,7 +26,7 @@ contract CoolHook is BaseHook { address, IPoolManager.PoolKey calldata key, IPoolManager.ModifyLiquidityParams calldata params - ) external override poolManagerOnly returns (bytes4) { + ) external override onlyByPoolManager returns (bytes4) { // hook logic return BaseHook.beforeAddLiquidity.selector; } diff --git a/broadcast/DeployStateView.s.sol/11155111/run-1721766499.json b/broadcast/DeployStateView.s.sol/11155111/run-1721766499.json new file mode 100644 index 00000000..2a418479 --- /dev/null +++ b/broadcast/DeployStateView.s.sol/11155111/run-1721766499.json @@ -0,0 +1,53 @@ +{ + "transactions": [ + { + "hash": "0xc101c6b9b2e5782c734e46af5af4a82b5cf3fc882417de0a84d915ace2d5d319", + "transactionType": "CREATE", + "contractName": "StateView", + "contractAddress": "0xc7a3b85d43ff66ad98a895de0eae4b9e24c932d7", + "function": null, + "arguments": [ + "0xc021A7Deb4a939fd7E661a0669faB5ac7Ba2D5d6" + ], + "transaction": { + "from": "0xb7a249bdeff39727b5eb4c7ad458f682bae6adad", + "gas": "0x1423e9", + "value": "0x0", + "input": "0x60a0604052348015600e575f80fd5b50604051611240380380611240833981016040819052602b91603b565b6001600160a01b03166080526066565b5f60208284031215604a575f80fd5b81516001600160a01b0381168114605f575f80fd5b9392505050565b6080516111756100cb5f395f81816102b3015281816103470152818161037d015281816103e801528181610424015281816104610152818161049a015281816104d40152818161050b0152818161054601528181610572015261059e01526111755ff3fe608060405234801561000f575f80fd5b50600436106100cf575f3560e01c80639ec538c81161007d578063dc4c90d311610058578063dc4c90d3146102ae578063f0928f29146102fa578063fa6793d51461032e575f80fd5b80639ec538c814610205578063c815641c14610218578063caedab5414610270575f80fd5b80637c40f1fe116100ad5780637c40f1fe146101685780638a2bb9e6146101b157806397fd7b42146101c4575f80fd5b80631c7ccb4c146100d357806353e9c1fb146100f95780637388426b14610121575b5f80fd5b6100e66100e1366004610e8e565b610341565b6040519081526020015b60405180910390f35b61010c610107366004610ed8565b610376565b604080519283526020830191909152016100f0565b61013461012f366004610f11565b6103b0565b6040805182516fffffffffffffffffffffffffffffffff1681526020808401519082015291810151908201526060016100f0565b61017b610176366004610f7c565b61041b565b604080516fffffffffffffffffffffffffffffffff9095168552600f9390930b60208501529183015260608201526080016100f0565b61010c6101bf366004610f7c565b61045a565b6101d76101d2366004610fa6565b610492565b604080516fffffffffffffffffffffffffffffffff90941684526020840192909252908201526060016100f0565b61010c610213366004610fc6565b6104cd565b61022b610226366004610fc6565b610502565b6040805173ffffffffffffffffffffffffffffffffffffffff909516855260029390930b602085015262ffffff918216928401929092521660608201526080016100f0565b61028361027e366004610f7c565b61053f565b604080516fffffffffffffffffffffffffffffffff9093168352600f9190910b6020830152016100f0565b6102d57f000000000000000000000000000000000000000000000000000000000000000081565b60405173ffffffffffffffffffffffffffffffffffffffff90911681526020016100f0565b61030d610308366004610fa6565b61056c565b6040516fffffffffffffffffffffffffffffffff90911681526020016100f0565b61030d61033c366004610fc6565b610598565b5f61036d7f000000000000000000000000000000000000000000000000000000000000000084846105c3565b90505b92915050565b5f806103a47f00000000000000000000000000000000000000000000000000000000000000008686866106c6565b91509150935093915050565b6103e360405180606001604052805f6fffffffffffffffffffffffffffffffff1681526020015f81526020015f81525090565b6104117f00000000000000000000000000000000000000000000000000000000000000008787878787610762565b9695505050505050565b5f805f8061044a7f00000000000000000000000000000000000000000000000000000000000000008787610811565b9299919850965090945092505050565b5f806104877f00000000000000000000000000000000000000000000000000000000000000008585610914565b915091509250929050565b5f805f6104c07f00000000000000000000000000000000000000000000000000000000000000008686610a02565b9250925092509250925092565b5f806104f97f000000000000000000000000000000000000000000000000000000000000000084610ae9565b91509150915091565b5f805f806105307f000000000000000000000000000000000000000000000000000000000000000086610b62565b93509350935093509193509193565b5f806104877f00000000000000000000000000000000000000000000000000000000000000008585610c47565b5f61036d7f00000000000000000000000000000000000000000000000000000000000000008484610d0b565b5f6103707f000000000000000000000000000000000000000000000000000000000000000083610db0565b5f806105ce84610dc9565b90505f6105dc600583610fdd565b60408051600187900b60208201529081018290529091505f90606001604080518083037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe00181529082905280516020909101207f1e2eaeaf00000000000000000000000000000000000000000000000000000000825260048201819052915073ffffffffffffffffffffffffffffffffffffffff881690631e2eaeaf90602401602060405180830381865afa158015610697573d5f803e3d5ffd5b505050506040513d601f19601f820116820180604052508101906106bb9190611015565b979650505050505050565b5f805f806106d48888610ae9565b915091505f806106e58a8a8a610914565b915091505f806106f68c8c8b610914565b915091505f6107058d8d610b62565b50509150508a60020b8160020b12156107275782850398508184039750610752565b8960020b8160020b126107435784830398508382039750610752565b82858803039850818487030397505b5050505050505094509492505050565b61079560405180606001604052805f6fffffffffffffffffffffffffffffffff1681526020015f81526020015f81525090565b5f604051836026820152846006820152856003820152868152603a600c82012091505f60408201525f60208201525f8152505f805f6107d58b8b86610a02565b604080516060810182526fffffffffffffffffffffffffffffffff90941684526020840192909252908201529450505050509695505050505050565b5f805f805f6108208787610e05565b6040517f35fd631a00000000000000000000000000000000000000000000000000000000815260048101829052600360248201529091505f9073ffffffffffffffffffffffffffffffffffffffff8a16906335fd631a906044015f60405180830381865afa158015610894573d5f803e3d5ffd5b505050506040513d5f823e601f3d9081017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01682016040526108d99190810190611059565b602081015160408201516060909201516fffffffffffffffffffffffffffffffff82169c60809290921d9b5091995090975095505050505050565b5f805f6109218585610e05565b90505f73ffffffffffffffffffffffffffffffffffffffff87166335fd631a61094b846001610fdd565b60405160e083901b7fffffffff000000000000000000000000000000000000000000000000000000001681526004810191909152600260248201526044015b5f60405180830381865afa1580156109a4573d5f803e3d5ffd5b505050506040513d5f823e601f3d9081017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01682016040526109e99190810190611059565b6020810151604090910151909890975095505050505050565b5f805f80610a108686610e59565b6040517f35fd631a00000000000000000000000000000000000000000000000000000000815260048101829052600360248201529091505f9073ffffffffffffffffffffffffffffffffffffffff8916906335fd631a906044015f60405180830381865afa158015610a84573d5f803e3d5ffd5b505050506040513d5f823e601f3d9081017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0168201604052610ac99190810190611059565b60208101516040820151606090920151909a919950975095505050505050565b5f805f610af584610dc9565b90505f610b03600183610fdd565b6040517f35fd631a00000000000000000000000000000000000000000000000000000000815260048101829052600260248201529091505f9073ffffffffffffffffffffffffffffffffffffffff8816906335fd631a9060440161098a565b5f805f805f610b7086610dc9565b6040517f1e2eaeaf000000000000000000000000000000000000000000000000000000008152600481018290529091505f9073ffffffffffffffffffffffffffffffffffffffff891690631e2eaeaf90602401602060405180830381865afa158015610bde573d5f803e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610c029190611015565b905073ffffffffffffffffffffffffffffffffffffffff811695508060a01c60020b945062ffffff8160b81c16935062ffffff8160d01c169250505092959194509250565b5f805f610c548585610e05565b6040517f1e2eaeaf000000000000000000000000000000000000000000000000000000008152600481018290529091505f9073ffffffffffffffffffffffffffffffffffffffff881690631e2eaeaf90602401602060405180830381865afa158015610cc2573d5f803e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610ce69190611015565b6fffffffffffffffffffffffffffffffff81169860809190911d975095505050505050565b5f80610d178484610e59565b6040517f1e2eaeaf0000000000000000000000000000000000000000000000000000000081526004810182905290915073ffffffffffffffffffffffffffffffffffffffff861690631e2eaeaf90602401602060405180830381865afa158015610d83573d5f803e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610da79190611015565b95945050505050565b5f80610dbb83610dc9565b90505f610d17600383610fdd565b6040515f90610de8908390600690602001918252602082015260400190565b604051602081830303815290604052805190602001209050919050565b5f80610e1084610dc9565b90505f610e1e600483610fdd565b60408051600287900b60208201529081018290529091506060015b604051602081830303815290604052805190602001209250505092915050565b5f80610e6484610dc9565b90505f610e72600683610fdd565b6040805160208101879052908101829052909150606001610e39565b5f8060408385031215610e9f575f80fd5b823591506020830135600181900b8114610eb7575f80fd5b809150509250929050565b8035600281900b8114610ed3575f80fd5b919050565b5f805f60608486031215610eea575f80fd5b83359250610efa60208501610ec2565b9150610f0860408501610ec2565b90509250925092565b5f805f805f60a08688031215610f25575f80fd5b85359450602086013573ffffffffffffffffffffffffffffffffffffffff81168114610f4f575f80fd5b9350610f5d60408701610ec2565b9250610f6b60608701610ec2565b949793965091946080013592915050565b5f8060408385031215610f8d575f80fd5b82359150610f9d60208401610ec2565b90509250929050565b5f8060408385031215610fb7575f80fd5b50508035926020909101359150565b5f60208284031215610fd6575f80fd5b5035919050565b80820180821115610370577f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f60208284031215611025575f80fd5b5051919050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b5f60208284031215611069575f80fd5b815167ffffffffffffffff81111561107f575f80fd5b8201601f8101841361108f575f80fd5b805167ffffffffffffffff8111156110a9576110a961102c565b8060051b6040517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0603f830116810181811067ffffffffffffffff821117156110f4576110f461102c565b604052918252602081840181019290810187841115611111575f80fd5b6020850194505b8385101561113457845180825260209586019590935001611118565b50969550505050505056fea2646970667358221220971f98e14cbd02081966516fbabc391e3ce3b60bb72adf272027a3f38994423764736f6c634300081a0033000000000000000000000000c021a7deb4a939fd7e661a0669fab5ac7ba2d5d6", + "nonce": "0x2", + "chainId": "0xaa36a7" + }, + "additionalContracts": [], + "isFixedGasLimit": false + } + ], + "receipts": [ + { + "status": "0x1", + "cumulativeGasUsed": "0x1b55e19", + "logs": [], + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "type": "0x2", + "transactionHash": "0xc101c6b9b2e5782c734e46af5af4a82b5cf3fc882417de0a84d915ace2d5d319", + "transactionIndex": "0x28", + "blockHash": "0x2019e9767cbfb5a4f556f04336703d7ecaef3557bcc39212fb1dd3458970f59a", + "blockNumber": "0x611a85", + "gasUsed": "0xf7e16", + "effectiveGasPrice": "0x1240761eb", + "from": "0xb7a249bdeff39727b5eb4c7ad458f682bae6adad", + "to": null, + "contractAddress": "0xc7a3b85d43ff66ad98a895de0eae4b9e24c932d7" + } + ], + "libraries": [], + "pending": [], + "returns": { + "state": { + "internal_type": "contract StateView", + "value": "0xc7A3b85D43fF66AD98A895dE0EaE4b9e24C932D7" + } + }, + "timestamp": 1721766499, + "chain": 11155111, + "commit": "e0aff22" +} \ No newline at end of file diff --git a/contracts/base/PeripheryPayments.sol b/contracts/base/PeripheryPayments.sol deleted file mode 100644 index 24466924..00000000 --- a/contracts/base/PeripheryPayments.sol +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; - -import {ERC20} from "solmate/tokens/ERC20.sol"; -import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; -import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; -import {IPeripheryPayments} from "../interfaces/IPeripheryPayments.sol"; - -abstract contract PeripheryPayments is IPeripheryPayments { - using CurrencyLibrary for Currency; - using SafeTransferLib for address; - using SafeTransferLib for ERC20; - - error InsufficientToken(); - error NativeTokenTransferFrom(); - - /// @inheritdoc IPeripheryPayments - function sweepToken(Currency currency, uint256 amountMinimum, address recipient) public payable override { - uint256 balanceCurrency = currency.balanceOfSelf(); - if (balanceCurrency < amountMinimum) revert InsufficientToken(); - - if (balanceCurrency > 0) { - currency.transfer(recipient, balanceCurrency); - } - } - - /// @param currency The currency to pay - /// @param payer The entity that must pay - /// @param recipient The entity that will receive payment - /// @param value The amount to pay - function pay(Currency currency, address payer, address recipient, uint256 value) internal { - if (payer == address(this)) { - // pay with tokens already in the contract (for the exact input multihop case) - currency.transfer(recipient, value); - } else { - if (currency.isNative()) revert NativeTokenTransferFrom(); - // pull payment - ERC20(Currency.unwrap(currency)).safeTransferFrom(payer, recipient, value); - } - } -} diff --git a/contracts/base/PeripheryValidation.sol b/contracts/base/PeripheryValidation.sol deleted file mode 100644 index b8ea81d4..00000000 --- a/contracts/base/PeripheryValidation.sol +++ /dev/null @@ -1,11 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; - -abstract contract PeripheryValidation { - error TransactionTooOld(); - - modifier checkDeadline(uint256 deadline) { - if (block.timestamp > deadline) revert TransactionTooOld(); - _; - } -} diff --git a/contracts/base/SelfPermit.sol b/contracts/base/SelfPermit.sol deleted file mode 100644 index 40449636..00000000 --- a/contracts/base/SelfPermit.sol +++ /dev/null @@ -1,52 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; - -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {IERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; - -import {IERC20PermitAllowed} from "../interfaces/external/IERC20PermitAllowed.sol"; -import {ISelfPermit} from "../interfaces/ISelfPermit.sol"; - -/// @title Self Permit -/// @notice Functionality to call permit on any EIP-2612-compliant token for use in the route -/// @dev These functions are expected to be embedded in multicalls to allow EOAs to approve a contract and call a function -/// that requires an approval in a single transaction. -abstract contract SelfPermit is ISelfPermit { - /// @inheritdoc ISelfPermit - function selfPermit(address token, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) - public - payable - override - { - IERC20Permit(token).permit(msg.sender, address(this), value, deadline, v, r, s); - } - - /// @inheritdoc ISelfPermit - function selfPermitIfNecessary(address token, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) - external - payable - override - { - if (IERC20(token).allowance(msg.sender, address(this)) < value) selfPermit(token, value, deadline, v, r, s); - } - - /// @inheritdoc ISelfPermit - function selfPermitAllowed(address token, uint256 nonce, uint256 expiry, uint8 v, bytes32 r, bytes32 s) - public - payable - override - { - IERC20PermitAllowed(token).permit(msg.sender, address(this), nonce, expiry, true, v, r, s); - } - - /// @inheritdoc ISelfPermit - function selfPermitAllowedIfNecessary(address token, uint256 nonce, uint256 expiry, uint8 v, bytes32 r, bytes32 s) - external - payable - override - { - if (IERC20(token).allowance(msg.sender, address(this)) < type(uint256).max) { - selfPermitAllowed(token, nonce, expiry, v, r, s); - } - } -} diff --git a/contracts/hooks/examples/FullRange.sol b/contracts/hooks/examples/FullRange.sol deleted file mode 100644 index 191593b8..00000000 --- a/contracts/hooks/examples/FullRange.sol +++ /dev/null @@ -1,368 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; - -import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; -import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; -import {BaseHook} from "../../BaseHook.sol"; -import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; -import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; -import {CurrencyLibrary, Currency} from "@uniswap/v4-core/src/types/Currency.sol"; -import {CurrencySettler} from "@uniswap/v4-core/test/utils/CurrencySettler.sol"; -import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; -import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; -import {IERC20Minimal} from "@uniswap/v4-core/src/interfaces/external/IERC20Minimal.sol"; -import {IUnlockCallback} from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol"; -import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; -import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; -import {FullMath} from "@uniswap/v4-core/src/libraries/FullMath.sol"; -import {UniswapV4ERC20} from "../../libraries/UniswapV4ERC20.sol"; -import {FixedPoint96} from "@uniswap/v4-core/src/libraries/FixedPoint96.sol"; -import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; -import {IERC20Metadata} from "@openzeppelin/contracts/interfaces/IERC20Metadata.sol"; -import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; -import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; -import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol"; - -import "../../libraries/LiquidityAmounts.sol"; - -contract FullRange is BaseHook { - using CurrencyLibrary for Currency; - using CurrencySettler for Currency; - using PoolIdLibrary for PoolKey; - using SafeCast for uint256; - using SafeCast for uint128; - using StateLibrary for IPoolManager; - - /// @notice Thrown when trying to interact with a non-initialized pool - error PoolNotInitialized(); - error TickSpacingNotDefault(); - error LiquidityDoesntMeetMinimum(); - error SenderMustBeHook(); - error ExpiredPastDeadline(); - error TooMuchSlippage(); - - bytes internal constant ZERO_BYTES = bytes(""); - - /// @dev Min tick for full range with tick spacing of 60 - int24 internal constant MIN_TICK = -887220; - /// @dev Max tick for full range with tick spacing of 60 - int24 internal constant MAX_TICK = -MIN_TICK; - - int256 internal constant MAX_INT = type(int256).max; - uint16 internal constant MINIMUM_LIQUIDITY = 1000; - - struct CallbackData { - address sender; - PoolKey key; - IPoolManager.ModifyLiquidityParams params; - } - - struct PoolInfo { - bool hasAccruedFees; - address liquidityToken; - } - - struct AddLiquidityParams { - Currency currency0; - Currency currency1; - uint24 fee; - uint256 amount0Desired; - uint256 amount1Desired; - uint256 amount0Min; - uint256 amount1Min; - address to; - uint256 deadline; - } - - struct RemoveLiquidityParams { - Currency currency0; - Currency currency1; - uint24 fee; - uint256 liquidity; - uint256 deadline; - } - - mapping(PoolId => PoolInfo) public poolInfo; - - constructor(IPoolManager _manager) BaseHook(_manager) {} - - modifier ensure(uint256 deadline) { - if (deadline < block.timestamp) revert ExpiredPastDeadline(); - _; - } - - function getHookPermissions() public pure override returns (Hooks.Permissions memory) { - return Hooks.Permissions({ - beforeInitialize: true, - afterInitialize: false, - beforeAddLiquidity: true, - beforeRemoveLiquidity: false, - afterAddLiquidity: false, - afterRemoveLiquidity: false, - beforeSwap: true, - afterSwap: false, - beforeDonate: false, - afterDonate: false, - beforeSwapReturnDelta: false, - afterSwapReturnDelta: false, - afterAddLiquidityReturnDelta: false, - afterRemoveLiquidityReturnDelta: false - }); - } - - function addLiquidity(AddLiquidityParams calldata params) - external - ensure(params.deadline) - returns (uint128 liquidity) - { - PoolKey memory key = PoolKey({ - currency0: params.currency0, - currency1: params.currency1, - fee: params.fee, - tickSpacing: 60, - hooks: IHooks(address(this)) - }); - - PoolId poolId = key.toId(); - - (uint160 sqrtPriceX96,,,) = manager.getSlot0(poolId); - - if (sqrtPriceX96 == 0) revert PoolNotInitialized(); - - PoolInfo storage pool = poolInfo[poolId]; - - uint128 poolLiquidity = manager.getLiquidity(poolId); - - liquidity = LiquidityAmounts.getLiquidityForAmounts( - sqrtPriceX96, - TickMath.getSqrtPriceAtTick(MIN_TICK), - TickMath.getSqrtPriceAtTick(MAX_TICK), - params.amount0Desired, - params.amount1Desired - ); - - if (poolLiquidity == 0 && liquidity <= MINIMUM_LIQUIDITY) { - revert LiquidityDoesntMeetMinimum(); - } - BalanceDelta addedDelta = modifyLiquidity( - key, - IPoolManager.ModifyLiquidityParams({ - tickLower: MIN_TICK, - tickUpper: MAX_TICK, - liquidityDelta: liquidity.toInt256(), - salt: 0 - }) - ); - - if (poolLiquidity == 0) { - // permanently lock the first MINIMUM_LIQUIDITY tokens - liquidity -= MINIMUM_LIQUIDITY; - UniswapV4ERC20(pool.liquidityToken).mint(address(0), MINIMUM_LIQUIDITY); - } - - UniswapV4ERC20(pool.liquidityToken).mint(params.to, liquidity); - - if (uint128(-addedDelta.amount0()) < params.amount0Min || uint128(-addedDelta.amount1()) < params.amount1Min) { - revert TooMuchSlippage(); - } - } - - function removeLiquidity(RemoveLiquidityParams calldata params) - public - virtual - ensure(params.deadline) - returns (BalanceDelta delta) - { - PoolKey memory key = PoolKey({ - currency0: params.currency0, - currency1: params.currency1, - fee: params.fee, - tickSpacing: 60, - hooks: IHooks(address(this)) - }); - - PoolId poolId = key.toId(); - - (uint160 sqrtPriceX96,,,) = manager.getSlot0(poolId); - - if (sqrtPriceX96 == 0) revert PoolNotInitialized(); - - UniswapV4ERC20 erc20 = UniswapV4ERC20(poolInfo[poolId].liquidityToken); - - delta = modifyLiquidity( - key, - IPoolManager.ModifyLiquidityParams({ - tickLower: MIN_TICK, - tickUpper: MAX_TICK, - liquidityDelta: -(params.liquidity.toInt256()), - salt: 0 - }) - ); - - erc20.burn(msg.sender, params.liquidity); - } - - function beforeInitialize(address, PoolKey calldata key, uint160, bytes calldata) - external - override - returns (bytes4) - { - if (key.tickSpacing != 60) revert TickSpacingNotDefault(); - - PoolId poolId = key.toId(); - - string memory tokenSymbol = string( - abi.encodePacked( - "UniV4", - "-", - IERC20Metadata(Currency.unwrap(key.currency0)).symbol(), - "-", - IERC20Metadata(Currency.unwrap(key.currency1)).symbol(), - "-", - Strings.toString(uint256(key.fee)) - ) - ); - address poolToken = address(new UniswapV4ERC20(tokenSymbol, tokenSymbol)); - - poolInfo[poolId] = PoolInfo({hasAccruedFees: false, liquidityToken: poolToken}); - - return FullRange.beforeInitialize.selector; - } - - function beforeAddLiquidity( - address sender, - PoolKey calldata, - IPoolManager.ModifyLiquidityParams calldata, - bytes calldata - ) external view override returns (bytes4) { - if (sender != address(this)) revert SenderMustBeHook(); - - return FullRange.beforeAddLiquidity.selector; - } - - function beforeSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata, bytes calldata) - external - override - returns (bytes4, BeforeSwapDelta, uint24) - { - PoolId poolId = key.toId(); - - if (!poolInfo[poolId].hasAccruedFees) { - PoolInfo storage pool = poolInfo[poolId]; - pool.hasAccruedFees = true; - } - - return (IHooks.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0); - } - - function modifyLiquidity(PoolKey memory key, IPoolManager.ModifyLiquidityParams memory params) - internal - returns (BalanceDelta delta) - { - delta = abi.decode(manager.unlock(abi.encode(CallbackData(msg.sender, key, params))), (BalanceDelta)); - } - - function _settleDeltas(address sender, PoolKey memory key, BalanceDelta delta) internal { - key.currency0.settle(manager, sender, uint256(int256(-delta.amount0())), false); - key.currency1.settle(manager, sender, uint256(int256(-delta.amount1())), false); - } - - function _takeDeltas(address sender, PoolKey memory key, BalanceDelta delta) internal { - manager.take(key.currency0, sender, uint256(uint128(delta.amount0()))); - manager.take(key.currency1, sender, uint256(uint128(delta.amount1()))); - } - - function _removeLiquidity(PoolKey memory key, IPoolManager.ModifyLiquidityParams memory params) - internal - returns (BalanceDelta delta) - { - PoolId poolId = key.toId(); - PoolInfo storage pool = poolInfo[poolId]; - - if (pool.hasAccruedFees) { - _rebalance(key); - } - - uint256 liquidityToRemove = FullMath.mulDiv( - uint256(-params.liquidityDelta), - manager.getLiquidity(poolId), - UniswapV4ERC20(pool.liquidityToken).totalSupply() - ); - - params.liquidityDelta = -(liquidityToRemove.toInt256()); - (delta,) = manager.modifyLiquidity(key, params, ZERO_BYTES); - pool.hasAccruedFees = false; - } - - function _unlockCallback(bytes calldata rawData) internal override returns (bytes memory) { - CallbackData memory data = abi.decode(rawData, (CallbackData)); - BalanceDelta delta; - - if (data.params.liquidityDelta < 0) { - delta = _removeLiquidity(data.key, data.params); - _takeDeltas(data.sender, data.key, delta); - } else { - (delta,) = manager.modifyLiquidity(data.key, data.params, ZERO_BYTES); - _settleDeltas(data.sender, data.key, delta); - } - return abi.encode(delta); - } - - function _rebalance(PoolKey memory key) public { - PoolId poolId = key.toId(); - (BalanceDelta balanceDelta,) = manager.modifyLiquidity( - key, - IPoolManager.ModifyLiquidityParams({ - tickLower: MIN_TICK, - tickUpper: MAX_TICK, - liquidityDelta: -(manager.getLiquidity(poolId).toInt256()), - salt: 0 - }), - ZERO_BYTES - ); - - uint160 newSqrtPriceX96 = ( - FixedPointMathLib.sqrt( - FullMath.mulDiv(uint128(balanceDelta.amount1()), FixedPoint96.Q96, uint128(balanceDelta.amount0())) - ) * FixedPointMathLib.sqrt(FixedPoint96.Q96) - ).toUint160(); - - (uint160 sqrtPriceX96,,,) = manager.getSlot0(poolId); - - manager.swap( - key, - IPoolManager.SwapParams({ - zeroForOne: newSqrtPriceX96 < sqrtPriceX96, - amountSpecified: -MAX_INT - 1, // equivalent of type(int256).min - sqrtPriceLimitX96: newSqrtPriceX96 - }), - ZERO_BYTES - ); - - uint128 liquidity = LiquidityAmounts.getLiquidityForAmounts( - newSqrtPriceX96, - TickMath.getSqrtPriceAtTick(MIN_TICK), - TickMath.getSqrtPriceAtTick(MAX_TICK), - uint256(uint128(balanceDelta.amount0())), - uint256(uint128(balanceDelta.amount1())) - ); - - (BalanceDelta balanceDeltaAfter,) = manager.modifyLiquidity( - key, - IPoolManager.ModifyLiquidityParams({ - tickLower: MIN_TICK, - tickUpper: MAX_TICK, - liquidityDelta: liquidity.toInt256(), - salt: 0 - }), - ZERO_BYTES - ); - - // Donate any "dust" from the sqrtRatio change as fees - uint128 donateAmount0 = uint128(balanceDelta.amount0() + balanceDeltaAfter.amount0()); - uint128 donateAmount1 = uint128(balanceDelta.amount1() + balanceDeltaAfter.amount1()); - - manager.donate(key, donateAmount0, donateAmount1, ZERO_BYTES); - } -} diff --git a/contracts/hooks/examples/GeomeanOracle.sol b/contracts/hooks/examples/GeomeanOracle.sol deleted file mode 100644 index df5a9ad1..00000000 --- a/contracts/hooks/examples/GeomeanOracle.sol +++ /dev/null @@ -1,186 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; - -import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; -import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; -import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; -import {Oracle} from "../../libraries/Oracle.sol"; -import {BaseHook} from "../../BaseHook.sol"; -import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; -import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; -import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol"; - -/// @notice A hook for a pool that allows a Uniswap pool to act as an oracle. Pools that use this hook must have full range -/// tick spacing and liquidity is always permanently locked in these pools. This is the suggested configuration -/// for protocols that wish to use a V3 style geomean oracle. -contract GeomeanOracle is BaseHook { - using Oracle for Oracle.Observation[65535]; - using PoolIdLibrary for PoolKey; - using StateLibrary for IPoolManager; - - /// @notice Oracle pools do not have fees because they exist to serve as an oracle for a pair of tokens - error OnlyOneOraclePoolAllowed(); - - /// @notice Oracle positions must be full range - error OraclePositionsMustBeFullRange(); - - /// @notice Oracle pools must have liquidity locked so that they cannot become more susceptible to price manipulation - error OraclePoolMustLockLiquidity(); - - /// @member index The index of the last written observation for the pool - /// @member cardinality The cardinality of the observations array for the pool - /// @member cardinalityNext The cardinality target of the observations array for the pool, which will replace cardinality when enough observations are written - struct ObservationState { - uint16 index; - uint16 cardinality; - uint16 cardinalityNext; - } - - /// @notice The list of observations for a given pool ID - mapping(PoolId => Oracle.Observation[65535]) public observations; - /// @notice The current observation array state for the given pool ID - mapping(PoolId => ObservationState) public states; - - /// @notice Returns the observation for the given pool key and observation index - function getObservation(PoolKey calldata key, uint256 index) - external - view - returns (Oracle.Observation memory observation) - { - observation = observations[PoolId.wrap(keccak256(abi.encode(key)))][index]; - } - - /// @notice Returns the state for the given pool key - function getState(PoolKey calldata key) external view returns (ObservationState memory state) { - state = states[PoolId.wrap(keccak256(abi.encode(key)))]; - } - - /// @dev For mocking - function _blockTimestamp() internal view virtual returns (uint32) { - return uint32(block.timestamp); - } - - constructor(IPoolManager _manager) BaseHook(_manager) {} - - function getHookPermissions() public pure override returns (Hooks.Permissions memory) { - return Hooks.Permissions({ - beforeInitialize: true, - afterInitialize: true, - beforeAddLiquidity: true, - beforeRemoveLiquidity: true, - afterAddLiquidity: false, - afterRemoveLiquidity: false, - beforeSwap: true, - afterSwap: false, - beforeDonate: false, - afterDonate: false, - beforeSwapReturnDelta: false, - afterSwapReturnDelta: false, - afterAddLiquidityReturnDelta: false, - afterRemoveLiquidityReturnDelta: false - }); - } - - function beforeInitialize(address, PoolKey calldata key, uint160, bytes calldata) - external - view - override - onlyByManager - returns (bytes4) - { - // This is to limit the fragmentation of pools using this oracle hook. In other words, - // there may only be one pool per pair of tokens that use this hook. The tick spacing is set to the maximum - // because we only allow max range liquidity in this pool. - if (key.fee != 0 || key.tickSpacing != manager.MAX_TICK_SPACING()) revert OnlyOneOraclePoolAllowed(); - return GeomeanOracle.beforeInitialize.selector; - } - - function afterInitialize(address, PoolKey calldata key, uint160, int24, bytes calldata) - external - override - onlyByManager - returns (bytes4) - { - PoolId id = key.toId(); - (states[id].cardinality, states[id].cardinalityNext) = observations[id].initialize(_blockTimestamp()); - return GeomeanOracle.afterInitialize.selector; - } - - /// @dev Called before any action that potentially modifies pool price or liquidity, such as swap or modify position - function _updatePool(PoolKey calldata key) private { - PoolId id = key.toId(); - (, int24 tick,,) = manager.getSlot0(id); - - uint128 liquidity = manager.getLiquidity(id); - - (states[id].index, states[id].cardinality) = observations[id].write( - states[id].index, _blockTimestamp(), tick, liquidity, states[id].cardinality, states[id].cardinalityNext - ); - } - - function beforeAddLiquidity( - address, - PoolKey calldata key, - IPoolManager.ModifyLiquidityParams calldata params, - bytes calldata - ) external override onlyByManager returns (bytes4) { - int24 maxTickSpacing = manager.MAX_TICK_SPACING(); - if ( - params.tickLower != TickMath.minUsableTick(maxTickSpacing) - || params.tickUpper != TickMath.maxUsableTick(maxTickSpacing) - ) revert OraclePositionsMustBeFullRange(); - _updatePool(key); - return GeomeanOracle.beforeAddLiquidity.selector; - } - - function beforeRemoveLiquidity( - address, - PoolKey calldata, - IPoolManager.ModifyLiquidityParams calldata, - bytes calldata - ) external view override onlyByManager returns (bytes4) { - revert OraclePoolMustLockLiquidity(); - } - - function beforeSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata, bytes calldata) - external - override - onlyByManager - returns (bytes4, BeforeSwapDelta, uint24) - { - _updatePool(key); - return (GeomeanOracle.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0); - } - - /// @notice Observe the given pool for the timestamps - function observe(PoolKey calldata key, uint32[] calldata secondsAgos) - external - view - returns (int56[] memory tickCumulatives, uint160[] memory secondsPerLiquidityCumulativeX128s) - { - PoolId id = key.toId(); - - ObservationState memory state = states[id]; - - (, int24 tick,,) = manager.getSlot0(id); - - uint128 liquidity = manager.getLiquidity(id); - - return observations[id].observe(_blockTimestamp(), secondsAgos, tick, state.index, liquidity, state.cardinality); - } - - /// @notice Increase the cardinality target for the given pool - function increaseCardinalityNext(PoolKey calldata key, uint16 cardinalityNext) - external - returns (uint16 cardinalityNextOld, uint16 cardinalityNextNew) - { - PoolId id = PoolId.wrap(keccak256(abi.encode(key))); - - ObservationState storage state = states[id]; - - cardinalityNextOld = state.cardinalityNext; - cardinalityNextNew = observations[id].grow(cardinalityNextOld, cardinalityNext); - state.cardinalityNext = cardinalityNextNew; - } -} diff --git a/contracts/hooks/examples/LimitOrder.sol b/contracts/hooks/examples/LimitOrder.sol deleted file mode 100644 index 2a8ca909..00000000 --- a/contracts/hooks/examples/LimitOrder.sol +++ /dev/null @@ -1,418 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; - -import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; -import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; -import {FullMath} from "@uniswap/v4-core/src/libraries/FullMath.sol"; -import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; -import {IERC20Minimal} from "@uniswap/v4-core/src/interfaces/external/IERC20Minimal.sol"; -import {IERC1155Receiver} from "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; -import {BaseHook} from "../../BaseHook.sol"; -import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; -import {CurrencySettler} from "@uniswap/v4-core/test/utils/CurrencySettler.sol"; -import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; -import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; -import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; - -type Epoch is uint232; - -library EpochLibrary { - function equals(Epoch a, Epoch b) internal pure returns (bool) { - return Epoch.unwrap(a) == Epoch.unwrap(b); - } - - function unsafeIncrement(Epoch a) internal pure returns (Epoch) { - unchecked { - return Epoch.wrap(Epoch.unwrap(a) + 1); - } - } -} - -contract LimitOrder is BaseHook { - using EpochLibrary for Epoch; - using PoolIdLibrary for PoolKey; - using CurrencyLibrary for Currency; - using CurrencySettler for Currency; - using StateLibrary for IPoolManager; - - error ZeroLiquidity(); - error InRange(); - error CrossedRange(); - error Filled(); - error NotFilled(); - error NotPoolManagerToken(); - - event Place( - address indexed owner, Epoch indexed epoch, PoolKey key, int24 tickLower, bool zeroForOne, uint128 liquidity - ); - - event Fill(Epoch indexed epoch, PoolKey key, int24 tickLower, bool zeroForOne); - - event Kill( - address indexed owner, Epoch indexed epoch, PoolKey key, int24 tickLower, bool zeroForOne, uint128 liquidity - ); - - event Withdraw(address indexed owner, Epoch indexed epoch, uint128 liquidity); - - bytes internal constant ZERO_BYTES = bytes(""); - - Epoch private constant EPOCH_DEFAULT = Epoch.wrap(0); - - mapping(PoolId => int24) public tickLowerLasts; - Epoch public epochNext = Epoch.wrap(1); - - struct EpochInfo { - bool filled; - Currency currency0; - Currency currency1; - uint256 token0Total; - uint256 token1Total; - uint128 liquidityTotal; - mapping(address => uint128) liquidity; - } - - mapping(bytes32 => Epoch) public epochs; - mapping(Epoch => EpochInfo) public epochInfos; - - constructor(IPoolManager _manager) BaseHook(_manager) {} - - function getHookPermissions() public pure override returns (Hooks.Permissions memory) { - return Hooks.Permissions({ - beforeInitialize: false, - afterInitialize: true, - beforeAddLiquidity: false, - beforeRemoveLiquidity: false, - afterAddLiquidity: false, - afterRemoveLiquidity: false, - beforeSwap: false, - afterSwap: true, - beforeDonate: false, - afterDonate: false, - beforeSwapReturnDelta: false, - afterSwapReturnDelta: false, - afterAddLiquidityReturnDelta: false, - afterRemoveLiquidityReturnDelta: false - }); - } - - function getTickLowerLast(PoolId poolId) public view returns (int24) { - return tickLowerLasts[poolId]; - } - - function setTickLowerLast(PoolId poolId, int24 tickLower) private { - tickLowerLasts[poolId] = tickLower; - } - - function getEpoch(PoolKey memory key, int24 tickLower, bool zeroForOne) public view returns (Epoch) { - return epochs[keccak256(abi.encode(key, tickLower, zeroForOne))]; - } - - function setEpoch(PoolKey memory key, int24 tickLower, bool zeroForOne, Epoch epoch) private { - epochs[keccak256(abi.encode(key, tickLower, zeroForOne))] = epoch; - } - - function getEpochLiquidity(Epoch epoch, address owner) external view returns (uint256) { - return epochInfos[epoch].liquidity[owner]; - } - - function getTick(PoolId poolId) private view returns (int24 tick) { - (, tick,,) = manager.getSlot0(poolId); - } - - function getTickLower(int24 tick, int24 tickSpacing) private pure returns (int24) { - int24 compressed = tick / tickSpacing; - if (tick < 0 && tick % tickSpacing != 0) compressed--; // round towards negative infinity - return compressed * tickSpacing; - } - - function afterInitialize(address, PoolKey calldata key, uint160, int24 tick, bytes calldata) - external - override - onlyByManager - returns (bytes4) - { - setTickLowerLast(key.toId(), getTickLower(tick, key.tickSpacing)); - return LimitOrder.afterInitialize.selector; - } - - function afterSwap( - address, - PoolKey calldata key, - IPoolManager.SwapParams calldata params, - BalanceDelta, - bytes calldata - ) external override onlyByManager returns (bytes4, int128) { - (int24 tickLower, int24 lower, int24 upper) = _getCrossedTicks(key.toId(), key.tickSpacing); - if (lower > upper) return (LimitOrder.afterSwap.selector, 0); - - // note that a zeroForOne swap means that the pool is actually gaining token0, so limit - // order fills are the opposite of swap fills, hence the inversion below - bool zeroForOne = !params.zeroForOne; - for (; lower <= upper; lower += key.tickSpacing) { - _fillEpoch(key, lower, zeroForOne); - } - - setTickLowerLast(key.toId(), tickLower); - return (LimitOrder.afterSwap.selector, 0); - } - - function _fillEpoch(PoolKey calldata key, int24 lower, bool zeroForOne) internal { - Epoch epoch = getEpoch(key, lower, zeroForOne); - if (!epoch.equals(EPOCH_DEFAULT)) { - EpochInfo storage epochInfo = epochInfos[epoch]; - - epochInfo.filled = true; - - (uint256 amount0, uint256 amount1) = - _unlockCallbackFill(key, lower, -int256(uint256(epochInfo.liquidityTotal))); - - unchecked { - epochInfo.token0Total += amount0; - epochInfo.token1Total += amount1; - } - - setEpoch(key, lower, zeroForOne, EPOCH_DEFAULT); - - emit Fill(epoch, key, lower, zeroForOne); - } - } - - function _getCrossedTicks(PoolId poolId, int24 tickSpacing) - internal - view - returns (int24 tickLower, int24 lower, int24 upper) - { - tickLower = getTickLower(getTick(poolId), tickSpacing); - int24 tickLowerLast = getTickLowerLast(poolId); - - if (tickLower < tickLowerLast) { - lower = tickLower + tickSpacing; - upper = tickLowerLast; - } else { - lower = tickLowerLast; - upper = tickLower - tickSpacing; - } - } - - function _unlockCallbackFill(PoolKey calldata key, int24 tickLower, int256 liquidityDelta) - private - onlyByManager - returns (uint128 amount0, uint128 amount1) - { - (BalanceDelta delta,) = manager.modifyLiquidity( - key, - IPoolManager.ModifyLiquidityParams({ - tickLower: tickLower, - tickUpper: tickLower + key.tickSpacing, - liquidityDelta: liquidityDelta, - salt: 0 - }), - ZERO_BYTES - ); - - if (delta.amount0() > 0) { - manager.mint(address(this), key.currency0.toId(), amount0 = uint128(delta.amount0())); - } - if (delta.amount1() > 0) { - manager.mint(address(this), key.currency1.toId(), amount1 = uint128(delta.amount1())); - } - } - - function place(PoolKey calldata key, int24 tickLower, bool zeroForOne, uint128 liquidity) - external - onlyValidPools(key.hooks) - { - if (liquidity == 0) revert ZeroLiquidity(); - - manager.unlock( - abi.encodeCall( - this.unlockCallbackPlace, (key, tickLower, zeroForOne, int256(uint256(liquidity)), msg.sender) - ) - ); - - EpochInfo storage epochInfo; - Epoch epoch = getEpoch(key, tickLower, zeroForOne); - if (epoch.equals(EPOCH_DEFAULT)) { - unchecked { - setEpoch(key, tickLower, zeroForOne, epoch = epochNext); - // since epoch was just assigned the current value of epochNext, - // this is equivalent to epochNext++, which is what's intended, - // and it saves an SLOAD - epochNext = epoch.unsafeIncrement(); - } - epochInfo = epochInfos[epoch]; - epochInfo.currency0 = key.currency0; - epochInfo.currency1 = key.currency1; - } else { - epochInfo = epochInfos[epoch]; - } - - unchecked { - epochInfo.liquidityTotal += liquidity; - epochInfo.liquidity[msg.sender] += liquidity; - } - - emit Place(msg.sender, epoch, key, tickLower, zeroForOne, liquidity); - } - - function unlockCallbackPlace( - PoolKey calldata key, - int24 tickLower, - bool zeroForOne, - int256 liquidityDelta, - address owner - ) external selfOnly { - (BalanceDelta delta,) = manager.modifyLiquidity( - key, - IPoolManager.ModifyLiquidityParams({ - tickLower: tickLower, - tickUpper: tickLower + key.tickSpacing, - liquidityDelta: liquidityDelta, - salt: 0 - }), - ZERO_BYTES - ); - - if (delta.amount0() < 0) { - if (delta.amount1() != 0) revert InRange(); - if (!zeroForOne) revert CrossedRange(); - key.currency0.settle(manager, owner, uint256(uint128(-delta.amount0())), false); - } else { - if (delta.amount0() != 0) revert InRange(); - if (zeroForOne) revert CrossedRange(); - key.currency1.settle(manager, owner, uint256(uint128(-delta.amount1())), false); - } - } - - function kill(PoolKey calldata key, int24 tickLower, bool zeroForOne, address to) external { - Epoch epoch = getEpoch(key, tickLower, zeroForOne); - EpochInfo storage epochInfo = epochInfos[epoch]; - - if (epochInfo.filled) revert Filled(); - - uint128 liquidity = epochInfo.liquidity[msg.sender]; - if (liquidity == 0) revert ZeroLiquidity(); - delete epochInfo.liquidity[msg.sender]; - - uint256 amount0Fee; - uint256 amount1Fee; - (amount0Fee, amount1Fee) = abi.decode( - manager.unlock( - abi.encodeCall( - this.unlockCallbackKill, - (key, tickLower, -int256(uint256(liquidity)), to, liquidity == epochInfo.liquidityTotal) - ) - ), - (uint256, uint256) - ); - epochInfo.liquidityTotal -= liquidity; - unchecked { - epochInfo.token0Total += amount0Fee; - epochInfo.token1Total += amount1Fee; - } - - emit Kill(msg.sender, epoch, key, tickLower, zeroForOne, liquidity); - } - - function unlockCallbackKill( - PoolKey calldata key, - int24 tickLower, - int256 liquidityDelta, - address to, - bool removingAllLiquidity - ) external selfOnly returns (uint128 amount0Fee, uint128 amount1Fee) { - int24 tickUpper = tickLower + key.tickSpacing; - - // because `modifyPosition` includes not just principal value but also fees, we cannot allocate - // the proceeds pro-rata. if we were to do so, users who have been in a limit order that's partially filled - // could be unfairly diluted by a user sychronously placing then killing a limit order to skim off fees. - // to prevent this, we allocate all fee revenue to remaining limit order placers, unless this is the last order. - if (!removingAllLiquidity) { - (, BalanceDelta deltaFee) = manager.modifyLiquidity( - key, - IPoolManager.ModifyLiquidityParams({ - tickLower: tickLower, - tickUpper: tickUpper, - liquidityDelta: 0, - salt: 0 - }), - ZERO_BYTES - ); - - if (deltaFee.amount0() > 0) { - manager.mint(address(this), key.currency0.toId(), amount0Fee = uint128(deltaFee.amount0())); - } - if (deltaFee.amount1() > 0) { - manager.mint(address(this), key.currency1.toId(), amount1Fee = uint128(deltaFee.amount1())); - } - } - - (BalanceDelta delta,) = manager.modifyLiquidity( - key, - IPoolManager.ModifyLiquidityParams({ - tickLower: tickLower, - tickUpper: tickUpper, - liquidityDelta: liquidityDelta, - salt: 0 - }), - ZERO_BYTES - ); - - if (delta.amount0() > 0) { - key.currency0.take(manager, to, uint256(uint128(delta.amount0())), false); - } - if (delta.amount1() > 0) { - key.currency1.take(manager, to, uint256(uint128(delta.amount1())), false); - } - } - - function withdraw(Epoch epoch, address to) external returns (uint256 amount0, uint256 amount1) { - EpochInfo storage epochInfo = epochInfos[epoch]; - - if (!epochInfo.filled) revert NotFilled(); - - uint128 liquidity = epochInfo.liquidity[msg.sender]; - if (liquidity == 0) revert ZeroLiquidity(); - delete epochInfo.liquidity[msg.sender]; - - uint128 liquidityTotal = epochInfo.liquidityTotal; - - amount0 = FullMath.mulDiv(epochInfo.token0Total, liquidity, liquidityTotal); - amount1 = FullMath.mulDiv(epochInfo.token1Total, liquidity, liquidityTotal); - - epochInfo.token0Total -= amount0; - epochInfo.token1Total -= amount1; - epochInfo.liquidityTotal = liquidityTotal - liquidity; - - manager.unlock( - abi.encodeCall( - this.unlockCallbackWithdraw, (epochInfo.currency0, epochInfo.currency1, amount0, amount1, to) - ) - ); - - emit Withdraw(msg.sender, epoch, liquidity); - } - - function unlockCallbackWithdraw( - Currency currency0, - Currency currency1, - uint256 token0Amount, - uint256 token1Amount, - address to - ) external selfOnly { - if (token0Amount > 0) { - manager.burn(address(this), currency0.toId(), token0Amount); - manager.take(currency0, to, token0Amount); - } - if (token1Amount > 0) { - manager.burn(address(this), currency1.toId(), token1Amount); - manager.take(currency1, to, token1Amount); - } - } - - function onERC1155Received(address, address, uint256, uint256, bytes calldata) external view returns (bytes4) { - if (msg.sender != address(manager)) revert NotPoolManagerToken(); - return IERC1155Receiver.onERC1155Received.selector; - } -} diff --git a/contracts/hooks/examples/TWAMM.sol b/contracts/hooks/examples/TWAMM.sol deleted file mode 100644 index dc1f3b00..00000000 --- a/contracts/hooks/examples/TWAMM.sol +++ /dev/null @@ -1,654 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.15; - -import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; -import {TickBitmap} from "@uniswap/v4-core/src/libraries/TickBitmap.sol"; -import {SqrtPriceMath} from "@uniswap/v4-core/src/libraries/SqrtPriceMath.sol"; -import {FixedPoint96} from "@uniswap/v4-core/src/libraries/FixedPoint96.sol"; -import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; -import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; -import {BaseHook} from "../../BaseHook.sol"; -import {IERC20Minimal} from "@uniswap/v4-core/src/interfaces/external/IERC20Minimal.sol"; -import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import {ITWAMM} from "../../interfaces/ITWAMM.sol"; -import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; -import {TransferHelper} from "../../libraries/TransferHelper.sol"; -import {TwammMath} from "../../libraries/TWAMM/TwammMath.sol"; -import {OrderPool} from "../../libraries/TWAMM/OrderPool.sol"; -import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; -import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; -import {PoolGetters} from "../../libraries/PoolGetters.sol"; -import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; -import {CurrencySettler} from "@uniswap/v4-core/test/utils/CurrencySettler.sol"; -import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; -import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol"; - -contract TWAMM is BaseHook, ITWAMM { - using TransferHelper for IERC20Minimal; - using CurrencyLibrary for Currency; - using CurrencySettler for Currency; - using OrderPool for OrderPool.State; - using PoolIdLibrary for PoolKey; - using TickMath for int24; - using TickMath for uint160; - using SafeCast for uint256; - using PoolGetters for IPoolManager; - using TickBitmap for mapping(int16 => uint256); - using StateLibrary for IPoolManager; - - bytes internal constant ZERO_BYTES = bytes(""); - - int256 internal constant MIN_DELTA = -1; - bool internal constant ZERO_FOR_ONE = true; - bool internal constant ONE_FOR_ZERO = false; - - /// @notice Contains full state related to the TWAMM - /// @member lastVirtualOrderTimestamp Last timestamp in which virtual orders were executed - /// @member orderPool0For1 Order pool trading token0 for token1 of pool - /// @member orderPool1For0 Order pool trading token1 for token0 of pool - /// @member orders Mapping of orderId to individual orders on pool - struct State { - uint256 lastVirtualOrderTimestamp; - OrderPool.State orderPool0For1; - OrderPool.State orderPool1For0; - mapping(bytes32 => Order) orders; - } - - /// @inheritdoc ITWAMM - uint256 public immutable expirationInterval; - // twammStates[poolId] => Twamm.State - mapping(PoolId => State) internal twammStates; - // tokensOwed[token][owner] => amountOwed - mapping(Currency => mapping(address => uint256)) public tokensOwed; - - constructor(IPoolManager _manager, uint256 _expirationInterval) BaseHook(_manager) { - expirationInterval = _expirationInterval; - } - - function getHookPermissions() public pure override returns (Hooks.Permissions memory) { - return Hooks.Permissions({ - beforeInitialize: true, - afterInitialize: false, - beforeAddLiquidity: true, - beforeRemoveLiquidity: false, - afterAddLiquidity: false, - afterRemoveLiquidity: false, - beforeSwap: true, - afterSwap: false, - beforeDonate: false, - afterDonate: false, - beforeSwapReturnDelta: false, - afterSwapReturnDelta: false, - afterAddLiquidityReturnDelta: false, - afterRemoveLiquidityReturnDelta: false - }); - } - - function beforeInitialize(address, PoolKey calldata key, uint160, bytes calldata) - external - virtual - override - onlyByManager - returns (bytes4) - { - // one-time initialization enforced in PoolManager - initialize(_getTWAMM(key)); - return BaseHook.beforeInitialize.selector; - } - - function beforeAddLiquidity( - address, - PoolKey calldata key, - IPoolManager.ModifyLiquidityParams calldata, - bytes calldata - ) external override onlyByManager returns (bytes4) { - executeTWAMMOrders(key); - return BaseHook.beforeAddLiquidity.selector; - } - - function beforeSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata, bytes calldata) - external - override - onlyByManager - returns (bytes4, BeforeSwapDelta, uint24) - { - executeTWAMMOrders(key); - return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0); - } - - function lastVirtualOrderTimestamp(PoolId key) external view returns (uint256) { - return twammStates[key].lastVirtualOrderTimestamp; - } - - function getOrder(PoolKey calldata poolKey, OrderKey calldata orderKey) external view returns (Order memory) { - return _getOrder(twammStates[PoolId.wrap(keccak256(abi.encode(poolKey)))], orderKey); - } - - function getOrderPool(PoolKey calldata key, bool zeroForOne) - external - view - returns (uint256 sellRateCurrent, uint256 earningsFactorCurrent) - { - State storage twamm = _getTWAMM(key); - return zeroForOne - ? (twamm.orderPool0For1.sellRateCurrent, twamm.orderPool0For1.earningsFactorCurrent) - : (twamm.orderPool1For0.sellRateCurrent, twamm.orderPool1For0.earningsFactorCurrent); - } - - /// @notice Initialize TWAMM state - function initialize(State storage self) internal { - self.lastVirtualOrderTimestamp = block.timestamp; - } - - /// @inheritdoc ITWAMM - function executeTWAMMOrders(PoolKey memory key) public { - PoolId poolId = key.toId(); - (uint160 sqrtPriceX96,,,) = manager.getSlot0(poolId); - State storage twamm = twammStates[poolId]; - - (bool zeroForOne, uint160 sqrtPriceLimitX96) = - _executeTWAMMOrders(twamm, manager, key, PoolParamsOnExecute(sqrtPriceX96, manager.getLiquidity(poolId))); - - if (sqrtPriceLimitX96 != 0 && sqrtPriceLimitX96 != sqrtPriceX96) { - manager.unlock(abi.encode(key, IPoolManager.SwapParams(zeroForOne, type(int256).max, sqrtPriceLimitX96))); - } - } - - /// @inheritdoc ITWAMM - function submitOrder(PoolKey calldata key, OrderKey memory orderKey, uint256 amountIn) - external - returns (bytes32 orderId) - { - PoolId poolId = PoolId.wrap(keccak256(abi.encode(key))); - State storage twamm = twammStates[poolId]; - executeTWAMMOrders(key); - - uint256 sellRate; - unchecked { - // checks done in TWAMM library - uint256 duration = orderKey.expiration - block.timestamp; - sellRate = amountIn / duration; - orderId = _submitOrder(twamm, orderKey, sellRate); - IERC20Minimal(orderKey.zeroForOne ? Currency.unwrap(key.currency0) : Currency.unwrap(key.currency1)) - .safeTransferFrom(msg.sender, address(this), sellRate * duration); - } - - emit SubmitOrder( - poolId, - orderKey.owner, - orderKey.expiration, - orderKey.zeroForOne, - sellRate, - _getOrder(twamm, orderKey).earningsFactorLast - ); - } - - /// @notice Submits a new long term order into the TWAMM - /// @dev executeTWAMMOrders must be executed up to current timestamp before calling submitOrder - /// @param orderKey The OrderKey for the new order - function _submitOrder(State storage self, OrderKey memory orderKey, uint256 sellRate) - internal - returns (bytes32 orderId) - { - if (orderKey.owner != msg.sender) revert MustBeOwner(orderKey.owner, msg.sender); - if (self.lastVirtualOrderTimestamp == 0) revert NotInitialized(); - if (orderKey.expiration <= block.timestamp) revert ExpirationLessThanBlocktime(orderKey.expiration); - if (sellRate == 0) revert SellRateCannotBeZero(); - if (orderKey.expiration % expirationInterval != 0) revert ExpirationNotOnInterval(orderKey.expiration); - - orderId = _orderId(orderKey); - if (self.orders[orderId].sellRate != 0) revert OrderAlreadyExists(orderKey); - - OrderPool.State storage orderPool = orderKey.zeroForOne ? self.orderPool0For1 : self.orderPool1For0; - - unchecked { - orderPool.sellRateCurrent += sellRate; - orderPool.sellRateEndingAtInterval[orderKey.expiration] += sellRate; - } - - self.orders[orderId] = Order({sellRate: sellRate, earningsFactorLast: orderPool.earningsFactorCurrent}); - } - - /// @inheritdoc ITWAMM - function updateOrder(PoolKey memory key, OrderKey memory orderKey, int256 amountDelta) - external - returns (uint256 tokens0Owed, uint256 tokens1Owed) - { - PoolId poolId = PoolId.wrap(keccak256(abi.encode(key))); - State storage twamm = twammStates[poolId]; - - executeTWAMMOrders(key); - - // This call reverts if the caller is not the owner of the order - (uint256 buyTokensOwed, uint256 sellTokensOwed, uint256 newSellrate, uint256 newEarningsFactorLast) = - _updateOrder(twamm, orderKey, amountDelta); - - if (orderKey.zeroForOne) { - tokens0Owed += sellTokensOwed; - tokens1Owed += buyTokensOwed; - } else { - tokens0Owed += buyTokensOwed; - tokens1Owed += sellTokensOwed; - } - - tokensOwed[key.currency0][orderKey.owner] += tokens0Owed; - tokensOwed[key.currency1][orderKey.owner] += tokens1Owed; - - if (amountDelta > 0) { - IERC20Minimal(orderKey.zeroForOne ? Currency.unwrap(key.currency0) : Currency.unwrap(key.currency1)) - .safeTransferFrom(msg.sender, address(this), uint256(amountDelta)); - } - - emit UpdateOrder( - poolId, orderKey.owner, orderKey.expiration, orderKey.zeroForOne, newSellrate, newEarningsFactorLast - ); - } - - function _updateOrder(State storage self, OrderKey memory orderKey, int256 amountDelta) - internal - returns (uint256 buyTokensOwed, uint256 sellTokensOwed, uint256 newSellRate, uint256 earningsFactorLast) - { - Order storage order = _getOrder(self, orderKey); - OrderPool.State storage orderPool = orderKey.zeroForOne ? self.orderPool0For1 : self.orderPool1For0; - - if (orderKey.owner != msg.sender) revert MustBeOwner(orderKey.owner, msg.sender); - if (order.sellRate == 0) revert OrderDoesNotExist(orderKey); - if (amountDelta != 0 && orderKey.expiration <= block.timestamp) revert CannotModifyCompletedOrder(orderKey); - - unchecked { - uint256 earningsFactor = orderPool.earningsFactorCurrent - order.earningsFactorLast; - buyTokensOwed = (earningsFactor * order.sellRate) >> FixedPoint96.RESOLUTION; - earningsFactorLast = orderPool.earningsFactorCurrent; - order.earningsFactorLast = earningsFactorLast; - - if (orderKey.expiration <= block.timestamp) { - delete self.orders[_orderId(orderKey)]; - } - - if (amountDelta != 0) { - uint256 duration = orderKey.expiration - block.timestamp; - uint256 unsoldAmount = order.sellRate * duration; - if (amountDelta == MIN_DELTA) amountDelta = -(unsoldAmount.toInt256()); - int256 newSellAmount = unsoldAmount.toInt256() + amountDelta; - if (newSellAmount < 0) revert InvalidAmountDelta(orderKey, unsoldAmount, amountDelta); - - newSellRate = uint256(newSellAmount) / duration; - - if (amountDelta < 0) { - uint256 sellRateDelta = order.sellRate - newSellRate; - orderPool.sellRateCurrent -= sellRateDelta; - orderPool.sellRateEndingAtInterval[orderKey.expiration] -= sellRateDelta; - sellTokensOwed = uint256(-amountDelta); - } else { - uint256 sellRateDelta = newSellRate - order.sellRate; - orderPool.sellRateCurrent += sellRateDelta; - orderPool.sellRateEndingAtInterval[orderKey.expiration] += sellRateDelta; - } - if (newSellRate == 0) { - delete self.orders[_orderId(orderKey)]; - } else { - order.sellRate = newSellRate; - } - } - } - } - - /// @inheritdoc ITWAMM - function claimTokens(Currency token, address to, uint256 amountRequested) - external - returns (uint256 amountTransferred) - { - uint256 currentBalance = token.balanceOfSelf(); - amountTransferred = tokensOwed[token][msg.sender]; - if (amountRequested != 0 && amountRequested < amountTransferred) amountTransferred = amountRequested; - if (currentBalance < amountTransferred) amountTransferred = currentBalance; // to catch precision errors - tokensOwed[token][msg.sender] -= amountTransferred; - IERC20Minimal(Currency.unwrap(token)).safeTransfer(to, amountTransferred); - } - - function _unlockCallback(bytes calldata rawData) internal override returns (bytes memory) { - (PoolKey memory key, IPoolManager.SwapParams memory swapParams) = - abi.decode(rawData, (PoolKey, IPoolManager.SwapParams)); - - BalanceDelta delta = manager.swap(key, swapParams, ZERO_BYTES); - - if (swapParams.zeroForOne) { - if (delta.amount0() < 0) { - key.currency0.settle(manager, address(this), uint256(uint128(-delta.amount0())), false); - } - if (delta.amount1() > 0) { - key.currency1.take(manager, address(this), uint256(uint128(delta.amount1())), false); - } - } else { - if (delta.amount1() < 0) { - key.currency1.settle(manager, address(this), uint256(uint128(-delta.amount1())), false); - } - if (delta.amount0() > 0) { - key.currency0.take(manager, address(this), uint256(uint128(delta.amount0())), false); - } - } - return bytes(""); - } - - function _getTWAMM(PoolKey memory key) private view returns (State storage) { - return twammStates[PoolId.wrap(keccak256(abi.encode(key)))]; - } - - struct PoolParamsOnExecute { - uint160 sqrtPriceX96; - uint128 liquidity; - } - - /// @notice Executes all existing long term orders in the TWAMM - /// @param pool The relevant state of the pool - function _executeTWAMMOrders( - State storage self, - IPoolManager manager, - PoolKey memory key, - PoolParamsOnExecute memory pool - ) internal returns (bool zeroForOne, uint160 newSqrtPriceX96) { - if (!_hasOutstandingOrders(self)) { - self.lastVirtualOrderTimestamp = block.timestamp; - return (false, 0); - } - - uint160 initialSqrtPriceX96 = pool.sqrtPriceX96; - uint256 prevTimestamp = self.lastVirtualOrderTimestamp; - uint256 nextExpirationTimestamp = prevTimestamp + (expirationInterval - (prevTimestamp % expirationInterval)); - - OrderPool.State storage orderPool0For1 = self.orderPool0For1; - OrderPool.State storage orderPool1For0 = self.orderPool1For0; - - unchecked { - while (nextExpirationTimestamp <= block.timestamp) { - if ( - orderPool0For1.sellRateEndingAtInterval[nextExpirationTimestamp] > 0 - || orderPool1For0.sellRateEndingAtInterval[nextExpirationTimestamp] > 0 - ) { - if (orderPool0For1.sellRateCurrent != 0 && orderPool1For0.sellRateCurrent != 0) { - pool = _advanceToNewTimestamp( - self, - manager, - key, - AdvanceParams( - expirationInterval, - nextExpirationTimestamp, - nextExpirationTimestamp - prevTimestamp, - pool - ) - ); - } else { - pool = _advanceTimestampForSinglePoolSell( - self, - manager, - key, - AdvanceSingleParams( - expirationInterval, - nextExpirationTimestamp, - nextExpirationTimestamp - prevTimestamp, - pool, - orderPool0For1.sellRateCurrent != 0 - ) - ); - } - prevTimestamp = nextExpirationTimestamp; - } - nextExpirationTimestamp += expirationInterval; - - if (!_hasOutstandingOrders(self)) break; - } - - if (prevTimestamp < block.timestamp && _hasOutstandingOrders(self)) { - if (orderPool0For1.sellRateCurrent != 0 && orderPool1For0.sellRateCurrent != 0) { - pool = _advanceToNewTimestamp( - self, - manager, - key, - AdvanceParams(expirationInterval, block.timestamp, block.timestamp - prevTimestamp, pool) - ); - } else { - pool = _advanceTimestampForSinglePoolSell( - self, - manager, - key, - AdvanceSingleParams( - expirationInterval, - block.timestamp, - block.timestamp - prevTimestamp, - pool, - orderPool0For1.sellRateCurrent != 0 - ) - ); - } - } - } - - self.lastVirtualOrderTimestamp = block.timestamp; - newSqrtPriceX96 = pool.sqrtPriceX96; - zeroForOne = initialSqrtPriceX96 > newSqrtPriceX96; - } - - struct AdvanceParams { - uint256 expirationInterval; - uint256 nextTimestamp; - uint256 secondsElapsed; - PoolParamsOnExecute pool; - } - - function _advanceToNewTimestamp( - State storage self, - IPoolManager manager, - PoolKey memory poolKey, - AdvanceParams memory params - ) private returns (PoolParamsOnExecute memory) { - uint160 finalSqrtPriceX96; - uint256 secondsElapsedX96 = params.secondsElapsed * FixedPoint96.Q96; - - OrderPool.State storage orderPool0For1 = self.orderPool0For1; - OrderPool.State storage orderPool1For0 = self.orderPool1For0; - - while (true) { - TwammMath.ExecutionUpdateParams memory executionParams = TwammMath.ExecutionUpdateParams( - secondsElapsedX96, - params.pool.sqrtPriceX96, - params.pool.liquidity, - orderPool0For1.sellRateCurrent, - orderPool1For0.sellRateCurrent - ); - - finalSqrtPriceX96 = TwammMath.getNewSqrtPriceX96(executionParams); - - (bool crossingInitializedTick, int24 tick) = - _isCrossingInitializedTick(params.pool, manager, poolKey, finalSqrtPriceX96); - unchecked { - if (crossingInitializedTick) { - uint256 secondsUntilCrossingX96; - (params.pool, secondsUntilCrossingX96) = _advanceTimeThroughTickCrossing( - self, - manager, - poolKey, - TickCrossingParams(tick, params.nextTimestamp, secondsElapsedX96, params.pool) - ); - secondsElapsedX96 = secondsElapsedX96 - secondsUntilCrossingX96; - } else { - (uint256 earningsFactorPool0, uint256 earningsFactorPool1) = - TwammMath.calculateEarningsUpdates(executionParams, finalSqrtPriceX96); - - if (params.nextTimestamp % params.expirationInterval == 0) { - orderPool0For1.advanceToInterval(params.nextTimestamp, earningsFactorPool0); - orderPool1For0.advanceToInterval(params.nextTimestamp, earningsFactorPool1); - } else { - orderPool0For1.advanceToCurrentTime(earningsFactorPool0); - orderPool1For0.advanceToCurrentTime(earningsFactorPool1); - } - params.pool.sqrtPriceX96 = finalSqrtPriceX96; - break; - } - } - } - - return params.pool; - } - - struct AdvanceSingleParams { - uint256 expirationInterval; - uint256 nextTimestamp; - uint256 secondsElapsed; - PoolParamsOnExecute pool; - bool zeroForOne; - } - - function _advanceTimestampForSinglePoolSell( - State storage self, - IPoolManager manager, - PoolKey memory poolKey, - AdvanceSingleParams memory params - ) private returns (PoolParamsOnExecute memory) { - OrderPool.State storage orderPool = params.zeroForOne ? self.orderPool0For1 : self.orderPool1For0; - uint256 sellRateCurrent = orderPool.sellRateCurrent; - uint256 amountSelling = sellRateCurrent * params.secondsElapsed; - uint256 totalEarnings; - - while (true) { - uint160 finalSqrtPriceX96 = SqrtPriceMath.getNextSqrtPriceFromInput( - params.pool.sqrtPriceX96, params.pool.liquidity, amountSelling, params.zeroForOne - ); - - (bool crossingInitializedTick, int24 tick) = - _isCrossingInitializedTick(params.pool, manager, poolKey, finalSqrtPriceX96); - - if (crossingInitializedTick) { - (, int128 liquidityNetAtTick) = manager.getTickLiquidity(poolKey.toId(), tick); - uint160 initializedSqrtPrice = TickMath.getSqrtPriceAtTick(tick); - - uint256 swapDelta0 = SqrtPriceMath.getAmount0Delta( - params.pool.sqrtPriceX96, initializedSqrtPrice, params.pool.liquidity, true - ); - uint256 swapDelta1 = SqrtPriceMath.getAmount1Delta( - params.pool.sqrtPriceX96, initializedSqrtPrice, params.pool.liquidity, true - ); - - params.pool.liquidity = params.zeroForOne - ? params.pool.liquidity - uint128(liquidityNetAtTick) - : params.pool.liquidity + uint128(-liquidityNetAtTick); - params.pool.sqrtPriceX96 = initializedSqrtPrice; - - unchecked { - totalEarnings += params.zeroForOne ? swapDelta1 : swapDelta0; - amountSelling -= params.zeroForOne ? swapDelta0 : swapDelta1; - } - } else { - if (params.zeroForOne) { - totalEarnings += SqrtPriceMath.getAmount1Delta( - params.pool.sqrtPriceX96, finalSqrtPriceX96, params.pool.liquidity, true - ); - } else { - totalEarnings += SqrtPriceMath.getAmount0Delta( - params.pool.sqrtPriceX96, finalSqrtPriceX96, params.pool.liquidity, true - ); - } - - uint256 accruedEarningsFactor = (totalEarnings * FixedPoint96.Q96) / sellRateCurrent; - - if (params.nextTimestamp % params.expirationInterval == 0) { - orderPool.advanceToInterval(params.nextTimestamp, accruedEarningsFactor); - } else { - orderPool.advanceToCurrentTime(accruedEarningsFactor); - } - params.pool.sqrtPriceX96 = finalSqrtPriceX96; - break; - } - } - - return params.pool; - } - - struct TickCrossingParams { - int24 initializedTick; - uint256 nextTimestamp; - uint256 secondsElapsedX96; - PoolParamsOnExecute pool; - } - - function _advanceTimeThroughTickCrossing( - State storage self, - IPoolManager manager, - PoolKey memory poolKey, - TickCrossingParams memory params - ) private returns (PoolParamsOnExecute memory, uint256) { - uint160 initializedSqrtPrice = params.initializedTick.getSqrtPriceAtTick(); - - uint256 secondsUntilCrossingX96 = TwammMath.calculateTimeBetweenTicks( - params.pool.liquidity, - params.pool.sqrtPriceX96, - initializedSqrtPrice, - self.orderPool0For1.sellRateCurrent, - self.orderPool1For0.sellRateCurrent - ); - - (uint256 earningsFactorPool0, uint256 earningsFactorPool1) = TwammMath.calculateEarningsUpdates( - TwammMath.ExecutionUpdateParams( - secondsUntilCrossingX96, - params.pool.sqrtPriceX96, - params.pool.liquidity, - self.orderPool0For1.sellRateCurrent, - self.orderPool1For0.sellRateCurrent - ), - initializedSqrtPrice - ); - - self.orderPool0For1.advanceToCurrentTime(earningsFactorPool0); - self.orderPool1For0.advanceToCurrentTime(earningsFactorPool1); - - unchecked { - // update pool - (, int128 liquidityNet) = manager.getTickLiquidity(poolKey.toId(), params.initializedTick); - if (initializedSqrtPrice < params.pool.sqrtPriceX96) liquidityNet = -liquidityNet; - params.pool.liquidity = liquidityNet < 0 - ? params.pool.liquidity - uint128(-liquidityNet) - : params.pool.liquidity + uint128(liquidityNet); - - params.pool.sqrtPriceX96 = initializedSqrtPrice; - } - return (params.pool, secondsUntilCrossingX96); - } - - function _isCrossingInitializedTick( - PoolParamsOnExecute memory pool, - IPoolManager manager, - PoolKey memory poolKey, - uint160 nextSqrtPriceX96 - ) internal view returns (bool crossingInitializedTick, int24 nextTickInit) { - // use current price as a starting point for nextTickInit - nextTickInit = pool.sqrtPriceX96.getTickAtSqrtPrice(); - int24 targetTick = nextSqrtPriceX96.getTickAtSqrtPrice(); - bool searchingLeft = nextSqrtPriceX96 < pool.sqrtPriceX96; - bool nextTickInitFurtherThanTarget = false; // initialize as false - - // nextTickInit returns the furthest tick within one word if no tick within that word is initialized - // so we must keep iterating if we haven't reached a tick further than our target tick - while (!nextTickInitFurtherThanTarget) { - unchecked { - if (searchingLeft) nextTickInit -= 1; - } - (nextTickInit, crossingInitializedTick) = manager.getNextInitializedTickWithinOneWord( - poolKey.toId(), nextTickInit, poolKey.tickSpacing, searchingLeft - ); - nextTickInitFurtherThanTarget = searchingLeft ? nextTickInit <= targetTick : nextTickInit > targetTick; - if (crossingInitializedTick == true) break; - } - if (nextTickInitFurtherThanTarget) crossingInitializedTick = false; - } - - function _getOrder(State storage self, OrderKey memory key) internal view returns (Order storage) { - return self.orders[_orderId(key)]; - } - - function _orderId(OrderKey memory key) private pure returns (bytes32) { - return keccak256(abi.encode(key)); - } - - function _hasOutstandingOrders(State storage self) internal view returns (bool) { - return self.orderPool0For1.sellRateCurrent != 0 || self.orderPool1For0.sellRateCurrent != 0; - } -} diff --git a/contracts/hooks/examples/VolatilityOracle.sol b/contracts/hooks/examples/VolatilityOracle.sol deleted file mode 100644 index 2900632f..00000000 --- a/contracts/hooks/examples/VolatilityOracle.sol +++ /dev/null @@ -1,70 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; - -import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; -import {LPFeeLibrary} from "@uniswap/v4-core/src/libraries/LPFeeLibrary.sol"; -import {BaseHook} from "../../BaseHook.sol"; -import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; - -contract VolatilityOracle is BaseHook { - using LPFeeLibrary for uint24; - - error MustUseDynamicFee(); - - uint32 immutable deployTimestamp; - - /// @dev For mocking - function _blockTimestamp() internal view virtual returns (uint32) { - return uint32(block.timestamp); - } - - constructor(IPoolManager _manager) BaseHook(_manager) { - deployTimestamp = _blockTimestamp(); - } - - function getHookPermissions() public pure override returns (Hooks.Permissions memory) { - return Hooks.Permissions({ - beforeInitialize: true, - afterInitialize: true, - beforeAddLiquidity: false, - beforeRemoveLiquidity: false, - afterAddLiquidity: false, - afterRemoveLiquidity: false, - beforeSwap: false, - afterSwap: false, - beforeDonate: false, - afterDonate: false, - beforeSwapReturnDelta: false, - afterSwapReturnDelta: false, - afterAddLiquidityReturnDelta: false, - afterRemoveLiquidityReturnDelta: false - }); - } - - function beforeInitialize(address, PoolKey calldata key, uint160, bytes calldata) - external - pure - override - returns (bytes4) - { - if (!key.fee.isDynamicFee()) revert MustUseDynamicFee(); - return VolatilityOracle.beforeInitialize.selector; - } - - function setFee(PoolKey calldata key) public { - uint24 startingFee = 3000; - uint32 lapsed = _blockTimestamp() - deployTimestamp; - uint24 fee = startingFee + (uint24(lapsed) * 100) / 60; // 100 bps a minute - manager.updateDynamicLPFee(key, fee); // initial fee 0.30% - } - - function afterInitialize(address, PoolKey calldata key, uint160, int24, bytes calldata) - external - override - returns (bytes4) - { - setFee(key); - return BaseHook.afterInitialize.selector; - } -} diff --git a/contracts/interfaces/IPeripheryPayments.sol b/contracts/interfaces/IPeripheryPayments.sol deleted file mode 100644 index f3c24660..00000000 --- a/contracts/interfaces/IPeripheryPayments.sol +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; - -import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; - -/// @title Periphery Payments -/// @notice Functions to ease deposits and withdrawals of ETH -interface IPeripheryPayments { - // TODO: figure out if we still need unwrapWETH9 from v3? - - /// @notice Transfers the full amount of a token held by this contract to recipient - /// @dev The amountMinimum parameter prevents malicious contracts from stealing the token from users - /// @param currency The contract address of the token which will be transferred to `recipient` - /// @param amountMinimum The minimum amount of token required for a transfer - /// @param recipient The destination address of the token - function sweepToken(Currency currency, uint256 amountMinimum, address recipient) external payable; -} diff --git a/contracts/interfaces/ISelfPermit.sol b/contracts/interfaces/ISelfPermit.sol deleted file mode 100644 index cb2445f5..00000000 --- a/contracts/interfaces/ISelfPermit.sol +++ /dev/null @@ -1,56 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.5.0; - -/// @title Self Permit -/// @notice Functionality to call permit on any EIP-2612-compliant token for use in the route -interface ISelfPermit { - /// @notice Permits this contract to spend a given token from `msg.sender` - /// @dev The `owner` is always msg.sender and the `spender` is always address(this). - /// @param token The address of the token spent - /// @param value The amount that can be spent of token - /// @param deadline A timestamp, the current blocktime must be less than or equal to this timestamp - /// @param v Must produce valid secp256k1 signature from the holder along with `r` and `s` - /// @param r Must produce valid secp256k1 signature from the holder along with `v` and `s` - /// @param s Must produce valid secp256k1 signature from the holder along with `r` and `v` - function selfPermit(address token, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) - external - payable; - - /// @notice Permits this contract to spend a given token from `msg.sender` - /// @dev The `owner` is always msg.sender and the `spender` is always address(this). - /// Can be used instead of #selfPermit to prevent calls from failing due to a frontrun of a call to #selfPermit - /// @param token The address of the token spent - /// @param value The amount that can be spent of token - /// @param deadline A timestamp, the current blocktime must be less than or equal to this timestamp - /// @param v Must produce valid secp256k1 signature from the holder along with `r` and `s` - /// @param r Must produce valid secp256k1 signature from the holder along with `v` and `s` - /// @param s Must produce valid secp256k1 signature from the holder along with `r` and `v` - function selfPermitIfNecessary(address token, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) - external - payable; - - /// @notice Permits this contract to spend the sender's tokens for permit signatures that have the `allowed` parameter - /// @dev The `owner` is always msg.sender and the `spender` is always address(this) - /// @param token The address of the token spent - /// @param nonce The current nonce of the owner - /// @param expiry The timestamp at which the permit is no longer valid - /// @param v Must produce valid secp256k1 signature from the holder along with `r` and `s` - /// @param r Must produce valid secp256k1 signature from the holder along with `v` and `s` - /// @param s Must produce valid secp256k1 signature from the holder along with `r` and `v` - function selfPermitAllowed(address token, uint256 nonce, uint256 expiry, uint8 v, bytes32 r, bytes32 s) - external - payable; - - /// @notice Permits this contract to spend the sender's tokens for permit signatures that have the `allowed` parameter - /// @dev The `owner` is always msg.sender and the `spender` is always address(this) - /// Can be used instead of #selfPermitAllowed to prevent calls from failing due to a frontrun of a call to #selfPermitAllowed. - /// @param token The address of the token spent - /// @param nonce The current nonce of the owner - /// @param expiry The timestamp at which the permit is no longer valid - /// @param v Must produce valid secp256k1 signature from the holder along with `r` and `s` - /// @param r Must produce valid secp256k1 signature from the holder along with `v` and `s` - /// @param s Must produce valid secp256k1 signature from the holder along with `r` and `v` - function selfPermitAllowedIfNecessary(address token, uint256 nonce, uint256 expiry, uint8 v, bytes32 r, bytes32 s) - external - payable; -} diff --git a/contracts/interfaces/ITWAMM.sol b/contracts/interfaces/ITWAMM.sol deleted file mode 100644 index 3b932d3c..00000000 --- a/contracts/interfaces/ITWAMM.sol +++ /dev/null @@ -1,136 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.15; - -import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import {IERC20Minimal} from "@uniswap/v4-core/src/interfaces/external/IERC20Minimal.sol"; -import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; -import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; -import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; - -interface ITWAMM { - /// @notice Thrown when account other than owner attempts to interact with an order - /// @param owner The owner of the order - /// @param currentAccount The invalid account attempting to interact with the order - error MustBeOwner(address owner, address currentAccount); - - /// @notice Thrown when trying to cancel an already completed order - /// @param orderKey The orderKey - error CannotModifyCompletedOrder(OrderKey orderKey); - - /// @notice Thrown when trying to submit an order with an expiration that isn't on the interval. - /// @param expiration The expiration timestamp of the order - error ExpirationNotOnInterval(uint256 expiration); - - /// @notice Thrown when trying to submit an order with an expiration time in the past. - /// @param expiration The expiration timestamp of the order - error ExpirationLessThanBlocktime(uint256 expiration); - - /// @notice Thrown when trying to submit an order without initializing TWAMM state first - error NotInitialized(); - - /// @notice Thrown when trying to submit an order that's already ongoing. - /// @param orderKey The already existing orderKey - error OrderAlreadyExists(OrderKey orderKey); - - /// @notice Thrown when trying to interact with an order that does not exist. - /// @param orderKey The already existing orderKey - error OrderDoesNotExist(OrderKey orderKey); - - /// @notice Thrown when trying to subtract more value from a long term order than exists - /// @param orderKey The orderKey - /// @param unsoldAmount The amount still unsold - /// @param amountDelta The amount delta for the order - error InvalidAmountDelta(OrderKey orderKey, uint256 unsoldAmount, int256 amountDelta); - - /// @notice Thrown when submitting an order with a sellRate of 0 - error SellRateCannotBeZero(); - - /// @notice Information associated with a long term order - /// @member sellRate Amount of tokens sold per interval - /// @member earningsFactorLast The accrued earnings factor from which to start claiming owed earnings for this order - struct Order { - uint256 sellRate; - uint256 earningsFactorLast; - } - - /// @notice Information that identifies an order - /// @member owner Owner of the order - /// @member expiration Timestamp when the order expires - /// @member zeroForOne Bool whether the order is zeroForOne - struct OrderKey { - address owner; - uint160 expiration; - bool zeroForOne; - } - - /// @notice Emitted when a new long term order is submitted - /// @param poolId The id of the corresponding pool - /// @param owner The owner of the new order - /// @param expiration The expiration timestamp of the order - /// @param zeroForOne Whether the order is selling token 0 for token 1 - /// @param sellRate The sell rate of tokens per second being sold in the order - /// @param earningsFactorLast The current earningsFactor of the order pool - event SubmitOrder( - PoolId indexed poolId, - address indexed owner, - uint160 expiration, - bool zeroForOne, - uint256 sellRate, - uint256 earningsFactorLast - ); - - /// @notice Emitted when a long term order is updated - /// @param poolId The id of the corresponding pool - /// @param owner The owner of the existing order - /// @param expiration The expiration timestamp of the order - /// @param zeroForOne Whether the order is selling token 0 for token 1 - /// @param sellRate The updated sellRate of tokens per second being sold in the order - /// @param earningsFactorLast The current earningsFactor of the order pool - /// (since updated orders will claim existing earnings) - event UpdateOrder( - PoolId indexed poolId, - address indexed owner, - uint160 expiration, - bool zeroForOne, - uint256 sellRate, - uint256 earningsFactorLast - ); - - /// @notice Time interval on which orders are allowed to expire. Conserves processing needed on execute. - function expirationInterval() external view returns (uint256); - - /// @notice Submits a new long term order into the TWAMM. Also executes TWAMM orders if not up to date. - /// @param key The PoolKey for which to identify the amm pool of the order - /// @param orderKey The OrderKey for the new order - /// @param amountIn The amount of sell token to add to the order. Some precision on amountIn may be lost up to the - /// magnitude of (orderKey.expiration - block.timestamp) - /// @return orderId The bytes32 ID of the order - function submitOrder(PoolKey calldata key, OrderKey calldata orderKey, uint256 amountIn) - external - returns (bytes32 orderId); - - /// @notice Update an existing long term order with current earnings, optionally modify the amount selling. - /// @param key The PoolKey for which to identify the amm pool of the order - /// @param orderKey The OrderKey for which to identify the order - /// @param amountDelta The delta for the order sell amount. Negative to remove from order, positive to add, or - /// -1 to remove full amount from order. - function updateOrder(PoolKey calldata key, OrderKey calldata orderKey, int256 amountDelta) - external - returns (uint256 tokens0Owed, uint256 tokens1Owed); - - /// @notice Claim tokens owed from TWAMM contract - /// @param token The token to claim - /// @param to The receipient of the claim - /// @param amountRequested The amount of tokens requested to claim. Set to 0 to claim all. - /// @return amountTransferred The total token amount to be collected - function claimTokens(Currency token, address to, uint256 amountRequested) - external - returns (uint256 amountTransferred); - - /// @notice Executes TWAMM orders on the pool, swapping on the pool itself to make up the difference between the - /// two TWAMM pools swapping against each other - /// @param key The pool key associated with the TWAMM - function executeTWAMMOrders(PoolKey memory key) external; - - function tokensOwed(Currency token, address owner) external returns (uint256); -} diff --git a/contracts/interfaces/external/IERC1271.sol b/contracts/interfaces/external/IERC1271.sol deleted file mode 100644 index dcb30cb8..00000000 --- a/contracts/interfaces/external/IERC1271.sol +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.5.0; - -/// @title Interface for verifying contract-based account signatures -/// @notice Interface that verifies provided signature for the data -/// @dev Interface defined by EIP-1271 -interface IERC1271 { - /// @notice Returns whether the provided signature is valid for the provided data - /// @dev MUST return the bytes4 magic value 0x1626ba7e when function passes. - /// MUST NOT modify state (using STATICCALL for solc < 0.5, view modifier for solc > 0.5). - /// MUST allow external calls. - /// @param hash Hash of the data to be signed - /// @param signature Signature byte array associated with _data - /// @return magicValue The bytes4 magic value 0x1626ba7e - function isValidSignature(bytes32 hash, bytes memory signature) external view returns (bytes4 magicValue); -} diff --git a/contracts/libraries/LiquidityAmounts.sol b/contracts/libraries/LiquidityAmounts.sol deleted file mode 100644 index 742e48f5..00000000 --- a/contracts/libraries/LiquidityAmounts.sol +++ /dev/null @@ -1,134 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.20; - -import "@uniswap/v4-core/src/libraries/FullMath.sol"; -import "@uniswap/v4-core/src/libraries/FixedPoint96.sol"; - -/// @title Liquidity amount functions -/// @notice Provides functions for computing liquidity amounts from token amounts and prices -library LiquidityAmounts { - /// @notice Downcasts uint256 to uint128 - /// @param x The uint258 to be downcasted - /// @return y The passed value, downcasted to uint128 - function toUint128(uint256 x) private pure returns (uint128 y) { - require((y = uint128(x)) == x); - } - - /// @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 sqrtRatioAX96 A sqrt price representing the first tick boundary - /// @param sqrtRatioBX96 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 sqrtRatioAX96, uint160 sqrtRatioBX96, uint256 amount0) - internal - pure - returns (uint128 liquidity) - { - if (sqrtRatioAX96 > sqrtRatioBX96) (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96); - uint256 intermediate = FullMath.mulDiv(sqrtRatioAX96, sqrtRatioBX96, FixedPoint96.Q96); - return toUint128(FullMath.mulDiv(amount0, intermediate, sqrtRatioBX96 - sqrtRatioAX96)); - } - - /// @notice Computes the amount of liquidity received for a given amount of token1 and price range - /// @dev Calculates amount1 / (sqrt(upper) - sqrt(lower)). - /// @param sqrtRatioAX96 A sqrt price representing the first tick boundary - /// @param sqrtRatioBX96 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 sqrtRatioAX96, uint160 sqrtRatioBX96, uint256 amount1) - internal - pure - returns (uint128 liquidity) - { - if (sqrtRatioAX96 > sqrtRatioBX96) (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96); - return toUint128(FullMath.mulDiv(amount1, FixedPoint96.Q96, sqrtRatioBX96 - sqrtRatioAX96)); - } - - /// @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 sqrtRatioX96 A sqrt price representing the current pool prices - /// @param sqrtRatioAX96 A sqrt price representing the first tick boundary - /// @param sqrtRatioBX96 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 sqrtRatioX96, - uint160 sqrtRatioAX96, - uint160 sqrtRatioBX96, - uint256 amount0, - uint256 amount1 - ) internal pure returns (uint128 liquidity) { - if (sqrtRatioAX96 > sqrtRatioBX96) (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96); - - if (sqrtRatioX96 <= sqrtRatioAX96) { - liquidity = getLiquidityForAmount0(sqrtRatioAX96, sqrtRatioBX96, amount0); - } else if (sqrtRatioX96 < sqrtRatioBX96) { - uint128 liquidity0 = getLiquidityForAmount0(sqrtRatioX96, sqrtRatioBX96, amount0); - uint128 liquidity1 = getLiquidityForAmount1(sqrtRatioAX96, sqrtRatioX96, amount1); - - liquidity = liquidity0 < liquidity1 ? liquidity0 : liquidity1; - } else { - liquidity = getLiquidityForAmount1(sqrtRatioAX96, sqrtRatioBX96, amount1); - } - } - - /// @notice Computes the amount of token0 for a given amount of liquidity and a price range - /// @param sqrtRatioAX96 A sqrt price representing the first tick boundary - /// @param sqrtRatioBX96 A sqrt price representing the second tick boundary - /// @param liquidity The liquidity being valued - /// @return amount0 The amount of token0 - function getAmount0ForLiquidity(uint160 sqrtRatioAX96, uint160 sqrtRatioBX96, uint128 liquidity) - internal - pure - returns (uint256 amount0) - { - if (sqrtRatioAX96 > sqrtRatioBX96) (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96); - - return FullMath.mulDiv( - uint256(liquidity) << FixedPoint96.RESOLUTION, sqrtRatioBX96 - sqrtRatioAX96, sqrtRatioBX96 - ) / sqrtRatioAX96; - } - - /// @notice Computes the amount of token1 for a given amount of liquidity and a price range - /// @param sqrtRatioAX96 A sqrt price representing the first tick boundary - /// @param sqrtRatioBX96 A sqrt price representing the second tick boundary - /// @param liquidity The liquidity being valued - /// @return amount1 The amount of token1 - function getAmount1ForLiquidity(uint160 sqrtRatioAX96, uint160 sqrtRatioBX96, uint128 liquidity) - internal - pure - returns (uint256 amount1) - { - if (sqrtRatioAX96 > sqrtRatioBX96) (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96); - - return FullMath.mulDiv(liquidity, sqrtRatioBX96 - sqrtRatioAX96, FixedPoint96.Q96); - } - - /// @notice Computes the token0 and token1 value for a given amount of liquidity, the current - /// pool prices and the prices at the tick boundaries - /// @param sqrtRatioX96 A sqrt price representing the current pool prices - /// @param sqrtRatioAX96 A sqrt price representing the first tick boundary - /// @param sqrtRatioBX96 A sqrt price representing the second tick boundary - /// @param liquidity The liquidity being valued - /// @return amount0 The amount of token0 - /// @return amount1 The amount of token1 - function getAmountsForLiquidity( - uint160 sqrtRatioX96, - uint160 sqrtRatioAX96, - uint160 sqrtRatioBX96, - uint128 liquidity - ) internal pure returns (uint256 amount0, uint256 amount1) { - if (sqrtRatioAX96 > sqrtRatioBX96) (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96); - - if (sqrtRatioX96 <= sqrtRatioAX96) { - amount0 = getAmount0ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, liquidity); - } else if (sqrtRatioX96 < sqrtRatioBX96) { - amount0 = getAmount0ForLiquidity(sqrtRatioX96, sqrtRatioBX96, liquidity); - amount1 = getAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioX96, liquidity); - } else { - amount1 = getAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, liquidity); - } - } -} diff --git a/contracts/libraries/Oracle.sol b/contracts/libraries/Oracle.sol deleted file mode 100644 index 822f356f..00000000 --- a/contracts/libraries/Oracle.sol +++ /dev/null @@ -1,337 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; - -/// @title Oracle -/// @notice Provides price and liquidity data useful for a wide variety of system designs -/// @dev Instances of stored oracle data, "observations", are collected in the oracle array -/// Every pool is initialized with an oracle array length of 1. Anyone can pay the SSTOREs to increase the -/// maximum length of the oracle array. New slots will be added when the array is fully populated. -/// Observations are overwritten when the full length of the oracle array is populated. -/// The most recent observation is available, independent of the length of the oracle array, by passing 0 to observe() -library Oracle { - /// @notice Thrown when trying to interact with an Oracle of a non-initialized pool - error OracleCardinalityCannotBeZero(); - - /// @notice Thrown when trying to observe a price that is older than the oldest recorded price - /// @param oldestTimestamp Timestamp of the oldest remaining observation - /// @param targetTimestamp Invalid timestamp targeted to be observed - error TargetPredatesOldestObservation(uint32 oldestTimestamp, uint32 targetTimestamp); - - struct Observation { - // the block timestamp of the observation - uint32 blockTimestamp; - // the tick accumulator, i.e. tick * time elapsed since the pool was first initialized - int56 tickCumulative; - // the seconds per liquidity, i.e. seconds elapsed / max(1, liquidity) since the pool was first initialized - uint160 secondsPerLiquidityCumulativeX128; - // whether or not the observation is initialized - bool initialized; - } - - /// @notice Transforms a previous observation into a new observation, given the passage of time and the current tick and liquidity values - /// @dev blockTimestamp _must_ be chronologically equal to or greater than last.blockTimestamp, safe for 0 or 1 overflows - /// @param last The specified observation to be transformed - /// @param blockTimestamp The timestamp of the new observation - /// @param tick The active tick at the time of the new observation - /// @param liquidity The total in-range liquidity at the time of the new observation - /// @return Observation The newly populated observation - function transform(Observation memory last, uint32 blockTimestamp, int24 tick, uint128 liquidity) - private - pure - returns (Observation memory) - { - unchecked { - uint32 delta = blockTimestamp - last.blockTimestamp; - return Observation({ - blockTimestamp: blockTimestamp, - tickCumulative: last.tickCumulative + int56(tick) * int56(uint56(delta)), - secondsPerLiquidityCumulativeX128: last.secondsPerLiquidityCumulativeX128 - + ((uint160(delta) << 128) / (liquidity > 0 ? liquidity : 1)), - initialized: true - }); - } - } - - /// @notice Initialize the oracle array by writing the first slot. Called once for the lifecycle of the observations array - /// @param self The stored oracle array - /// @param time The time of the oracle initialization, via block.timestamp truncated to uint32 - /// @return cardinality The number of populated elements in the oracle array - /// @return cardinalityNext The new length of the oracle array, independent of population - function initialize(Observation[65535] storage self, uint32 time) - internal - returns (uint16 cardinality, uint16 cardinalityNext) - { - self[0] = Observation({ - blockTimestamp: time, - tickCumulative: 0, - secondsPerLiquidityCumulativeX128: 0, - initialized: true - }); - return (1, 1); - } - - /// @notice Writes an oracle observation to the array - /// @dev Writable at most once per block. Index represents the most recently written element. cardinality and index must be tracked externally. - /// If the index is at the end of the allowable array length (according to cardinality), and the next cardinality - /// is greater than the current one, cardinality may be increased. This restriction is created to preserve ordering. - /// @param self The stored oracle array - /// @param index The index of the observation that was most recently written to the observations array - /// @param blockTimestamp The timestamp of the new observation - /// @param tick The active tick at the time of the new observation - /// @param liquidity The total in-range liquidity at the time of the new observation - /// @param cardinality The number of populated elements in the oracle array - /// @param cardinalityNext The new length of the oracle array, independent of population - /// @return indexUpdated The new index of the most recently written element in the oracle array - /// @return cardinalityUpdated The new cardinality of the oracle array - function write( - Observation[65535] storage self, - uint16 index, - uint32 blockTimestamp, - int24 tick, - uint128 liquidity, - uint16 cardinality, - uint16 cardinalityNext - ) internal returns (uint16 indexUpdated, uint16 cardinalityUpdated) { - unchecked { - Observation memory last = self[index]; - - // early return if we've already written an observation this block - if (last.blockTimestamp == blockTimestamp) return (index, cardinality); - - // if the conditions are right, we can bump the cardinality - if (cardinalityNext > cardinality && index == (cardinality - 1)) { - cardinalityUpdated = cardinalityNext; - } else { - cardinalityUpdated = cardinality; - } - - indexUpdated = (index + 1) % cardinalityUpdated; - self[indexUpdated] = transform(last, blockTimestamp, tick, liquidity); - } - } - - /// @notice Prepares the oracle array to store up to `next` observations - /// @param self The stored oracle array - /// @param current The current next cardinality of the oracle array - /// @param next The proposed next cardinality which will be populated in the oracle array - /// @return next The next cardinality which will be populated in the oracle array - function grow(Observation[65535] storage self, uint16 current, uint16 next) internal returns (uint16) { - unchecked { - if (current == 0) revert OracleCardinalityCannotBeZero(); - // no-op if the passed next value isn't greater than the current next value - if (next <= current) return current; - // store in each slot to prevent fresh SSTOREs in swaps - // this data will not be used because the initialized boolean is still false - for (uint16 i = current; i < next; i++) { - self[i].blockTimestamp = 1; - } - return next; - } - } - - /// @notice comparator for 32-bit timestamps - /// @dev safe for 0 or 1 overflows, a and b _must_ be chronologically before or equal to time - /// @param time A timestamp truncated to 32 bits - /// @param a A comparison timestamp from which to determine the relative position of `time` - /// @param b From which to determine the relative position of `time` - /// @return Whether `a` is chronologically <= `b` - function lte(uint32 time, uint32 a, uint32 b) private pure returns (bool) { - unchecked { - // if there hasn't been overflow, no need to adjust - if (a <= time && b <= time) return a <= b; - - uint256 aAdjusted = a > time ? a : a + 2 ** 32; - uint256 bAdjusted = b > time ? b : b + 2 ** 32; - - return aAdjusted <= bAdjusted; - } - } - - /// @notice Fetches the observations beforeOrAt and atOrAfter a target, i.e. where [beforeOrAt, atOrAfter] is satisfied. - /// The result may be the same observation, or adjacent observations. - /// @dev The answer must be contained in the array, used when the target is located within the stored observation - /// boundaries: older than the most recent observation and younger, or the same age as, the oldest observation - /// @param self The stored oracle array - /// @param time The current block.timestamp - /// @param target The timestamp at which the reserved observation should be for - /// @param index The index of the observation that was most recently written to the observations array - /// @param cardinality The number of populated elements in the oracle array - /// @return beforeOrAt The observation recorded before, or at, the target - /// @return atOrAfter The observation recorded at, or after, the target - function binarySearch(Observation[65535] storage self, uint32 time, uint32 target, uint16 index, uint16 cardinality) - private - view - returns (Observation memory beforeOrAt, Observation memory atOrAfter) - { - unchecked { - uint256 l = (index + 1) % cardinality; // oldest observation - uint256 r = l + cardinality - 1; // newest observation - uint256 i; - while (true) { - i = (l + r) / 2; - - beforeOrAt = self[i % cardinality]; - - // we've landed on an uninitialized tick, keep searching higher (more recently) - if (!beforeOrAt.initialized) { - l = i + 1; - continue; - } - - atOrAfter = self[(i + 1) % cardinality]; - - bool targetAtOrAfter = lte(time, beforeOrAt.blockTimestamp, target); - - // check if we've found the answer! - if (targetAtOrAfter && lte(time, target, atOrAfter.blockTimestamp)) break; - - if (!targetAtOrAfter) r = i - 1; - else l = i + 1; - } - } - } - - /// @notice Fetches the observations beforeOrAt and atOrAfter a given target, i.e. where [beforeOrAt, atOrAfter] is satisfied - /// @dev Assumes there is at least 1 initialized observation. - /// Used by observeSingle() to compute the counterfactual accumulator values as of a given block timestamp. - /// @param self The stored oracle array - /// @param time The current block.timestamp - /// @param target The timestamp at which the reserved observation should be for - /// @param tick The active tick at the time of the returned or simulated observation - /// @param index The index of the observation that was most recently written to the observations array - /// @param liquidity The total pool liquidity at the time of the call - /// @param cardinality The number of populated elements in the oracle array - /// @return beforeOrAt The observation which occurred at, or before, the given timestamp - /// @return atOrAfter The observation which occurred at, or after, the given timestamp - function getSurroundingObservations( - Observation[65535] storage self, - uint32 time, - uint32 target, - int24 tick, - uint16 index, - uint128 liquidity, - uint16 cardinality - ) private view returns (Observation memory beforeOrAt, Observation memory atOrAfter) { - unchecked { - // optimistically set before to the newest observation - beforeOrAt = self[index]; - - // if the target is chronologically at or after the newest observation, we can early return - if (lte(time, beforeOrAt.blockTimestamp, target)) { - if (beforeOrAt.blockTimestamp == target) { - // if newest observation equals target, we're in the same block, so we can ignore atOrAfter - return (beforeOrAt, atOrAfter); - } else { - // otherwise, we need to transform - return (beforeOrAt, transform(beforeOrAt, target, tick, liquidity)); - } - } - - // now, set before to the oldest observation - beforeOrAt = self[(index + 1) % cardinality]; - if (!beforeOrAt.initialized) beforeOrAt = self[0]; - - // ensure that the target is chronologically at or after the oldest observation - if (!lte(time, beforeOrAt.blockTimestamp, target)) { - revert TargetPredatesOldestObservation(beforeOrAt.blockTimestamp, target); - } - - // if we've reached this point, we have to binary search - return binarySearch(self, time, target, index, cardinality); - } - } - - /// @dev Reverts if an observation at or before the desired observation timestamp does not exist. - /// 0 may be passed as `secondsAgo' to return the current cumulative values. - /// If called with a timestamp falling between two observations, returns the counterfactual accumulator values - /// at exactly the timestamp between the two observations. - /// @param self The stored oracle array - /// @param time The current block timestamp - /// @param secondsAgo The amount of time to look back, in seconds, at which point to return an observation - /// @param tick The current tick - /// @param index The index of the observation that was most recently written to the observations array - /// @param liquidity The current in-range pool liquidity - /// @param cardinality The number of populated elements in the oracle array - /// @return tickCumulative The tick * time elapsed since the pool was first initialized, as of `secondsAgo` - /// @return secondsPerLiquidityCumulativeX128 The time elapsed / max(1, liquidity) since the pool was first initialized, as of `secondsAgo` - function observeSingle( - Observation[65535] storage self, - uint32 time, - uint32 secondsAgo, - int24 tick, - uint16 index, - uint128 liquidity, - uint16 cardinality - ) internal view returns (int56 tickCumulative, uint160 secondsPerLiquidityCumulativeX128) { - unchecked { - if (secondsAgo == 0) { - Observation memory last = self[index]; - if (last.blockTimestamp != time) last = transform(last, time, tick, liquidity); - return (last.tickCumulative, last.secondsPerLiquidityCumulativeX128); - } - - uint32 target = time - secondsAgo; - - (Observation memory beforeOrAt, Observation memory atOrAfter) = - getSurroundingObservations(self, time, target, tick, index, liquidity, cardinality); - - if (target == beforeOrAt.blockTimestamp) { - // we're at the left boundary - return (beforeOrAt.tickCumulative, beforeOrAt.secondsPerLiquidityCumulativeX128); - } else if (target == atOrAfter.blockTimestamp) { - // we're at the right boundary - return (atOrAfter.tickCumulative, atOrAfter.secondsPerLiquidityCumulativeX128); - } else { - // we're in the middle - uint32 observationTimeDelta = atOrAfter.blockTimestamp - beforeOrAt.blockTimestamp; - uint32 targetDelta = target - beforeOrAt.blockTimestamp; - return ( - beforeOrAt.tickCumulative - + ((atOrAfter.tickCumulative - beforeOrAt.tickCumulative) / int56(uint56(observationTimeDelta))) - * int56(uint56(targetDelta)), - beforeOrAt.secondsPerLiquidityCumulativeX128 - + uint160( - ( - uint256( - atOrAfter.secondsPerLiquidityCumulativeX128 - - beforeOrAt.secondsPerLiquidityCumulativeX128 - ) * targetDelta - ) / observationTimeDelta - ) - ); - } - } - } - - /// @notice Returns the accumulator values as of each time seconds ago from the given time in the array of `secondsAgos` - /// @dev Reverts if `secondsAgos` > oldest observation - /// @param self The stored oracle array - /// @param time The current block.timestamp - /// @param secondsAgos Each amount of time to look back, in seconds, at which point to return an observation - /// @param tick The current tick - /// @param index The index of the observation that was most recently written to the observations array - /// @param liquidity The current in-range pool liquidity - /// @param cardinality The number of populated elements in the oracle array - /// @return tickCumulatives The tick * time elapsed since the pool was first initialized, as of each `secondsAgo` - /// @return secondsPerLiquidityCumulativeX128s The cumulative seconds / max(1, liquidity) since the pool was first initialized, as of each `secondsAgo` - function observe( - Observation[65535] storage self, - uint32 time, - uint32[] memory secondsAgos, - int24 tick, - uint16 index, - uint128 liquidity, - uint16 cardinality - ) internal view returns (int56[] memory tickCumulatives, uint160[] memory secondsPerLiquidityCumulativeX128s) { - unchecked { - if (cardinality == 0) revert OracleCardinalityCannotBeZero(); - - tickCumulatives = new int56[](secondsAgos.length); - secondsPerLiquidityCumulativeX128s = new uint160[](secondsAgos.length); - for (uint256 i = 0; i < secondsAgos.length; i++) { - (tickCumulatives[i], secondsPerLiquidityCumulativeX128s[i]) = - observeSingle(self, time, secondsAgos[i], tick, index, liquidity, cardinality); - } - } - } -} diff --git a/contracts/libraries/PoolGetters.sol b/contracts/libraries/PoolGetters.sol deleted file mode 100644 index df31f3c1..00000000 --- a/contracts/libraries/PoolGetters.sol +++ /dev/null @@ -1,106 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; - -import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import {Pool} from "@uniswap/v4-core/src/libraries/Pool.sol"; -import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; -import {BitMath} from "@uniswap/v4-core/src/libraries/BitMath.sol"; -import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; - -/// @title Helper functions to access pool information -/// TODO: Expose other getters on core with extsload. Only use when extsload is available and storage layout is frozen. -library PoolGetters { - uint256 constant POOL_SLOT = 10; - uint256 constant TICKS_OFFSET = 4; - uint256 constant TICK_BITMAP_OFFSET = 5; - - using StateLibrary for IPoolManager; - - function getNetLiquidityAtTick(IPoolManager poolManager, PoolId poolId, int24 tick) - internal - view - returns (int128 l) - { - bytes32 value = poolManager.extsload( - keccak256(abi.encode(tick, uint256(keccak256(abi.encode(poolId, POOL_SLOT))) + TICKS_OFFSET)) - ); - - assembly { - l := shr(128, and(value, shl(128, sub(shl(128, 1), 1)))) - } - } - - function getTickBitmapAtWord(IPoolManager poolManager, PoolId poolId, int16 word) - internal - view - returns (uint256 bm) - { - bm = uint256( - poolManager.extsload( - keccak256(abi.encode(word, uint256(keccak256(abi.encode(poolId, POOL_SLOT))) + TICK_BITMAP_OFFSET)) - ) - ); - } - - /// @notice Returns the next initialized tick contained in the same word (or adjacent word) as the tick that is either - /// to the left (less than or equal to) or right (greater than) of the given tick - /// @param poolManager The mapping in which to compute the next initialized tick - /// @param tick The starting tick - /// @param tickSpacing The spacing between usable ticks - /// @param lte Whether to search for the next initialized tick to the left (less than or equal to the starting tick) - /// @return next The next initialized or uninitialized tick up to 256 ticks away from the current tick - /// @return initialized Whether the next tick is initialized, as the function only searches within up to 256 ticks - function getNextInitializedTickWithinOneWord( - IPoolManager poolManager, - PoolId poolId, - int24 tick, - int24 tickSpacing, - bool lte - ) internal view returns (int24 next, bool initialized) { - unchecked { - int24 compressed = tick / tickSpacing; - if (tick < 0 && tick % tickSpacing != 0) compressed--; // round towards negative infinity - - if (lte) { - (int16 wordPos, uint8 bitPos) = position(compressed); - // all the 1s at or to the right of the current bitPos - uint256 mask = (1 << bitPos) - 1 + (1 << bitPos); - // uint256 masked = self[wordPos] & mask; - uint256 tickBitmap = poolManager.getTickBitmap(poolId, wordPos); - uint256 masked = tickBitmap & mask; - - // if there are no initialized ticks to the right of or at the current tick, return rightmost in the word - initialized = masked != 0; - // overflow/underflow is possible, but prevented externally by limiting both tickSpacing and tick - next = initialized - ? (compressed - int24(uint24(bitPos - BitMath.mostSignificantBit(masked)))) * tickSpacing - : (compressed - int24(uint24(bitPos))) * tickSpacing; - } else { - // start from the word of the next tick, since the current tick state doesn't matter - (int16 wordPos, uint8 bitPos) = position(compressed + 1); - // all the 1s at or to the left of the bitPos - uint256 mask = ~((1 << bitPos) - 1); - uint256 tickBitmap = poolManager.getTickBitmap(poolId, wordPos); - uint256 masked = tickBitmap & mask; - - // if there are no initialized ticks to the left of the current tick, return leftmost in the word - initialized = masked != 0; - // overflow/underflow is possible, but prevented externally by limiting both tickSpacing and tick - next = initialized - ? (compressed + 1 + int24(uint24(BitMath.leastSignificantBit(masked) - bitPos))) * tickSpacing - : (compressed + 1 + int24(uint24(type(uint8).max - bitPos))) * tickSpacing; - } - } - } - - /// @notice Computes the position in the mapping where the initialized bit for a tick lives - /// @param tick The tick for which to compute the position - /// @return wordPos The key in the mapping containing the word in which the bit is stored - /// @return bitPos The bit position in the word where the flag is stored - function position(int24 tick) private pure returns (int16 wordPos, uint8 bitPos) { - unchecked { - wordPos = int16(tick >> 8); - bitPos = uint8(int8(tick % 256)); - } - } -} diff --git a/contracts/libraries/TWAMM/ABDKMathQuad.sol b/contracts/libraries/TWAMM/ABDKMathQuad.sol deleted file mode 100644 index 00fa5a05..00000000 --- a/contracts/libraries/TWAMM/ABDKMathQuad.sol +++ /dev/null @@ -1,1546 +0,0 @@ -// SPDX-License-Identifier: BSD-4-Clause -/* - * ABDK Math Quad Smart Contract Library. Copyright © 2019 by ABDK Consulting. - * Author: Mikhail Vladimirov - */ -pragma solidity ^0.8.0; - -/** - * Smart contract library of mathematical functions operating with IEEE 754 - * quadruple-precision binary floating-point numbers (quadruple precision - * numbers). As long as quadruple precision numbers are 16-bytes long, they are - * represented by bytes16 type. - */ -library ABDKMathQuad { - /* - * 0. - */ - bytes16 private constant POSITIVE_ZERO = 0x00000000000000000000000000000000; - - /* - * -0. - */ - bytes16 private constant NEGATIVE_ZERO = 0x80000000000000000000000000000000; - - /* - * +Infinity. - */ - bytes16 private constant POSITIVE_INFINITY = 0x7FFF0000000000000000000000000000; - - /* - * -Infinity. - */ - bytes16 private constant NEGATIVE_INFINITY = 0xFFFF0000000000000000000000000000; - - /* - * Canonical NaN value. - */ - bytes16 private constant NaN = 0x7FFF8000000000000000000000000000; - - /** - * Convert signed 256-bit integer number into quadruple precision number. - * - * @param x signed 256-bit integer number - * @return quadruple precision number - */ - function fromInt(int256 x) internal pure returns (bytes16) { - unchecked { - if (x == 0) { - return bytes16(0); - } else { - // We rely on overflow behavior here - uint256 result = uint256(x > 0 ? x : -x); - - uint256 msb = mostSignificantBit(result); - if (msb < 112) result <<= 112 - msb; - else if (msb > 112) result >>= msb - 112; - - result = result & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF | 16383 + msb << 112; - if (x < 0) result |= 0x80000000000000000000000000000000; - - return bytes16(uint128(result)); - } - } - } - - /** - * Convert quadruple precision number into signed 256-bit integer number - * rounding towards zero. Revert on overflow. - * - * @param x quadruple precision number - * @return signed 256-bit integer number - */ - function toInt(bytes16 x) internal pure returns (int256) { - unchecked { - uint256 exponent = uint128(x) >> 112 & 0x7FFF; - - require(exponent <= 16638); // Overflow - if (exponent < 16383) return 0; // Underflow - - uint256 result = uint256(uint128(x)) & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF | 0x10000000000000000000000000000; - - if (exponent < 16495) result >>= 16495 - exponent; - else if (exponent > 16495) result <<= exponent - 16495; - - if (uint128(x) >= 0x80000000000000000000000000000000) { - // Negative - require(result <= 0x8000000000000000000000000000000000000000000000000000000000000000); - return -int256(result); // We rely on overflow behavior here - } else { - require(result <= 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF); - return int256(result); - } - } - } - - /** - * Convert unsigned 256-bit integer number into quadruple precision number. - * - * @param x unsigned 256-bit integer number - * @return quadruple precision number - */ - function fromUInt(uint256 x) internal pure returns (bytes16) { - unchecked { - if (x == 0) { - return bytes16(0); - } else { - uint256 result = x; - - uint256 msb = mostSignificantBit(result); - if (msb < 112) result <<= 112 - msb; - else if (msb > 112) result >>= msb - 112; - - result = result & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF | 16383 + msb << 112; - - return bytes16(uint128(result)); - } - } - } - - /** - * Convert quadruple precision number into unsigned 256-bit integer number - * rounding towards zero. Revert on underflow. Note, that negative floating - * point numbers in range (-1.0 .. 0.0) may be converted to unsigned integer - * without error, because they are rounded to zero. - * - * @param x quadruple precision number - * @return unsigned 256-bit integer number - */ - function toUInt(bytes16 x) internal pure returns (uint256) { - unchecked { - uint256 exponent = uint128(x) >> 112 & 0x7FFF; - - if (exponent < 16383) return 0; // Underflow - - require(uint128(x) < 0x80000000000000000000000000000000); // Negative - - require(exponent <= 16638); // Overflow - uint256 result = uint256(uint128(x)) & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF | 0x10000000000000000000000000000; - - if (exponent < 16495) result >>= 16495 - exponent; - else if (exponent > 16495) result <<= exponent - 16495; - - return result; - } - } - - /** - * Convert signed 128.128 bit fixed point number into quadruple precision - * number. - * - * @param x signed 128.128 bit fixed point number - * @return quadruple precision number - */ - function from128x128(int256 x) internal pure returns (bytes16) { - unchecked { - if (x == 0) { - return bytes16(0); - } else { - // We rely on overflow behavior here - uint256 result = uint256(x > 0 ? x : -x); - - uint256 msb = mostSignificantBit(result); - if (msb < 112) result <<= 112 - msb; - else if (msb > 112) result >>= msb - 112; - - result = result & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF | 16255 + msb << 112; - if (x < 0) result |= 0x80000000000000000000000000000000; - - return bytes16(uint128(result)); - } - } - } - - /** - * Convert quadruple precision number into signed 128.128 bit fixed point - * number. Revert on overflow. - * - * @param x quadruple precision number - * @return signed 128.128 bit fixed point number - */ - function to128x128(bytes16 x) internal pure returns (int256) { - unchecked { - uint256 exponent = uint128(x) >> 112 & 0x7FFF; - - require(exponent <= 16510); // Overflow - if (exponent < 16255) return 0; // Underflow - - uint256 result = uint256(uint128(x)) & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF | 0x10000000000000000000000000000; - - if (exponent < 16367) result >>= 16367 - exponent; - else if (exponent > 16367) result <<= exponent - 16367; - - if (uint128(x) >= 0x80000000000000000000000000000000) { - // Negative - require(result <= 0x8000000000000000000000000000000000000000000000000000000000000000); - return -int256(result); // We rely on overflow behavior here - } else { - require(result <= 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF); - return int256(result); - } - } - } - - /** - * Convert signed 64.64 bit fixed point number into quadruple precision - * number. - * - * @param x signed 64.64 bit fixed point number - * @return quadruple precision number - */ - function from64x64(int128 x) internal pure returns (bytes16) { - unchecked { - if (x == 0) { - return bytes16(0); - } else { - // We rely on overflow behavior here - uint256 result = uint128(x > 0 ? x : -x); - - uint256 msb = mostSignificantBit(result); - if (msb < 112) result <<= 112 - msb; - else if (msb > 112) result >>= msb - 112; - - result = result & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF | 16319 + msb << 112; - if (x < 0) result |= 0x80000000000000000000000000000000; - - return bytes16(uint128(result)); - } - } - } - - /** - * Convert quadruple precision number into signed 64.64 bit fixed point - * number. Revert on overflow. - * - * @param x quadruple precision number - * @return signed 64.64 bit fixed point number - */ - function to64x64(bytes16 x) internal pure returns (int128) { - unchecked { - uint256 exponent = uint128(x) >> 112 & 0x7FFF; - - require(exponent <= 16446); // Overflow - if (exponent < 16319) return 0; // Underflow - - uint256 result = uint256(uint128(x)) & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF | 0x10000000000000000000000000000; - - if (exponent < 16431) result >>= 16431 - exponent; - else if (exponent > 16431) result <<= exponent - 16431; - - if (uint128(x) >= 0x80000000000000000000000000000000) { - // Negative - require(result <= 0x80000000000000000000000000000000); - return -int128(int256(result)); // We rely on overflow behavior here - } else { - require(result <= 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF); - return int128(int256(result)); - } - } - } - - /** - * Convert octuple precision number into quadruple precision number. - * - * @param x octuple precision number - * @return quadruple precision number - */ - function fromOctuple(bytes32 x) internal pure returns (bytes16) { - unchecked { - bool negative = x & 0x8000000000000000000000000000000000000000000000000000000000000000 > 0; - - uint256 exponent = uint256(x) >> 236 & 0x7FFFF; - uint256 significand = uint256(x) & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - - if (exponent == 0x7FFFF) { - if (significand > 0) return NaN; - else return negative ? NEGATIVE_INFINITY : POSITIVE_INFINITY; - } - - if (exponent > 278526) { - return negative ? NEGATIVE_INFINITY : POSITIVE_INFINITY; - } else if (exponent < 245649) { - return negative ? NEGATIVE_ZERO : POSITIVE_ZERO; - } else if (exponent < 245761) { - significand = - (significand | 0x100000000000000000000000000000000000000000000000000000000000) >> 245885 - exponent; - exponent = 0; - } else { - significand >>= 124; - exponent -= 245760; - } - - uint128 result = uint128(significand | exponent << 112); - if (negative) result |= 0x80000000000000000000000000000000; - - return bytes16(result); - } - } - - /** - * Convert quadruple precision number into octuple precision number. - * - * @param x quadruple precision number - * @return octuple precision number - */ - function toOctuple(bytes16 x) internal pure returns (bytes32) { - unchecked { - uint256 exponent = uint128(x) >> 112 & 0x7FFF; - - uint256 result = uint128(x) & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - - if (exponent == 0x7FFF) { - exponent = 0x7FFFF; - } // Infinity or NaN - else if (exponent == 0) { - if (result > 0) { - uint256 msb = mostSignificantBit(result); - result = result << 236 - msb & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - exponent = 245649 + msb; - } - } else { - result <<= 124; - exponent += 245760; - } - - result |= exponent << 236; - if (uint128(x) >= 0x80000000000000000000000000000000) { - result |= 0x8000000000000000000000000000000000000000000000000000000000000000; - } - - return bytes32(result); - } - } - - /** - * Convert double precision number into quadruple precision number. - * - * @param x double precision number - * @return quadruple precision number - */ - function fromDouble(bytes8 x) internal pure returns (bytes16) { - unchecked { - uint256 exponent = uint64(x) >> 52 & 0x7FF; - - uint256 result = uint64(x) & 0xFFFFFFFFFFFFF; - - if (exponent == 0x7FF) { - exponent = 0x7FFF; - } // Infinity or NaN - else if (exponent == 0) { - if (result > 0) { - uint256 msb = mostSignificantBit(result); - result = result << 112 - msb & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - exponent = 15309 + msb; - } - } else { - result <<= 60; - exponent += 15360; - } - - result |= exponent << 112; - if (x & 0x8000000000000000 > 0) { - result |= 0x80000000000000000000000000000000; - } - - return bytes16(uint128(result)); - } - } - - /** - * Convert quadruple precision number into double precision number. - * - * @param x quadruple precision number - * @return double precision number - */ - function toDouble(bytes16 x) internal pure returns (bytes8) { - unchecked { - bool negative = uint128(x) >= 0x80000000000000000000000000000000; - - uint256 exponent = uint128(x) >> 112 & 0x7FFF; - uint256 significand = uint128(x) & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - - if (exponent == 0x7FFF) { - if (significand > 0) { - return 0x7FF8000000000000; - } // NaN - else { - return negative - ? bytes8(0xFFF0000000000000) // -Infinity - : bytes8(0x7FF0000000000000); - } // Infinity - } - - if (exponent > 17406) { - return negative - ? bytes8(0xFFF0000000000000) // -Infinity - : bytes8(0x7FF0000000000000); - } // Infinity - else if (exponent < 15309) { - return negative - ? bytes8(0x8000000000000000) // -0 - : bytes8(0x0000000000000000); - } // 0 - else if (exponent < 15361) { - significand = (significand | 0x10000000000000000000000000000) >> 15421 - exponent; - exponent = 0; - } else { - significand >>= 60; - exponent -= 15360; - } - - uint64 result = uint64(significand | exponent << 52); - if (negative) result |= 0x8000000000000000; - - return bytes8(result); - } - } - - /** - * Test whether given quadruple precision number is NaN. - * - * @param x quadruple precision number - * @return true if x is NaN, false otherwise - */ - function isNaN(bytes16 x) internal pure returns (bool) { - unchecked { - return uint128(x) & 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF > 0x7FFF0000000000000000000000000000; - } - } - - /** - * Test whether given quadruple precision number is positive or negative - * infinity. - * - * @param x quadruple precision number - * @return true if x is positive or negative infinity, false otherwise - */ - function isInfinity(bytes16 x) internal pure returns (bool) { - unchecked { - return uint128(x) & 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF == 0x7FFF0000000000000000000000000000; - } - } - - /** - * Calculate sign of x, i.e. -1 if x is negative, 0 if x if zero, and 1 if x - * is positive. Note that sign (-0) is zero. Revert if x is NaN. - * - * @param x quadruple precision number - * @return sign of x - */ - function sign(bytes16 x) internal pure returns (int8) { - unchecked { - uint128 absoluteX = uint128(x) & 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - - require(absoluteX <= 0x7FFF0000000000000000000000000000); // Not NaN - - if (absoluteX == 0) return 0; - else if (uint128(x) >= 0x80000000000000000000000000000000) return -1; - else return 1; - } - } - - /** - * Calculate sign (x - y). Revert if either argument is NaN, or both - * arguments are infinities of the same sign. - * - * @param x quadruple precision number - * @param y quadruple precision number - * @return sign (x - y) - */ - function gt(bytes16 x, bytes16 y) internal pure returns (int8) { - unchecked { - uint128 absoluteX = uint128(x) & 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - - require(absoluteX <= 0x7FFF0000000000000000000000000000); // Not NaN - - uint128 absoluteY = uint128(y) & 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - - require(absoluteY <= 0x7FFF0000000000000000000000000000); // Not NaN - - // Not infinities of the same sign - require(x != y || absoluteX < 0x7FFF0000000000000000000000000000); - - if (x == y) { - return 0; - } else { - bool negativeX = uint128(x) >= 0x80000000000000000000000000000000; - bool negativeY = uint128(y) >= 0x80000000000000000000000000000000; - - if (negativeX) { - if (negativeY) return absoluteX > absoluteY ? -1 : int8(1); - else return -1; - } else { - if (negativeY) return 1; - else return absoluteX > absoluteY ? int8(1) : -1; - } - } - } - } - - /** - * Test whether x equals y. NaN, infinity, and -infinity are not equal to - * anything. - * - * @param x quadruple precision number - * @param y quadruple precision number - * @return true if x equals to y, false otherwise - */ - function eq(bytes16 x, bytes16 y) internal pure returns (bool) { - unchecked { - if (x == y) { - return uint128(x) & 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF < 0x7FFF0000000000000000000000000000; - } else { - return false; - } - } - } - - /** - * Calculate x + y. Special values behave in the following way: - * - * NaN + x = NaN for any x. - * Infinity + x = Infinity for any finite x. - * -Infinity + x = -Infinity for any finite x. - * Infinity + Infinity = Infinity. - * -Infinity + -Infinity = -Infinity. - * Infinity + -Infinity = -Infinity + Infinity = NaN. - * - * @param x quadruple precision number - * @param y quadruple precision number - * @return quadruple precision number - */ - function add(bytes16 x, bytes16 y) internal pure returns (bytes16) { - unchecked { - uint256 xExponent = uint128(x) >> 112 & 0x7FFF; - uint256 yExponent = uint128(y) >> 112 & 0x7FFF; - - if (xExponent == 0x7FFF) { - if (yExponent == 0x7FFF) { - if (x == y) return x; - else return NaN; - } else { - return x; - } - } else if (yExponent == 0x7FFF) { - return y; - } else { - bool xSign = uint128(x) >= 0x80000000000000000000000000000000; - uint256 xSignifier = uint128(x) & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - if (xExponent == 0) xExponent = 1; - else xSignifier |= 0x10000000000000000000000000000; - - bool ySign = uint128(y) >= 0x80000000000000000000000000000000; - uint256 ySignifier = uint128(y) & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - if (yExponent == 0) yExponent = 1; - else ySignifier |= 0x10000000000000000000000000000; - - if (xSignifier == 0) { - return y == NEGATIVE_ZERO ? POSITIVE_ZERO : y; - } else if (ySignifier == 0) { - return x == NEGATIVE_ZERO ? POSITIVE_ZERO : x; - } else { - int256 delta = int256(xExponent) - int256(yExponent); - - if (xSign == ySign) { - if (delta > 112) { - return x; - } else if (delta > 0) { - ySignifier >>= uint256(delta); - } else if (delta < -112) { - return y; - } else if (delta < 0) { - xSignifier >>= uint256(-delta); - xExponent = yExponent; - } - - xSignifier += ySignifier; - - if (xSignifier >= 0x20000000000000000000000000000) { - xSignifier >>= 1; - xExponent += 1; - } - - if (xExponent == 0x7FFF) { - return xSign ? NEGATIVE_INFINITY : POSITIVE_INFINITY; - } else { - if (xSignifier < 0x10000000000000000000000000000) xExponent = 0; - else xSignifier &= 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - - return bytes16( - uint128( - (xSign ? 0x80000000000000000000000000000000 : 0) | (xExponent << 112) | xSignifier - ) - ); - } - } else { - if (delta > 0) { - xSignifier <<= 1; - xExponent -= 1; - } else if (delta < 0) { - ySignifier <<= 1; - xExponent = yExponent - 1; - } - - if (delta > 112) ySignifier = 1; - else if (delta > 1) ySignifier = (ySignifier - 1 >> uint256(delta - 1)) + 1; - else if (delta < -112) xSignifier = 1; - else if (delta < -1) xSignifier = (xSignifier - 1 >> uint256(-delta - 1)) + 1; - - if (xSignifier >= ySignifier) { - xSignifier -= ySignifier; - } else { - xSignifier = ySignifier - xSignifier; - xSign = ySign; - } - - if (xSignifier == 0) { - return POSITIVE_ZERO; - } - - uint256 msb = mostSignificantBit(xSignifier); - - if (msb == 113) { - xSignifier = xSignifier >> 1 & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - xExponent += 1; - } else if (msb < 112) { - uint256 shift = 112 - msb; - if (xExponent > shift) { - xSignifier = xSignifier << shift & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - xExponent -= shift; - } else { - xSignifier <<= xExponent - 1; - xExponent = 0; - } - } else { - xSignifier &= 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - } - - if (xExponent == 0x7FFF) { - return xSign ? NEGATIVE_INFINITY : POSITIVE_INFINITY; - } else { - return bytes16( - uint128( - (xSign ? 0x80000000000000000000000000000000 : 0) | (xExponent << 112) | xSignifier - ) - ); - } - } - } - } - } - } - - /** - * Calculate x - y. Special values behave in the following way: - * - * NaN - x = NaN for any x. - * Infinity - x = Infinity for any finite x. - * -Infinity - x = -Infinity for any finite x. - * Infinity - -Infinity = Infinity. - * -Infinity - Infinity = -Infinity. - * Infinity - Infinity = -Infinity - -Infinity = NaN. - * - * @param x quadruple precision number - * @param y quadruple precision number - * @return quadruple precision number - */ - function sub(bytes16 x, bytes16 y) internal pure returns (bytes16) { - unchecked { - return add(x, y ^ 0x80000000000000000000000000000000); - } - } - - /** - * Calculate x * y. Special values behave in the following way: - * - * NaN * x = NaN for any x. - * Infinity * x = Infinity for any finite positive x. - * Infinity * x = -Infinity for any finite negative x. - * -Infinity * x = -Infinity for any finite positive x. - * -Infinity * x = Infinity for any finite negative x. - * Infinity * 0 = NaN. - * -Infinity * 0 = NaN. - * Infinity * Infinity = Infinity. - * Infinity * -Infinity = -Infinity. - * -Infinity * Infinity = -Infinity. - * -Infinity * -Infinity = Infinity. - * - * @param x quadruple precision number - * @param y quadruple precision number - * @return quadruple precision number - */ - function mul(bytes16 x, bytes16 y) internal pure returns (bytes16) { - unchecked { - uint256 xExponent = uint128(x) >> 112 & 0x7FFF; - uint256 yExponent = uint128(y) >> 112 & 0x7FFF; - - if (xExponent == 0x7FFF) { - if (yExponent == 0x7FFF) { - if (x == y) return x ^ y & 0x80000000000000000000000000000000; - else if (x ^ y == 0x80000000000000000000000000000000) return x | y; - else return NaN; - } else { - if (y & 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF == 0) return NaN; - else return x ^ y & 0x80000000000000000000000000000000; - } - } else if (yExponent == 0x7FFF) { - if (x & 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF == 0) return NaN; - else return y ^ x & 0x80000000000000000000000000000000; - } else { - uint256 xSignifier = uint128(x) & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - if (xExponent == 0) xExponent = 1; - else xSignifier |= 0x10000000000000000000000000000; - - uint256 ySignifier = uint128(y) & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - if (yExponent == 0) yExponent = 1; - else ySignifier |= 0x10000000000000000000000000000; - - xSignifier *= ySignifier; - if (xSignifier == 0) { - return (x ^ y) & 0x80000000000000000000000000000000 > 0 ? NEGATIVE_ZERO : POSITIVE_ZERO; - } - - xExponent += yExponent; - - uint256 msb = xSignifier >= 0x200000000000000000000000000000000000000000000000000000000 - ? 225 - : xSignifier >= 0x100000000000000000000000000000000000000000000000000000000 - ? 224 - : mostSignificantBit(xSignifier); - - if (xExponent + msb < 16496) { - // Underflow - xExponent = 0; - xSignifier = 0; - } else if (xExponent + msb < 16608) { - // Subnormal - if (xExponent < 16496) { - xSignifier >>= 16496 - xExponent; - } else if (xExponent > 16496) { - xSignifier <<= xExponent - 16496; - } - xExponent = 0; - } else if (xExponent + msb > 49373) { - xExponent = 0x7FFF; - xSignifier = 0; - } else { - if (msb > 112) { - xSignifier >>= msb - 112; - } else if (msb < 112) { - xSignifier <<= 112 - msb; - } - - xSignifier &= 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - - xExponent = xExponent + msb - 16607; - } - - return bytes16( - uint128(uint128((x ^ y) & 0x80000000000000000000000000000000) | xExponent << 112 | xSignifier) - ); - } - } - } - - /** - * Calculate x / y. Special values behave in the following way: - * - * NaN / x = NaN for any x. - * x / NaN = NaN for any x. - * Infinity / x = Infinity for any finite non-negative x. - * Infinity / x = -Infinity for any finite negative x including -0. - * -Infinity / x = -Infinity for any finite non-negative x. - * -Infinity / x = Infinity for any finite negative x including -0. - * x / Infinity = 0 for any finite non-negative x. - * x / -Infinity = -0 for any finite non-negative x. - * x / Infinity = -0 for any finite non-negative x including -0. - * x / -Infinity = 0 for any finite non-negative x including -0. - * - * Infinity / Infinity = NaN. - * Infinity / -Infinity = -NaN. - * -Infinity / Infinity = -NaN. - * -Infinity / -Infinity = NaN. - * - * Division by zero behaves in the following way: - * - * x / 0 = Infinity for any finite positive x. - * x / -0 = -Infinity for any finite positive x. - * x / 0 = -Infinity for any finite negative x. - * x / -0 = Infinity for any finite negative x. - * 0 / 0 = NaN. - * 0 / -0 = NaN. - * -0 / 0 = NaN. - * -0 / -0 = NaN. - * - * @param x quadruple precision number - * @param y quadruple precision number - * @return quadruple precision number - */ - function div(bytes16 x, bytes16 y) internal pure returns (bytes16) { - unchecked { - uint256 xExponent = uint128(x) >> 112 & 0x7FFF; - uint256 yExponent = uint128(y) >> 112 & 0x7FFF; - - if (xExponent == 0x7FFF) { - if (yExponent == 0x7FFF) return NaN; - else return x ^ y & 0x80000000000000000000000000000000; - } else if (yExponent == 0x7FFF) { - if (y & 0x0000FFFFFFFFFFFFFFFFFFFFFFFFFFFF != 0) return NaN; - else return POSITIVE_ZERO | (x ^ y) & 0x80000000000000000000000000000000; - } else if (y & 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF == 0) { - if (x & 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF == 0) return NaN; - else return POSITIVE_INFINITY | (x ^ y) & 0x80000000000000000000000000000000; - } else { - uint256 ySignifier = uint128(y) & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - if (yExponent == 0) yExponent = 1; - else ySignifier |= 0x10000000000000000000000000000; - - uint256 xSignifier = uint128(x) & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - if (xExponent == 0) { - if (xSignifier != 0) { - uint256 shift = 226 - mostSignificantBit(xSignifier); - - xSignifier <<= shift; - - xExponent = 1; - yExponent += shift - 114; - } - } else { - xSignifier = (xSignifier | 0x10000000000000000000000000000) << 114; - } - - xSignifier = xSignifier / ySignifier; - if (xSignifier == 0) { - return (x ^ y) & 0x80000000000000000000000000000000 > 0 ? NEGATIVE_ZERO : POSITIVE_ZERO; - } - - assert(xSignifier >= 0x1000000000000000000000000000); - - uint256 msb = xSignifier >= 0x80000000000000000000000000000 - ? mostSignificantBit(xSignifier) - : xSignifier >= 0x40000000000000000000000000000 - ? 114 - : xSignifier >= 0x20000000000000000000000000000 ? 113 : 112; - - if (xExponent + msb > yExponent + 16497) { - // Overflow - xExponent = 0x7FFF; - xSignifier = 0; - } else if (xExponent + msb + 16380 < yExponent) { - // Underflow - xExponent = 0; - xSignifier = 0; - } else if (xExponent + msb + 16268 < yExponent) { - // Subnormal - if (xExponent + 16380 > yExponent) { - xSignifier <<= xExponent + 16380 - yExponent; - } else if (xExponent + 16380 < yExponent) { - xSignifier >>= yExponent - xExponent - 16380; - } - - xExponent = 0; - } else { - // Normal - if (msb > 112) { - xSignifier >>= msb - 112; - } - - xSignifier &= 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - - xExponent = xExponent + msb + 16269 - yExponent; - } - - return bytes16( - uint128(uint128((x ^ y) & 0x80000000000000000000000000000000) | xExponent << 112 | xSignifier) - ); - } - } - } - - /** - * Calculate -x. - * - * @param x quadruple precision number - * @return quadruple precision number - */ - function neg(bytes16 x) internal pure returns (bytes16) { - unchecked { - return x ^ 0x80000000000000000000000000000000; - } - } - - /** - * Calculate |x|. - * - * @param x quadruple precision number - * @return quadruple precision number - */ - function abs(bytes16 x) internal pure returns (bytes16) { - unchecked { - return x & 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - } - } - - /** - * Calculate square root of x. Return NaN on negative x excluding -0. - * - * @param x quadruple precision number - * @return quadruple precision number - */ - function sqrt(bytes16 x) internal pure returns (bytes16) { - unchecked { - if (uint128(x) > 0x80000000000000000000000000000000) { - return NaN; - } else { - uint256 xExponent = uint128(x) >> 112 & 0x7FFF; - if (xExponent == 0x7FFF) { - return x; - } else { - uint256 xSignifier = uint128(x) & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - if (xExponent == 0) xExponent = 1; - else xSignifier |= 0x10000000000000000000000000000; - - if (xSignifier == 0) return POSITIVE_ZERO; - - bool oddExponent = xExponent & 0x1 == 0; - xExponent = xExponent + 16383 >> 1; - - if (oddExponent) { - if (xSignifier >= 0x10000000000000000000000000000) { - xSignifier <<= 113; - } else { - uint256 msb = mostSignificantBit(xSignifier); - uint256 shift = (226 - msb) & 0xFE; - xSignifier <<= shift; - xExponent -= shift - 112 >> 1; - } - } else { - if (xSignifier >= 0x10000000000000000000000000000) { - xSignifier <<= 112; - } else { - uint256 msb = mostSignificantBit(xSignifier); - uint256 shift = (225 - msb) & 0xFE; - xSignifier <<= shift; - xExponent -= shift - 112 >> 1; - } - } - - uint256 r = 0x10000000000000000000000000000; - r = (r + xSignifier / r) >> 1; - r = (r + xSignifier / r) >> 1; - r = (r + xSignifier / r) >> 1; - r = (r + xSignifier / r) >> 1; - r = (r + xSignifier / r) >> 1; - r = (r + xSignifier / r) >> 1; - r = (r + xSignifier / r) >> 1; // Seven iterations should be enough - uint256 r1 = xSignifier / r; - if (r1 < r) r = r1; - - return bytes16(uint128(xExponent << 112 | r & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF)); - } - } - } - } - - /** - * Calculate binary logarithm of x. Return NaN on negative x excluding -0. - * - * @param x quadruple precision number - * @return quadruple precision number - */ - function log_2(bytes16 x) internal pure returns (bytes16) { - unchecked { - if (uint128(x) > 0x80000000000000000000000000000000) { - return NaN; - } else if (x == 0x3FFF0000000000000000000000000000) { - return POSITIVE_ZERO; - } else { - uint256 xExponent = uint128(x) >> 112 & 0x7FFF; - if (xExponent == 0x7FFF) { - return x; - } else { - uint256 xSignifier = uint128(x) & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - if (xExponent == 0) xExponent = 1; - else xSignifier |= 0x10000000000000000000000000000; - - if (xSignifier == 0) return NEGATIVE_INFINITY; - - bool resultNegative; - uint256 resultExponent = 16495; - uint256 resultSignifier; - - if (xExponent >= 0x3FFF) { - resultNegative = false; - resultSignifier = xExponent - 0x3FFF; - xSignifier <<= 15; - } else { - resultNegative = true; - if (xSignifier >= 0x10000000000000000000000000000) { - resultSignifier = 0x3FFE - xExponent; - xSignifier <<= 15; - } else { - uint256 msb = mostSignificantBit(xSignifier); - resultSignifier = 16493 - msb; - xSignifier <<= 127 - msb; - } - } - - if (xSignifier == 0x80000000000000000000000000000000) { - if (resultNegative) resultSignifier += 1; - uint256 shift = 112 - mostSignificantBit(resultSignifier); - resultSignifier <<= shift; - resultExponent -= shift; - } else { - uint256 bb = resultNegative ? 1 : 0; - while (resultSignifier < 0x10000000000000000000000000000) { - resultSignifier <<= 1; - resultExponent -= 1; - - xSignifier *= xSignifier; - uint256 b = xSignifier >> 255; - resultSignifier += b ^ bb; - xSignifier >>= 127 + b; - } - } - - return bytes16( - uint128( - (resultNegative ? 0x80000000000000000000000000000000 : 0) | resultExponent << 112 - | resultSignifier & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF - ) - ); - } - } - } - } - - /** - * Calculate natural logarithm of x. Return NaN on negative x excluding -0. - * - * @param x quadruple precision number - * @return quadruple precision number - */ - function ln(bytes16 x) internal pure returns (bytes16) { - unchecked { - return mul(log_2(x), 0x3FFE62E42FEFA39EF35793C7673007E5); - } - } - - /** - * Calculate 2^x. - * - * @param x quadruple precision number - * @return quadruple precision number - */ - function pow_2(bytes16 x) internal pure returns (bytes16) { - unchecked { - bool xNegative = uint128(x) > 0x80000000000000000000000000000000; - uint256 xExponent = uint128(x) >> 112 & 0x7FFF; - uint256 xSignifier = uint128(x) & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - - if (xExponent == 0x7FFF && xSignifier != 0) { - return NaN; - } else if (xExponent > 16397) { - return xNegative ? POSITIVE_ZERO : POSITIVE_INFINITY; - } else if (xExponent < 16255) { - return 0x3FFF0000000000000000000000000000; - } else { - if (xExponent == 0) xExponent = 1; - else xSignifier |= 0x10000000000000000000000000000; - - if (xExponent > 16367) { - xSignifier <<= xExponent - 16367; - } else if (xExponent < 16367) { - xSignifier >>= 16367 - xExponent; - } - - if (xNegative && xSignifier > 0x406E00000000000000000000000000000000) { - return POSITIVE_ZERO; - } - - if (!xNegative && xSignifier > 0x3FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) { - return POSITIVE_INFINITY; - } - - uint256 resultExponent = xSignifier >> 128; - xSignifier &= 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - if (xNegative && xSignifier != 0) { - xSignifier = ~xSignifier; - resultExponent += 1; - } - - uint256 resultSignifier = 0x80000000000000000000000000000000; - if (xSignifier & 0x80000000000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x16A09E667F3BCC908B2FB1366EA957D3E >> 128; - } - if (xSignifier & 0x40000000000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x1306FE0A31B7152DE8D5A46305C85EDEC >> 128; - } - if (xSignifier & 0x20000000000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x1172B83C7D517ADCDF7C8C50EB14A791F >> 128; - } - if (xSignifier & 0x10000000000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x10B5586CF9890F6298B92B71842A98363 >> 128; - } - if (xSignifier & 0x8000000000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x1059B0D31585743AE7C548EB68CA417FD >> 128; - } - if (xSignifier & 0x4000000000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x102C9A3E778060EE6F7CACA4F7A29BDE8 >> 128; - } - if (xSignifier & 0x2000000000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x10163DA9FB33356D84A66AE336DCDFA3F >> 128; - } - if (xSignifier & 0x1000000000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x100B1AFA5ABCBED6129AB13EC11DC9543 >> 128; - } - if (xSignifier & 0x800000000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x10058C86DA1C09EA1FF19D294CF2F679B >> 128; - } - if (xSignifier & 0x400000000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x1002C605E2E8CEC506D21BFC89A23A00F >> 128; - } - if (xSignifier & 0x200000000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x100162F3904051FA128BCA9C55C31E5DF >> 128; - } - if (xSignifier & 0x100000000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x1000B175EFFDC76BA38E31671CA939725 >> 128; - } - if (xSignifier & 0x80000000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x100058BA01FB9F96D6CACD4B180917C3D >> 128; - } - if (xSignifier & 0x40000000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x10002C5CC37DA9491D0985C348C68E7B3 >> 128; - } - if (xSignifier & 0x20000000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x1000162E525EE054754457D5995292026 >> 128; - } - if (xSignifier & 0x10000000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x10000B17255775C040618BF4A4ADE83FC >> 128; - } - if (xSignifier & 0x8000000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x1000058B91B5BC9AE2EED81E9B7D4CFAB >> 128; - } - if (xSignifier & 0x4000000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x100002C5C89D5EC6CA4D7C8ACC017B7C9 >> 128; - } - if (xSignifier & 0x2000000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x10000162E43F4F831060E02D839A9D16D >> 128; - } - if (xSignifier & 0x1000000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x100000B1721BCFC99D9F890EA06911763 >> 128; - } - if (xSignifier & 0x800000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x10000058B90CF1E6D97F9CA14DBCC1628 >> 128; - } - if (xSignifier & 0x400000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x1000002C5C863B73F016468F6BAC5CA2B >> 128; - } - if (xSignifier & 0x200000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x100000162E430E5A18F6119E3C02282A5 >> 128; - } - if (xSignifier & 0x100000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x1000000B1721835514B86E6D96EFD1BFE >> 128; - } - if (xSignifier & 0x80000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x100000058B90C0B48C6BE5DF846C5B2EF >> 128; - } - if (xSignifier & 0x40000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x10000002C5C8601CC6B9E94213C72737A >> 128; - } - if (xSignifier & 0x20000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x1000000162E42FFF037DF38AA2B219F06 >> 128; - } - if (xSignifier & 0x10000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x10000000B17217FBA9C739AA5819F44F9 >> 128; - } - if (xSignifier & 0x8000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x1000000058B90BFCDEE5ACD3C1CEDC823 >> 128; - } - if (xSignifier & 0x4000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x100000002C5C85FE31F35A6A30DA1BE50 >> 128; - } - if (xSignifier & 0x2000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x10000000162E42FF0999CE3541B9FFFCF >> 128; - } - if (xSignifier & 0x1000000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x100000000B17217F80F4EF5AADDA45554 >> 128; - } - if (xSignifier & 0x800000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x10000000058B90BFBF8479BD5A81B51AD >> 128; - } - if (xSignifier & 0x400000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x1000000002C5C85FDF84BD62AE30A74CC >> 128; - } - if (xSignifier & 0x200000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x100000000162E42FEFB2FED257559BDAA >> 128; - } - if (xSignifier & 0x100000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x1000000000B17217F7D5A7716BBA4A9AE >> 128; - } - if (xSignifier & 0x80000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x100000000058B90BFBE9DDBAC5E109CCE >> 128; - } - if (xSignifier & 0x40000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x10000000002C5C85FDF4B15DE6F17EB0D >> 128; - } - if (xSignifier & 0x20000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x1000000000162E42FEFA494F1478FDE05 >> 128; - } - if (xSignifier & 0x10000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x10000000000B17217F7D20CF927C8E94C >> 128; - } - if (xSignifier & 0x8000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x1000000000058B90BFBE8F71CB4E4B33D >> 128; - } - if (xSignifier & 0x4000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x100000000002C5C85FDF477B662B26945 >> 128; - } - if (xSignifier & 0x2000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x10000000000162E42FEFA3AE53369388C >> 128; - } - if (xSignifier & 0x1000000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x100000000000B17217F7D1D351A389D40 >> 128; - } - if (xSignifier & 0x800000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x10000000000058B90BFBE8E8B2D3D4EDE >> 128; - } - if (xSignifier & 0x400000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x1000000000002C5C85FDF4741BEA6E77E >> 128; - } - if (xSignifier & 0x200000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x100000000000162E42FEFA39FE95583C2 >> 128; - } - if (xSignifier & 0x100000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x1000000000000B17217F7D1CFB72B45E1 >> 128; - } - if (xSignifier & 0x80000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x100000000000058B90BFBE8E7CC35C3F0 >> 128; - } - if (xSignifier & 0x40000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x10000000000002C5C85FDF473E242EA38 >> 128; - } - if (xSignifier & 0x20000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x1000000000000162E42FEFA39F02B772C >> 128; - } - if (xSignifier & 0x10000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x10000000000000B17217F7D1CF7D83C1A >> 128; - } - if (xSignifier & 0x8000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x1000000000000058B90BFBE8E7BDCBE2E >> 128; - } - if (xSignifier & 0x4000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x100000000000002C5C85FDF473DEA871F >> 128; - } - if (xSignifier & 0x2000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x10000000000000162E42FEFA39EF44D91 >> 128; - } - if (xSignifier & 0x1000000000000000000 > 0) { - resultSignifier = resultSignifier * 0x100000000000000B17217F7D1CF79E949 >> 128; - } - if (xSignifier & 0x800000000000000000 > 0) { - resultSignifier = resultSignifier * 0x10000000000000058B90BFBE8E7BCE544 >> 128; - } - if (xSignifier & 0x400000000000000000 > 0) { - resultSignifier = resultSignifier * 0x1000000000000002C5C85FDF473DE6ECA >> 128; - } - if (xSignifier & 0x200000000000000000 > 0) { - resultSignifier = resultSignifier * 0x100000000000000162E42FEFA39EF366F >> 128; - } - if (xSignifier & 0x100000000000000000 > 0) { - resultSignifier = resultSignifier * 0x1000000000000000B17217F7D1CF79AFA >> 128; - } - if (xSignifier & 0x80000000000000000 > 0) { - resultSignifier = resultSignifier * 0x100000000000000058B90BFBE8E7BCD6D >> 128; - } - if (xSignifier & 0x40000000000000000 > 0) { - resultSignifier = resultSignifier * 0x10000000000000002C5C85FDF473DE6B2 >> 128; - } - if (xSignifier & 0x20000000000000000 > 0) { - resultSignifier = resultSignifier * 0x1000000000000000162E42FEFA39EF358 >> 128; - } - if (xSignifier & 0x10000000000000000 > 0) { - resultSignifier = resultSignifier * 0x10000000000000000B17217F7D1CF79AB >> 128; - } - if (xSignifier & 0x8000000000000000 > 0) { - resultSignifier = resultSignifier * 0x1000000000000000058B90BFBE8E7BCD5 >> 128; - } - if (xSignifier & 0x4000000000000000 > 0) { - resultSignifier = resultSignifier * 0x100000000000000002C5C85FDF473DE6A >> 128; - } - if (xSignifier & 0x2000000000000000 > 0) { - resultSignifier = resultSignifier * 0x10000000000000000162E42FEFA39EF34 >> 128; - } - if (xSignifier & 0x1000000000000000 > 0) { - resultSignifier = resultSignifier * 0x100000000000000000B17217F7D1CF799 >> 128; - } - if (xSignifier & 0x800000000000000 > 0) { - resultSignifier = resultSignifier * 0x10000000000000000058B90BFBE8E7BCC >> 128; - } - if (xSignifier & 0x400000000000000 > 0) { - resultSignifier = resultSignifier * 0x1000000000000000002C5C85FDF473DE5 >> 128; - } - if (xSignifier & 0x200000000000000 > 0) { - resultSignifier = resultSignifier * 0x100000000000000000162E42FEFA39EF2 >> 128; - } - if (xSignifier & 0x100000000000000 > 0) { - resultSignifier = resultSignifier * 0x1000000000000000000B17217F7D1CF78 >> 128; - } - if (xSignifier & 0x80000000000000 > 0) { - resultSignifier = resultSignifier * 0x100000000000000000058B90BFBE8E7BB >> 128; - } - if (xSignifier & 0x40000000000000 > 0) { - resultSignifier = resultSignifier * 0x10000000000000000002C5C85FDF473DD >> 128; - } - if (xSignifier & 0x20000000000000 > 0) { - resultSignifier = resultSignifier * 0x1000000000000000000162E42FEFA39EE >> 128; - } - if (xSignifier & 0x10000000000000 > 0) { - resultSignifier = resultSignifier * 0x10000000000000000000B17217F7D1CF6 >> 128; - } - if (xSignifier & 0x8000000000000 > 0) { - resultSignifier = resultSignifier * 0x1000000000000000000058B90BFBE8E7A >> 128; - } - if (xSignifier & 0x4000000000000 > 0) { - resultSignifier = resultSignifier * 0x100000000000000000002C5C85FDF473C >> 128; - } - if (xSignifier & 0x2000000000000 > 0) { - resultSignifier = resultSignifier * 0x10000000000000000000162E42FEFA39D >> 128; - } - if (xSignifier & 0x1000000000000 > 0) { - resultSignifier = resultSignifier * 0x100000000000000000000B17217F7D1CE >> 128; - } - if (xSignifier & 0x800000000000 > 0) { - resultSignifier = resultSignifier * 0x10000000000000000000058B90BFBE8E6 >> 128; - } - if (xSignifier & 0x400000000000 > 0) { - resultSignifier = resultSignifier * 0x1000000000000000000002C5C85FDF472 >> 128; - } - if (xSignifier & 0x200000000000 > 0) { - resultSignifier = resultSignifier * 0x100000000000000000000162E42FEFA38 >> 128; - } - if (xSignifier & 0x100000000000 > 0) { - resultSignifier = resultSignifier * 0x1000000000000000000000B17217F7D1B >> 128; - } - if (xSignifier & 0x80000000000 > 0) { - resultSignifier = resultSignifier * 0x100000000000000000000058B90BFBE8D >> 128; - } - if (xSignifier & 0x40000000000 > 0) { - resultSignifier = resultSignifier * 0x10000000000000000000002C5C85FDF46 >> 128; - } - if (xSignifier & 0x20000000000 > 0) { - resultSignifier = resultSignifier * 0x1000000000000000000000162E42FEFA2 >> 128; - } - if (xSignifier & 0x10000000000 > 0) { - resultSignifier = resultSignifier * 0x10000000000000000000000B17217F7D0 >> 128; - } - if (xSignifier & 0x8000000000 > 0) { - resultSignifier = resultSignifier * 0x1000000000000000000000058B90BFBE7 >> 128; - } - if (xSignifier & 0x4000000000 > 0) { - resultSignifier = resultSignifier * 0x100000000000000000000002C5C85FDF3 >> 128; - } - if (xSignifier & 0x2000000000 > 0) { - resultSignifier = resultSignifier * 0x10000000000000000000000162E42FEF9 >> 128; - } - if (xSignifier & 0x1000000000 > 0) { - resultSignifier = resultSignifier * 0x100000000000000000000000B17217F7C >> 128; - } - if (xSignifier & 0x800000000 > 0) { - resultSignifier = resultSignifier * 0x10000000000000000000000058B90BFBD >> 128; - } - if (xSignifier & 0x400000000 > 0) { - resultSignifier = resultSignifier * 0x1000000000000000000000002C5C85FDE >> 128; - } - if (xSignifier & 0x200000000 > 0) { - resultSignifier = resultSignifier * 0x100000000000000000000000162E42FEE >> 128; - } - if (xSignifier & 0x100000000 > 0) { - resultSignifier = resultSignifier * 0x1000000000000000000000000B17217F6 >> 128; - } - if (xSignifier & 0x80000000 > 0) { - resultSignifier = resultSignifier * 0x100000000000000000000000058B90BFA >> 128; - } - if (xSignifier & 0x40000000 > 0) { - resultSignifier = resultSignifier * 0x10000000000000000000000002C5C85FC >> 128; - } - if (xSignifier & 0x20000000 > 0) { - resultSignifier = resultSignifier * 0x1000000000000000000000000162E42FD >> 128; - } - if (xSignifier & 0x10000000 > 0) { - resultSignifier = resultSignifier * 0x10000000000000000000000000B17217E >> 128; - } - if (xSignifier & 0x8000000 > 0) { - resultSignifier = resultSignifier * 0x1000000000000000000000000058B90BE >> 128; - } - if (xSignifier & 0x4000000 > 0) { - resultSignifier = resultSignifier * 0x100000000000000000000000002C5C85E >> 128; - } - if (xSignifier & 0x2000000 > 0) { - resultSignifier = resultSignifier * 0x10000000000000000000000000162E42E >> 128; - } - if (xSignifier & 0x1000000 > 0) { - resultSignifier = resultSignifier * 0x100000000000000000000000000B17216 >> 128; - } - if (xSignifier & 0x800000 > 0) { - resultSignifier = resultSignifier * 0x10000000000000000000000000058B90A >> 128; - } - if (xSignifier & 0x400000 > 0) { - resultSignifier = resultSignifier * 0x1000000000000000000000000002C5C84 >> 128; - } - if (xSignifier & 0x200000 > 0) { - resultSignifier = resultSignifier * 0x100000000000000000000000000162E41 >> 128; - } - if (xSignifier & 0x100000 > 0) { - resultSignifier = resultSignifier * 0x1000000000000000000000000000B1720 >> 128; - } - if (xSignifier & 0x80000 > 0) { - resultSignifier = resultSignifier * 0x100000000000000000000000000058B8F >> 128; - } - if (xSignifier & 0x40000 > 0) { - resultSignifier = resultSignifier * 0x10000000000000000000000000002C5C7 >> 128; - } - if (xSignifier & 0x20000 > 0) { - resultSignifier = resultSignifier * 0x1000000000000000000000000000162E3 >> 128; - } - if (xSignifier & 0x10000 > 0) { - resultSignifier = resultSignifier * 0x10000000000000000000000000000B171 >> 128; - } - if (xSignifier & 0x8000 > 0) { - resultSignifier = resultSignifier * 0x1000000000000000000000000000058B8 >> 128; - } - if (xSignifier & 0x4000 > 0) { - resultSignifier = resultSignifier * 0x100000000000000000000000000002C5B >> 128; - } - if (xSignifier & 0x2000 > 0) { - resultSignifier = resultSignifier * 0x10000000000000000000000000000162D >> 128; - } - if (xSignifier & 0x1000 > 0) { - resultSignifier = resultSignifier * 0x100000000000000000000000000000B16 >> 128; - } - if (xSignifier & 0x800 > 0) { - resultSignifier = resultSignifier * 0x10000000000000000000000000000058A >> 128; - } - if (xSignifier & 0x400 > 0) { - resultSignifier = resultSignifier * 0x1000000000000000000000000000002C4 >> 128; - } - if (xSignifier & 0x200 > 0) { - resultSignifier = resultSignifier * 0x100000000000000000000000000000161 >> 128; - } - if (xSignifier & 0x100 > 0) { - resultSignifier = resultSignifier * 0x1000000000000000000000000000000B0 >> 128; - } - if (xSignifier & 0x80 > 0) { - resultSignifier = resultSignifier * 0x100000000000000000000000000000057 >> 128; - } - if (xSignifier & 0x40 > 0) { - resultSignifier = resultSignifier * 0x10000000000000000000000000000002B >> 128; - } - if (xSignifier & 0x20 > 0) { - resultSignifier = resultSignifier * 0x100000000000000000000000000000015 >> 128; - } - if (xSignifier & 0x10 > 0) { - resultSignifier = resultSignifier * 0x10000000000000000000000000000000A >> 128; - } - if (xSignifier & 0x8 > 0) { - resultSignifier = resultSignifier * 0x100000000000000000000000000000004 >> 128; - } - if (xSignifier & 0x4 > 0) { - resultSignifier = resultSignifier * 0x100000000000000000000000000000001 >> 128; - } - - if (!xNegative) { - resultSignifier = resultSignifier >> 15 & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - resultExponent += 0x3FFF; - } else if (resultExponent <= 0x3FFE) { - resultSignifier = resultSignifier >> 15 & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - resultExponent = 0x3FFF - resultExponent; - } else { - resultSignifier = resultSignifier >> resultExponent - 16367; - resultExponent = 0; - } - - return bytes16(uint128(resultExponent << 112 | resultSignifier)); - } - } - } - - /** - * Calculate e^x. - * - * @param x quadruple precision number - * @return quadruple precision number - */ - function exp(bytes16 x) internal pure returns (bytes16) { - unchecked { - return pow_2(mul(x, 0x3FFF71547652B82FE1777D0FFDA0D23A)); - } - } - - /** - * Get index of the most significant non-zero bit in binary representation of - * x. Reverts if x is zero. - * - * @return index of the most significant non-zero bit in binary representation - * of x - */ - function mostSignificantBit(uint256 x) private pure returns (uint256) { - unchecked { - require(x > 0); - - uint256 result = 0; - - if (x >= 0x100000000000000000000000000000000) { - x >>= 128; - result += 128; - } - if (x >= 0x10000000000000000) { - x >>= 64; - result += 64; - } - if (x >= 0x100000000) { - x >>= 32; - result += 32; - } - if (x >= 0x10000) { - x >>= 16; - result += 16; - } - if (x >= 0x100) { - x >>= 8; - result += 8; - } - if (x >= 0x10) { - x >>= 4; - result += 4; - } - if (x >= 0x4) { - x >>= 2; - result += 2; - } - if (x >= 0x2) result += 1; // No need to shift x anymore - - return result; - } - } -} diff --git a/contracts/libraries/TWAMM/OrderPool.sol b/contracts/libraries/TWAMM/OrderPool.sol deleted file mode 100644 index 4822dbff..00000000 --- a/contracts/libraries/TWAMM/OrderPool.sol +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.15; - -/// @title TWAMM OrderPool - Represents an OrderPool inside of a TWAMM -library OrderPool { - /// @notice Information related to a long term order pool. - /// @member sellRateCurrent The total current sell rate (sellAmount / second) among all orders - /// @member sellRateEndingAtInterval Mapping (timestamp => sellRate) The amount of expiring sellRate at this interval - /// @member earningsFactor Sum of (salesEarnings_k / salesRate_k) over every period k. Stored as Fixed Point X96. - /// @member earningsFactorAtInterval Mapping (timestamp => sellRate) The earnings factor accrued by a certain time interval. Stored as Fixed Point X96. - struct State { - uint256 sellRateCurrent; - mapping(uint256 => uint256) sellRateEndingAtInterval; - // - uint256 earningsFactorCurrent; - mapping(uint256 => uint256) earningsFactorAtInterval; - } - - // Performs all updates on an OrderPool that must happen when hitting an expiration interval with expiring orders - function advanceToInterval(State storage self, uint256 expiration, uint256 earningsFactor) internal { - unchecked { - self.earningsFactorCurrent += earningsFactor; - self.earningsFactorAtInterval[expiration] = self.earningsFactorCurrent; - self.sellRateCurrent -= self.sellRateEndingAtInterval[expiration]; - } - } - - // Performs all the updates on an OrderPool that must happen when updating to the current time not on an interval - function advanceToCurrentTime(State storage self, uint256 earningsFactor) internal { - unchecked { - self.earningsFactorCurrent += earningsFactor; - } - } -} diff --git a/contracts/libraries/TWAMM/TwammMath.sol b/contracts/libraries/TWAMM/TwammMath.sol deleted file mode 100644 index a5994b51..00000000 --- a/contracts/libraries/TWAMM/TwammMath.sol +++ /dev/null @@ -1,179 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.15; - -import {ABDKMathQuad} from "./ABDKMathQuad.sol"; -import {FixedPoint96} from "@uniswap/v4-core/src/libraries/FixedPoint96.sol"; -import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; -import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; - -/// @title TWAMM Math - Pure functions for TWAMM math calculations -library TwammMath { - using ABDKMathQuad for bytes16; - using ABDKMathQuad for uint256; - using ABDKMathQuad for uint160; - using ABDKMathQuad for uint128; - using SafeCast for uint256; - - // ABDKMathQuad FixedPoint96.Q96.fromUInt() - bytes16 internal constant Q96 = 0x405f0000000000000000000000000000; - - bytes16 internal constant ONE = 0x3fff0000000000000000000000000000; - //// @dev The minimum value that a pool price can equal, represented in bytes. - // (TickMath.MIN_SQRT_RATIO + 1).fromUInt() - bytes16 internal constant MIN_SQRT_RATIO_BYTES = 0x401f000276a400000000000000000000; - //// @dev The maximum value that a pool price can equal, represented in bytes. - // (TickMath.MAX_SQRT_RATIO - 1).fromUInt() - bytes16 internal constant MAX_SQRT_RATIO_BYTES = 0x409efffb12c7dfa3f8d4a0c91092bb2a; - - struct PriceParamsBytes16 { - bytes16 sqrtSellRatio; - bytes16 sqrtSellRate; - bytes16 secondsElapsed; - bytes16 sqrtPrice; - bytes16 liquidity; - } - - struct ExecutionUpdateParams { - uint256 secondsElapsedX96; - uint160 sqrtPriceX96; - uint128 liquidity; - uint256 sellRateCurrent0; - uint256 sellRateCurrent1; - } - - function getNewSqrtPriceX96(ExecutionUpdateParams memory params) internal pure returns (uint160 newSqrtPriceX96) { - bytes16 sellRateBytes0 = params.sellRateCurrent0.fromUInt(); - bytes16 sellRateBytes1 = params.sellRateCurrent1.fromUInt(); - bytes16 sqrtSellRateBytes = sellRateBytes0.mul(sellRateBytes1).sqrt(); - bytes16 sqrtSellRatioX96Bytes = sellRateBytes1.div(sellRateBytes0).sqrt().mul(Q96); - - PriceParamsBytes16 memory priceParams = PriceParamsBytes16({ - sqrtSellRatio: sqrtSellRatioX96Bytes.div(Q96), - sqrtSellRate: sqrtSellRateBytes, - secondsElapsed: params.secondsElapsedX96.fromUInt().div(Q96), - sqrtPrice: params.sqrtPriceX96.fromUInt().div(Q96), - liquidity: params.liquidity.fromUInt() - }); - - bytes16 newSqrtPriceBytesX96 = calculateNewSqrtPrice(priceParams).mul(Q96); - bool isOverflow = newSqrtPriceBytesX96.isInfinity() || newSqrtPriceBytesX96.isNaN(); - bytes16 newSqrtPriceX96Bytes = isOverflow ? sqrtSellRatioX96Bytes : newSqrtPriceBytesX96; - - newSqrtPriceX96 = getSqrtPriceWithinBounds( - params.sellRateCurrent0 > params.sellRateCurrent1, newSqrtPriceX96Bytes - ).toUInt().toUint160(); - } - - function getSqrtPriceWithinBounds(bool zeroForOne, bytes16 desiredPriceX96) - internal - pure - returns (bytes16 newSqrtPriceX96) - { - if (zeroForOne) { - newSqrtPriceX96 = MIN_SQRT_RATIO_BYTES.gt(desiredPriceX96) == 1 ? MIN_SQRT_RATIO_BYTES : desiredPriceX96; - } else { - newSqrtPriceX96 = desiredPriceX96.gt(MAX_SQRT_RATIO_BYTES) == 1 ? MAX_SQRT_RATIO_BYTES : desiredPriceX96; - } - } - - function calculateEarningsUpdates(ExecutionUpdateParams memory params, uint160 finalSqrtPriceX96) - internal - pure - returns (uint256 earningsFactorPool0, uint256 earningsFactorPool1) - { - bytes16 sellRateBytes0 = params.sellRateCurrent0.fromUInt(); - bytes16 sellRateBytes1 = params.sellRateCurrent1.fromUInt(); - - bytes16 sellRatio = sellRateBytes1.div(sellRateBytes0); - bytes16 sqrtSellRate = sellRateBytes0.mul(sellRateBytes1).sqrt(); - - EarningsFactorParams memory earningsFactorParams = EarningsFactorParams({ - secondsElapsed: params.secondsElapsedX96.fromUInt().div(Q96), - sellRatio: sellRatio, - sqrtSellRate: sqrtSellRate, - prevSqrtPrice: params.sqrtPriceX96.fromUInt().div(Q96), - newSqrtPrice: finalSqrtPriceX96.fromUInt().div(Q96), - liquidity: params.liquidity.fromUInt() - }); - - // Trade the amm orders. - // If liquidity is 0, it trades the twamm orders against each other for the time duration. - earningsFactorPool0 = getEarningsFactorPool0(earningsFactorParams).mul(Q96).toUInt(); - earningsFactorPool1 = getEarningsFactorPool1(earningsFactorParams).mul(Q96).toUInt(); - } - - struct calculateTimeBetweenTicksParams { - uint256 liquidity; - uint160 sqrtPriceStartX96; - uint160 sqrtPriceEndX96; - uint256 sellRate0; - uint256 sellRate1; - } - - /// @notice Used when crossing an initialized tick. Can extract the amount of seconds it took to cross - /// the tick, and recalibrate the calculation from there to accommodate liquidity changes - function calculateTimeBetweenTicks( - uint256 liquidity, - uint160 sqrtPriceStartX96, - uint160 sqrtPriceEndX96, - uint256 sellRate0, - uint256 sellRate1 - ) internal pure returns (uint256 secondsBetween) { - bytes16 sellRate0Bytes = sellRate0.fromUInt(); - bytes16 sellRate1Bytes = sellRate1.fromUInt(); - bytes16 sqrtPriceStartX96Bytes = sqrtPriceStartX96.fromUInt(); - bytes16 sqrtPriceEndX96Bytes = sqrtPriceEndX96.fromUInt(); - bytes16 sqrtSellRatioX96 = sellRate1Bytes.div(sellRate0Bytes).sqrt().mul(Q96); - bytes16 sqrtSellRate = sellRate0Bytes.mul(sellRate1Bytes).sqrt(); - - bytes16 multiple = getTimeBetweenTicksMultiple(sqrtSellRatioX96, sqrtPriceStartX96Bytes, sqrtPriceEndX96Bytes); - bytes16 numerator = multiple.mul(liquidity.fromUInt()); - bytes16 denominator = uint256(2).fromUInt().mul(sqrtSellRate); - return numerator.mul(Q96).div(denominator).toUInt(); - } - - function getTimeBetweenTicksMultiple(bytes16 sqrtSellRatioX96, bytes16 sqrtPriceStartX96, bytes16 sqrtPriceEndX96) - private - pure - returns (bytes16 multiple) - { - bytes16 multiple1 = sqrtSellRatioX96.add(sqrtPriceEndX96).div(sqrtSellRatioX96.sub(sqrtPriceEndX96)); - bytes16 multiple2 = sqrtSellRatioX96.sub(sqrtPriceStartX96).div(sqrtSellRatioX96.add(sqrtPriceStartX96)); - return multiple1.mul(multiple2).ln(); - } - - struct EarningsFactorParams { - bytes16 secondsElapsed; - bytes16 sellRatio; - bytes16 sqrtSellRate; - bytes16 prevSqrtPrice; - bytes16 newSqrtPrice; - bytes16 liquidity; - } - - function getEarningsFactorPool0(EarningsFactorParams memory params) private pure returns (bytes16 earningsFactor) { - bytes16 minuend = params.sellRatio.mul(params.secondsElapsed); - bytes16 subtrahend = params.liquidity.mul(params.sellRatio.sqrt()).mul( - params.newSqrtPrice.sub(params.prevSqrtPrice) - ).div(params.sqrtSellRate); - return minuend.sub(subtrahend); - } - - function getEarningsFactorPool1(EarningsFactorParams memory params) private pure returns (bytes16 earningsFactor) { - bytes16 minuend = params.secondsElapsed.div(params.sellRatio); - bytes16 subtrahend = params.liquidity.mul(reciprocal(params.sellRatio.sqrt())).mul( - reciprocal(params.newSqrtPrice).sub(reciprocal(params.prevSqrtPrice)) - ).div(params.sqrtSellRate); - return minuend.sub(subtrahend); - } - - function calculateNewSqrtPrice(PriceParamsBytes16 memory params) private pure returns (bytes16 newSqrtPrice) { - bytes16 pow = uint256(2).fromUInt().mul(params.sqrtSellRate).mul(params.secondsElapsed).div(params.liquidity); - bytes16 c = params.sqrtSellRatio.sub(params.sqrtPrice).div(params.sqrtSellRatio.add(params.sqrtPrice)); - newSqrtPrice = params.sqrtSellRatio.mul(pow.exp().sub(c)).div(pow.exp().add(c)); - } - - function reciprocal(bytes16 n) private pure returns (bytes16) { - return ONE.div(n); - } -} diff --git a/contracts/libraries/TransferHelper.sol b/contracts/libraries/TransferHelper.sol deleted file mode 100644 index 9ab40d9e..00000000 --- a/contracts/libraries/TransferHelper.sol +++ /dev/null @@ -1,54 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity ^0.8.15; - -import {IERC20Minimal} from "@uniswap/v4-core/src/interfaces/external/IERC20Minimal.sol"; - -/// @title TransferHelper -/// @notice Contains helper methods for interacting with ERC20 tokens that do not consistently return true/false -/// @dev implementation from https://github.com/Rari-Capital/solmate/blob/main/src/utils/SafeTransferLib.sol#L63 -library TransferHelper { - /// @notice Transfers tokens from msg.sender to a recipient - /// @dev Calls transfer on token contract, errors with TF if transfer fails - /// @param token The contract address of the token which will be transferred - /// @param to The recipient of the transfer - /// @param value The value of the transfer - function safeTransfer(IERC20Minimal token, address to, uint256 value) internal { - bool success; - - assembly { - // Get a pointer to some free memory. - let freeMemoryPointer := mload(0x40) - - // Write the abi-encoded calldata into memory, beginning with the function selector. - mstore(freeMemoryPointer, 0xa9059cbb00000000000000000000000000000000000000000000000000000000) - mstore(add(freeMemoryPointer, 4), to) // Append the "to" argument. - mstore(add(freeMemoryPointer, 36), value) // Append the "value" argument. - - success := - and( - // Set success to whether the call reverted, if not we check it either - // returned exactly 1 (can't just be non-zero data), or had no return data. - or(and(eq(mload(0), 1), gt(returndatasize(), 31)), iszero(returndatasize())), - // We use 68 because the length of our calldata totals up like so: 4 + 32 * 2. - // We use 0 and 32 to copy up to 32 bytes of return data into the scratch space. - // Counterintuitively, this call must be positioned second to the or() call in the - // surrounding and() call or else returndatasize() will be zero during the computation. - call(gas(), token, 0, freeMemoryPointer, 68, 0, 32) - ) - } - - require(success, "TRANSFER_FAILED"); - } - - /// @notice Transfers tokens from from to a recipient - /// @dev Calls transferFrom on token contract, errors with TF if transfer fails - /// @param token The contract address of the token which will be transferred - /// @param from The origin of the transfer - /// @param to The recipient of the transfer - /// @param value The value of the transfer - function safeTransferFrom(IERC20Minimal token, address from, address to, uint256 value) internal { - (bool success, bytes memory data) = - address(token).call(abi.encodeWithSelector(IERC20Minimal.transferFrom.selector, from, to, value)); - require(success && (data.length == 0 || abi.decode(data, (bool))), "STF"); - } -} diff --git a/contracts/libraries/UniswapV4ERC20.sol b/contracts/libraries/UniswapV4ERC20.sol deleted file mode 100644 index fdd93ba4..00000000 --- a/contracts/libraries/UniswapV4ERC20.sol +++ /dev/null @@ -1,16 +0,0 @@ -pragma solidity ^0.8.19; - -import {ERC20} from "solmate/tokens/ERC20.sol"; -import {Owned} from "solmate/auth/Owned.sol"; - -contract UniswapV4ERC20 is ERC20, Owned { - constructor(string memory name, string memory symbol) ERC20(name, symbol, 18) Owned(msg.sender) {} - - function mint(address account, uint256 amount) external onlyOwner { - _mint(account, amount); - } - - function burn(address account, uint256 amount) external onlyOwner { - _burn(account, amount); - } -} diff --git a/contracts/middleware/BaseMiddlewareFactory.sol b/contracts/middleware/BaseMiddlewareFactory.sol deleted file mode 100644 index d4c5a297..00000000 --- a/contracts/middleware/BaseMiddlewareFactory.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.24; - -import {IMiddlewareFactory} from "../interfaces/IMiddlewareFactory.sol"; -import {BaseMiddleware} from "./BaseMiddleware.sol"; -import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; - -abstract contract BaseMiddlewareFactory is IMiddlewareFactory { - mapping(address => address) private _implementations; - - IPoolManager public immutable manager; - - constructor(IPoolManager _manager) { - manager = _manager; - } - - function getImplementation(address middleware) external view override returns (address implementation) { - return _implementations[middleware]; - } - - function createMiddleware(address implementation, bytes32 salt) external override returns (address middleware) { - middleware = _deployMiddleware(implementation, salt); - _implementations[middleware] = implementation; - emit MiddlewareCreated(implementation, middleware); - } - - function _deployMiddleware(address implementation, bytes32 salt) internal virtual returns (address middleware) {} -} diff --git a/foundry.toml b/foundry.toml index b7851680..e7432aec 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,14 +1,14 @@ [profile.default] out = 'foundry-out' solc_version = '0.8.26' -optimizer_runs = 1000000 +optimizer_runs = 1_000_000 ffi = true fs_permissions = [{ access = "read-write", path = ".forge-snapshots/"}] evm_version = "cancun" gas_limit = "3000000000" -fuzz_runs = 10000 +fuzz_runs = 10_000 [profile.ci] -fuzz_runs = 100000 +fuzz_runs = 100_000 # See more config options https://github.com/foundry-rs/foundry/tree/master/config \ No newline at end of file diff --git a/lib/forge-gas-snapshot b/lib/forge-gas-snapshot deleted file mode 160000 index 2f884282..00000000 --- a/lib/forge-gas-snapshot +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 2f884282b4cd067298e798974f5b534288b13bc2 diff --git a/lib/forge-std b/lib/forge-std deleted file mode 160000 index 2b58ecbc..00000000 --- a/lib/forge-std +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 2b58ecbcf3dfde7a75959dc7b4eb3d0670278de6 diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts deleted file mode 160000 index 5ae63068..00000000 --- a/lib/openzeppelin-contracts +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 5ae630684a0f57de400ef69499addab4c32ac8fb diff --git a/lib/permit2 b/lib/permit2 new file mode 160000 index 00000000..cc56ad0f --- /dev/null +++ b/lib/permit2 @@ -0,0 +1 @@ +Subproject commit cc56ad0f3439c502c246fc5cfcc3db92bb8b7219 diff --git a/lib/solmate b/lib/solmate deleted file mode 160000 index bfc9c258..00000000 --- a/lib/solmate +++ /dev/null @@ -1 +0,0 @@ -Subproject commit bfc9c25865a274a7827fea5abf6e4fb64fc64e6c diff --git a/lib/v4-core b/lib/v4-core index 6e6ce35b..799dd2cb 160000 --- a/lib/v4-core +++ b/lib/v4-core @@ -1 +1 @@ -Subproject commit 6e6ce35b69b15cb61bd8cb8488c7d064fab52886 +Subproject commit 799dd2cb980319a8d3b827b6a7aa59a606634553 diff --git a/remappings.txt b/remappings.txt index e05c5bd6..c7951f83 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,4 +1,7 @@ @uniswap/v4-core/=lib/v4-core/ -solmate/=lib/solmate/src/ -forge-std/=lib/forge-std/src/ -@openzeppelin/=lib/openzeppelin-contracts/ +@openzeppelin/=lib/v4-core/lib/openzeppelin-contracts/ +forge-gas-snapshot/=lib/v4-core/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 diff --git a/script/DeployStateView.s.sol b/script/DeployStateView.s.sol new file mode 100644 index 00000000..b48526bc --- /dev/null +++ b/script/DeployStateView.s.sol @@ -0,0 +1,23 @@ +// 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 {StateView} from "../src/lens/StateView.sol"; + +contract DeployStateView is Script { + function setUp() public {} + + function run(address poolManager) public returns (StateView state) { + vm.startBroadcast(); + + // forge script --broadcast --sig 'run(address)' --rpc-url --private-key --verify script/DeployStateView.s.sol:DeployStateView + state = new StateView(IPoolManager(poolManager)); + console2.log("StateView", address(state)); + console2.log("PoolManager", address(state.poolManager())); + + vm.stopBroadcast(); + } +} diff --git a/src/PositionManager.sol b/src/PositionManager.sol new file mode 100644 index 00000000..43cafe54 --- /dev/null +++ b/src/PositionManager.sol @@ -0,0 +1,291 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +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, 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 {Position} from "@uniswap/v4-core/src/libraries/Position.sol"; +import {SafeTransferLib} from "solmate/src/utils/SafeTransferLib.sol"; +import {ERC20} from "solmate/src/tokens/ERC20.sol"; +import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol"; + +import {ERC721Permit} from "./base/ERC721Permit.sol"; +import {ReentrancyLock} from "./base/ReentrancyLock.sol"; +import {IPositionManager} from "./interfaces/IPositionManager.sol"; +import {Multicall} from "./base/Multicall.sol"; +import {PoolInitializer} from "./base/PoolInitializer.sol"; +import {DeltaResolver} from "./base/DeltaResolver.sol"; +import {PositionConfig, PositionConfigLibrary} from "./libraries/PositionConfig.sol"; +import {BaseActionsRouter} from "./base/BaseActionsRouter.sol"; +import {Actions} from "./libraries/Actions.sol"; +import {CalldataDecoder} from "./libraries/CalldataDecoder.sol"; +import {SlippageCheckLibrary} from "./libraries/SlippageCheck.sol"; + +contract PositionManager is + IPositionManager, + ERC721Permit, + PoolInitializer, + Multicall, + DeltaResolver, + ReentrancyLock, + BaseActionsRouter +{ + using SafeTransferLib for *; + using CurrencyLibrary for Currency; + using PoolIdLibrary for PoolKey; + using PositionConfigLibrary for PositionConfig; + using StateLibrary for IPoolManager; + using TransientStateLibrary for IPoolManager; + using SafeCast for uint256; + using CalldataDecoder for bytes; + using SlippageCheckLibrary for BalanceDelta; + + /// @dev The ID of the next token that will be minted. Skips 0 + uint256 public nextTokenId = 1; + + /// @inheritdoc IPositionManager + mapping(uint256 tokenId => bytes32 configId) public positionConfigs; + + IAllowanceTransfer public immutable permit2; + + constructor(IPoolManager _poolManager, IAllowanceTransfer _permit2) + BaseActionsRouter(_poolManager) + ERC721Permit("Uniswap V4 Positions NFT", "UNI-V4-POSM", "1") + { + permit2 = _permit2; + } + + modifier checkDeadline(uint256 deadline) { + if (block.timestamp > deadline) revert DeadlinePassed(); + _; + } + + /// @param unlockData is an encoding of actions, params, and currencies + /// @param deadline is the timestamp at which the unlockData will no longer be valid + function modifyLiquidities(bytes calldata unlockData, uint256 deadline) + external + payable + isNotLocked + checkDeadline(deadline) + { + _executeActions(unlockData); + } + + function _handleAction(uint256 action, bytes calldata params) internal virtual override { + if (action == Actions.INCREASE_LIQUIDITY) { + ( + uint256 tokenId, + PositionConfig calldata config, + uint256 liquidity, + uint128 amount0Max, + uint128 amount1Max, + bytes calldata hookData + ) = params.decodeModifyLiquidityParams(); + _increase(tokenId, config, liquidity, amount0Max, amount1Max, hookData); + } else if (action == Actions.DECREASE_LIQUIDITY) { + ( + uint256 tokenId, + PositionConfig calldata config, + uint256 liquidity, + uint128 amount0Min, + uint128 amount1Min, + bytes calldata hookData + ) = params.decodeModifyLiquidityParams(); + _decrease(tokenId, config, liquidity, amount0Min, amount1Min, hookData); + } else if (action == Actions.MINT_POSITION) { + ( + PositionConfig calldata config, + uint256 liquidity, + uint128 amount0Max, + uint128 amount1Max, + address owner, + bytes calldata hookData + ) = params.decodeMintParams(); + _mint(config, liquidity, amount0Max, amount1Max, owner, hookData); + } else if (action == Actions.CLOSE_CURRENCY) { + Currency currency = params.decodeCurrency(); + _close(currency); + } else if (action == Actions.CLEAR) { + (Currency currency, uint256 amountMax) = params.decodeCurrencyAndUint256(); + _clear(currency, amountMax); + } else if (action == Actions.BURN_POSITION) { + // Will automatically decrease liquidity to 0 if the position is not already empty. + ( + uint256 tokenId, + PositionConfig calldata config, + uint128 amount0Min, + uint128 amount1Min, + bytes calldata hookData + ) = params.decodeBurnParams(); + _burn(tokenId, config, amount0Min, amount1Min, hookData); + } else if (action == Actions.SETTLE_WITH_BALANCE) { + Currency currency = params.decodeCurrency(); + _settleWithBalance(currency); + } else if (action == Actions.SWEEP) { + (Currency currency, address to) = params.decodeCurrencyAndAddress(); + _sweep(currency, to); + } else { + revert UnsupportedAction(action); + } + } + + function _msgSender() internal view override returns (address) { + return _getLocker(); + } + + /// @dev Calling increase with 0 liquidity will credit the caller with any underlying fees of the position + function _increase( + uint256 tokenId, + PositionConfig calldata config, + uint256 liquidity, + uint128 amount0Max, + uint128 amount1Max, + bytes calldata hookData + ) internal { + if (positionConfigs[tokenId] != config.toId()) revert IncorrectPositionConfigForTokenId(tokenId); + // Note: The tokenId is used as the salt for this position, so every minted position has unique storage in the pool manager. + BalanceDelta liquidityDelta = _modifyLiquidity(config, liquidity.toInt256(), bytes32(tokenId), hookData); + liquidityDelta.validateMaxInNegative(amount0Max, amount1Max); + } + + /// @dev Calling decrease with 0 liquidity will credit the caller with any underlying fees of the position + function _decrease( + uint256 tokenId, + PositionConfig calldata config, + uint256 liquidity, + uint128 amount0Min, + uint128 amount1Min, + bytes calldata hookData + ) internal { + if (!_isApprovedOrOwner(_msgSender(), tokenId)) revert NotApproved(_msgSender()); + if (positionConfigs[tokenId] != config.toId()) revert IncorrectPositionConfigForTokenId(tokenId); + + // Note: the tokenId is used as the salt. + BalanceDelta liquidityDelta = _modifyLiquidity(config, -(liquidity.toInt256()), bytes32(tokenId), hookData); + liquidityDelta.validateMinOut(amount0Min, amount1Min); + } + + function _mint( + PositionConfig calldata config, + uint256 liquidity, + uint128 amount0Max, + uint128 amount1Max, + address owner, + bytes calldata hookData + ) internal { + // mint receipt token + uint256 tokenId; + // tokenId is assigned to current nextTokenId before incrementing it + unchecked { + tokenId = nextTokenId++; + } + _mint(owner, tokenId); + + // _beforeModify is not called here because the tokenId is newly minted + BalanceDelta liquidityDelta = _modifyLiquidity(config, liquidity.toInt256(), bytes32(tokenId), hookData); + liquidityDelta.validateMaxIn(amount0Max, amount1Max); + positionConfigs[tokenId] = config.toId(); + } + + function _close(Currency currency) internal { + // this address has applied all deltas on behalf of the user/owner + // it is safe to close this entire delta because of slippage checks throughout the batched calls. + int256 currencyDelta = poolManager.currencyDelta(address(this), currency); + + // the locker is the payer or receiver + address caller = _msgSender(); + if (currencyDelta < 0) { + _settle(currency, caller, uint256(-currencyDelta)); + } else if (currencyDelta > 0) { + _take(currency, caller, uint256(currencyDelta)); + } + } + + /// @dev integrators may elect to forfeit positive deltas with clear + /// provides a safety check that amount-to-clear is less than a user-provided maximum + function _clear(Currency currency, uint256 amountMax) internal { + int256 currencyDelta = poolManager.currencyDelta(address(this), currency); + if (uint256(currencyDelta) > amountMax) revert ClearExceedsMaxAmount(currency, currencyDelta, amountMax); + poolManager.clear(currency, uint256(currencyDelta)); + } + + /// @dev uses this addresses balance to settle a negative delta + function _settleWithBalance(Currency currency) internal { + // set the payer to this address, performs a transfer. + _settle(currency, address(this), _getFullSettleAmount(currency)); + } + + /// @dev this is overloaded with ERC721Permit._burn + function _burn( + uint256 tokenId, + PositionConfig calldata config, + uint128 amount0Min, + uint128 amount1Min, + bytes calldata hookData + ) internal { + if (!_isApprovedOrOwner(_msgSender(), tokenId)) revert NotApproved(_msgSender()); + if (positionConfigs[tokenId] != config.toId()) revert IncorrectPositionConfigForTokenId(tokenId); + uint256 liquidity = uint256(_getPositionLiquidity(config, tokenId)); + + BalanceDelta liquidityDelta; + // Can only call modify if there is non zero liquidity. + if (liquidity > 0) { + liquidityDelta = _modifyLiquidity(config, -(liquidity.toInt256()), bytes32(tokenId), hookData); + liquidityDelta.validateMinOut(amount0Min, amount1Min); + } + + delete positionConfigs[tokenId]; + // Burn the token. + _burn(tokenId); + } + + function _modifyLiquidity( + PositionConfig calldata config, + int256 liquidityChange, + bytes32 salt, + bytes calldata hookData + ) internal returns (BalanceDelta liquidityDelta) { + (liquidityDelta,) = poolManager.modifyLiquidity( + config.poolKey, + IPoolManager.ModifyLiquidityParams({ + tickLower: config.tickLower, + tickUpper: config.tickUpper, + liquidityDelta: liquidityChange, + salt: salt + }), + hookData + ); + } + + function _getPositionLiquidity(PositionConfig calldata config, uint256 tokenId) + internal + view + returns (uint128 liquidity) + { + bytes32 positionId = + Position.calculatePositionKey(address(this), config.tickLower, config.tickUpper, bytes32(tokenId)); + liquidity = poolManager.getPositionLiquidity(config.poolKey.toId(), positionId); + } + + /// @notice Sweeps the entire contract balance of specified currency to the recipient + function _sweep(Currency currency, address to) internal { + uint256 balance = currency.balanceOfSelf(); + if (balance > 0) currency.transfer(to, balance); + } + + // implementation of abstract function DeltaResolver._pay + function _pay(Currency currency, address payer, uint256 amount) internal override { + if (payer == address(this)) { + // TODO: currency is guaranteed to not be eth so the native check in transfer is not optimal. + currency.transfer(address(poolManager), amount); + } else { + permit2.transferFrom(payer, address(poolManager), uint160(amount), Currency.unwrap(currency)); + } + } +} diff --git a/src/V4Router.sol b/src/V4Router.sol new file mode 100644 index 00000000..cc31c69c --- /dev/null +++ b/src/V4Router.sol @@ -0,0 +1,161 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; + +import {PathKey, PathKeyLib} from "./libraries/PathKey.sol"; +import {CalldataDecoder} from "./libraries/CalldataDecoder.sol"; +import {IV4Router} from "./interfaces/IV4Router.sol"; +import {BaseActionsRouter} from "./base/BaseActionsRouter.sol"; +import {DeltaResolver} from "./base/DeltaResolver.sol"; +import {Actions} from "./libraries/Actions.sol"; +import {SafeCastTemp} from "./libraries/SafeCast.sol"; + +/// @title UniswapV4Router +/// @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 { + using SafeCastTemp for *; + using PathKeyLib for PathKey; + using CalldataDecoder for bytes; + + constructor(IPoolManager _poolManager) BaseActionsRouter(_poolManager) {} + + // TODO native support !! + function _handleAction(uint256 action, bytes calldata params) internal override { + // swap actions and payment actions in different blocks for gas efficiency + if (action < Actions.SETTLE) { + if (action == Actions.SWAP_EXACT_IN) { + IV4Router.ExactInputParams calldata swapParams = params.decodeSwapExactInParams(); + _swapExactInput(swapParams); + } else if (action == Actions.SWAP_EXACT_IN_SINGLE) { + IV4Router.ExactInputSingleParams calldata swapParams = params.decodeSwapExactInSingleParams(); + _swapExactInputSingle(swapParams); + } else if (action == Actions.SWAP_EXACT_OUT) { + IV4Router.ExactOutputParams calldata swapParams = params.decodeSwapExactOutParams(); + _swapExactOutput(swapParams); + } else if (action == Actions.SWAP_EXACT_OUT_SINGLE) { + IV4Router.ExactOutputSingleParams calldata swapParams = params.decodeSwapExactOutSingleParams(); + _swapExactOutputSingle(swapParams); + } else { + revert UnsupportedAction(action); + } + } else { + if (action == Actions.SETTLE_ALL) { + Currency currency = params.decodeCurrency(); + // TODO should it have a maxAmountOut added slippage protection? + _settle(currency, _msgSender(), _getFullSettleAmount(currency)); + } else if (action == Actions.SETTLE_WITH_BALANCE) { + Currency currency = params.decodeCurrency(); + _settle(currency, address(this), _getFullSettleAmount(currency)); + } else if (action == Actions.TAKE_ALL) { + (Currency currency, address recipient) = params.decodeCurrencyAndAddress(); + uint256 amount = _getFullTakeAmount(currency); + + // TODO should _take have a minAmountOut added slippage check? + // TODO recipient mapping + _take(currency, recipient, amount); + } else { + revert UnsupportedAction(action); + } + } + } + + function _swapExactInputSingle(IV4Router.ExactInputSingleParams calldata params) private { + uint128 amountOut = _swap( + params.poolKey, + params.zeroForOne, + int256(-int128(params.amountIn)), + params.sqrtPriceLimitX96, + params.hookData + ).toUint128(); + if (amountOut < params.amountOutMinimum) revert TooLittleReceived(); + } + + function _swapExactInput(IV4Router.ExactInputParams calldata params) private { + unchecked { + // Caching for gas savings + uint256 pathLength = params.path.length; + uint128 amountOut; + uint128 amountIn = params.amountIn; + Currency currencyIn = params.currencyIn; + PathKey calldata pathKey; + + for (uint256 i = 0; i < pathLength; i++) { + 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(); + + amountIn = amountOut; + currencyIn = pathKey.intermediateCurrency; + } + + if (amountOut < params.amountOutMinimum) revert TooLittleReceived(); + } + } + + function _swapExactOutputSingle(IV4Router.ExactOutputSingleParams calldata params) private { + uint128 amountIn = ( + -_swap( + params.poolKey, + params.zeroForOne, + int256(int128(params.amountOut)), + params.sqrtPriceLimitX96, + params.hookData + ) + ).toUint128(); + if (amountIn > params.amountInMaximum) revert TooMuchRequested(); + } + + function _swapExactOutput(IV4Router.ExactOutputParams calldata params) private { + unchecked { + // Caching for gas savings + uint256 pathLength = params.path.length; + uint128 amountIn; + uint128 amountOut = params.amountOut; + Currency currencyOut = params.currencyOut; + PathKey calldata pathKey; + + for (uint256 i = pathLength; i > 0; i--) { + 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 = (-_swap(poolKey, !oneForZero, int256(uint256(amountOut)), 0, pathKey.hookData)).toUint128(); + + amountOut = amountIn; + currencyOut = pathKey.intermediateCurrency; + } + if (amountIn > params.amountInMaximum) revert TooMuchRequested(); + } + } + + function _swap( + PoolKey memory poolKey, + bool zeroForOne, + int256 amountSpecified, + uint160 sqrtPriceLimitX96, + bytes calldata hookData + ) private returns (int128 reciprocalAmount) { + unchecked { + BalanceDelta delta = poolManager.swap( + poolKey, + IPoolManager.SwapParams( + zeroForOne, + amountSpecified, + sqrtPriceLimitX96 == 0 + ? (zeroForOne ? TickMath.MIN_SQRT_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1) + : sqrtPriceLimitX96 + ), + hookData + ); + + reciprocalAmount = (zeroForOne == amountSpecified < 0) ? delta.amount1() : delta.amount0(); + } + } +} diff --git a/src/base/BaseActionsRouter.sol b/src/base/BaseActionsRouter.sol new file mode 100644 index 00000000..bdc58083 --- /dev/null +++ b/src/base/BaseActionsRouter.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.24; + +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {SafeCallback} from "./SafeCallback.sol"; +import {CalldataDecoder} from "../libraries/CalldataDecoder.sol"; + +/// @notice Abstract contract for performing a combination of actions on Uniswap v4. +/// @dev Suggested uint256 action values are defined in Actions.sol, however any definition can be used +abstract contract BaseActionsRouter is SafeCallback { + using CalldataDecoder for bytes; + + /// @notice emitted when different numbers of parameters and actions are provided + error InputLengthMismatch(); + + /// @notice emitted when an inheriting contract does not support an action + error UnsupportedAction(uint256 action); + + constructor(IPoolManager _poolManager) SafeCallback(_poolManager) {} + + /// @notice internal function that triggers the execution of a set of actions on v4 + /// @dev inheriting contracts should call this function to trigger execution + function _executeActions(bytes calldata unlockData) internal { + poolManager.unlock(unlockData); + } + + /// @notice function that is called by the PoolManager through the SafeCallback.unlockCallback + /// @param data Abi encoding of (bytes actions, bytes[] params) + /// where params[i] is the encoded parameters for actions[i] + function _unlockCallback(bytes calldata data) internal override returns (bytes memory) { + // abi.decode(data, (bytes, bytes[])); + (bytes calldata actions, bytes[] calldata params) = data.decodeActionsRouterParams(); + + uint256 numActions = actions.length; + if (numActions != params.length) revert InputLengthMismatch(); + + for (uint256 actionIndex = 0; actionIndex < numActions; actionIndex++) { + uint256 action = uint256(uint8(actions[actionIndex])); + + _handleAction(action, params[actionIndex]); + } + + return ""; + } + + /// @notice function to handle the parsing and execution of an action and its parameters + function _handleAction(uint256 action, bytes calldata params) internal virtual; + + /// @notice function that returns address considered executer of the actions + /// @dev The other context functions, _msgData and _msgValue, are not supported by this contract + /// In many contracts this will be the address that calls the initial entry point that calls `_executeActions` + /// `msg.sender` shouldnt be used, as this will be the v4 pool manager contract that calls `unlockCallback` + /// If using ReentrancyLock.sol, this function can return _getLocker() + function _msgSender() internal view virtual returns (address); +} diff --git a/src/base/DeltaResolver.sol b/src/base/DeltaResolver.sol new file mode 100644 index 00000000..9ddd81db --- /dev/null +++ b/src/base/DeltaResolver.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.24; + +import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; +import {TransientStateLibrary} from "@uniswap/v4-core/src/libraries/TransientStateLibrary.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {ImmutableState} from "./ImmutableState.sol"; + +/// @notice Abstract contract used to sync, send, and settle funds to the pool manager +/// @dev Note that sync() is called before any erc-20 transfer in `settle`. +abstract contract DeltaResolver is ImmutableState { + using TransientStateLibrary for IPoolManager; + + /// @notice Emitted trying to settle a positive delta. + error IncorrectUseOfSettle(); + /// @notice Emitted trying to take a negative delta. + error IncorrectUseOfTake(); + + /// @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 + function _take(Currency currency, address recipient, uint256 amount) internal { + poolManager.take(currency, recipient, amount); + } + + /// @notice Pay and settle a currency to the PoolManager + /// @dev The implementing contract must ensure that the `payer` is a secure address + /// @param currency Currency to settle + /// @param payer Address of the payer + /// @param amount Amount to send + function _settle(Currency currency, address payer, uint256 amount) internal { + if (currency.isNative()) { + poolManager.settle{value: amount}(); + } else { + poolManager.sync(currency); + _pay(currency, payer, amount); + poolManager.settle(); + } + } + + /// @notice Abstract function for contracts to implement paying tokens to the poolManager + /// @dev The recipient of the payment should be the poolManager + /// @param token The token to settle. This is known not to be the native currency + /// @param payer The address who should pay tokens + /// @param amount The number of tokens to send + function _pay(Currency token, address payer, uint256 amount) internal virtual; + + function _getFullSettleAmount(Currency currency) internal view returns (uint256 amount) { + int256 _amount = poolManager.currencyDelta(address(this), currency); + // If the amount is positive, it should be taken not settled for. + if (_amount > 0) revert IncorrectUseOfSettle(); + amount = uint256(-_amount); + } + + function _getFullTakeAmount(Currency currency) internal view returns (uint256 amount) { + int256 _amount = poolManager.currencyDelta(address(this), currency); + // If the amount is negative, it should be settled not taken. + if (_amount < 0) revert IncorrectUseOfTake(); + amount = uint256(_amount); + } +} diff --git a/src/base/ERC721Permit.sol b/src/base/ERC721Permit.sol new file mode 100644 index 00000000..34597ee6 --- /dev/null +++ b/src/base/ERC721Permit.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.24; + +import {ERC721} from "solmate/src/tokens/ERC721.sol"; + +/// @notice An ERC721 contract that supports permit. +/// TODO: Support permit. +contract ERC721Permit is ERC721 { + constructor(string memory name_, string memory symbol_, string memory version_) ERC721(name_, symbol_) {} + + function _isApprovedOrOwner(address spender, uint256 tokenId) internal view returns (bool) { + return spender == ownerOf(tokenId) || getApproved[tokenId] == spender + || isApprovedForAll[ownerOf(tokenId)][spender]; + } + + // TODO: Use PositionDescriptor. + function tokenURI(uint256 id) public pure override returns (string memory) { + return string(abi.encode(id)); + } +} diff --git a/contracts/base/ImmutableState.sol b/src/base/ImmutableState.sol similarity index 59% rename from contracts/base/ImmutableState.sol rename to src/base/ImmutableState.sol index cce37514..dab1563c 100644 --- a/contracts/base/ImmutableState.sol +++ b/src/base/ImmutableState.sol @@ -4,9 +4,9 @@ pragma solidity ^0.8.19; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; contract ImmutableState { - IPoolManager public immutable manager; + IPoolManager public immutable poolManager; - constructor(IPoolManager _manager) { - manager = _manager; + constructor(IPoolManager _poolManager) { + poolManager = _poolManager; } } diff --git a/contracts/base/Multicall.sol b/src/base/Multicall.sol similarity index 100% rename from contracts/base/Multicall.sol rename to src/base/Multicall.sol diff --git a/src/base/PoolInitializer.sol b/src/base/PoolInitializer.sol new file mode 100644 index 00000000..bb75a3d9 --- /dev/null +++ b/src/base/PoolInitializer.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.24; + +import {ImmutableState} from "./ImmutableState.sol"; + +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; + +abstract contract PoolInitializer is ImmutableState { + function initializePool(PoolKey calldata key, uint160 sqrtPriceX96, bytes calldata hookData) + external + returns (int24) + { + return poolManager.initialize(key, sqrtPriceX96, hookData); + } +} diff --git a/src/base/ReentrancyLock.sol b/src/base/ReentrancyLock.sol new file mode 100644 index 00000000..29fa3d34 --- /dev/null +++ b/src/base/ReentrancyLock.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.24; + +import {Locker} from "../libraries/Locker.sol"; + +/// @notice A transient reentrancy lock, that stores the caller's address as the lock +contract ReentrancyLock { + error ContractLocked(); + + modifier isNotLocked() { + if (Locker.get() != address(0)) revert ContractLocked(); + Locker.set(msg.sender); + _; + Locker.set(address(0)); + } + + function _getLocker() internal view returns (address) { + return Locker.get(); + } +} diff --git a/contracts/base/SafeCallback.sol b/src/base/SafeCallback.sol similarity index 54% rename from contracts/base/SafeCallback.sol rename to src/base/SafeCallback.sol index f985e67c..99942a9e 100644 --- a/contracts/base/SafeCallback.sol +++ b/src/base/SafeCallback.sol @@ -1,20 +1,22 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.24; import {IUnlockCallback} from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {ImmutableState} from "./ImmutableState.sol"; abstract contract SafeCallback is ImmutableState, IUnlockCallback { - error NotManager(); + error NotPoolManager(); - modifier onlyByManager() { - if (msg.sender != address(manager)) revert NotManager(); + constructor(IPoolManager _poolManager) ImmutableState(_poolManager) {} + + modifier onlyByPoolManager() { + if (msg.sender != address(poolManager)) revert NotPoolManager(); _; } - /// @dev We force the onlyByManager modifier by exposing a virtual function after the onlyByManager check. - function unlockCallback(bytes calldata data) external onlyByManager returns (bytes memory) { + /// @dev We force the onlyByPoolManager modifier by exposing a virtual function after the onlyByPoolManager check. + function unlockCallback(bytes calldata data) external onlyByPoolManager returns (bytes memory) { return _unlockCallback(data); } diff --git a/contracts/BaseHook.sol b/src/base/hooks/BaseHook.sol similarity index 95% rename from contracts/BaseHook.sol rename to src/base/hooks/BaseHook.sol index 01fc4954..0c983cf6 100644 --- a/contracts/BaseHook.sol +++ b/src/base/hooks/BaseHook.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.24; import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; @@ -7,8 +7,7 @@ import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {BeforeSwapDelta} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol"; -import {SafeCallback} from "./base/SafeCallback.sol"; -import {ImmutableState} from "./base/ImmutableState.sol"; +import {SafeCallback} from "../SafeCallback.sol"; abstract contract BaseHook is IHooks, SafeCallback { error NotSelf(); @@ -16,7 +15,7 @@ abstract contract BaseHook is IHooks, SafeCallback { error LockFailure(); error HookNotImplemented(); - constructor(IPoolManager _manager) ImmutableState(_manager) { + constructor(IPoolManager _manager) SafeCallback(_manager) { validateHookAddress(this); } diff --git a/src/interfaces/IERC721Permit.sol b/src/interfaces/IERC721Permit.sol new file mode 100644 index 00000000..213bca2a --- /dev/null +++ b/src/interfaces/IERC721Permit.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity >=0.7.5; + +/// @title ERC721 with permit +/// @notice Extension to ERC721 that includes a permit function for signature based approvals +interface IERC721Permit { + error NonceAlreadyUsed(); + + /// @notice The permit typehash used in the permit signature + /// @return The typehash for the permit + function PERMIT_TYPEHASH() external pure returns (bytes32); + + /// @notice The domain separator used in the permit signature + /// @return The domain seperator used in encoding of permit signature + function DOMAIN_SEPARATOR() external view returns (bytes32); + + /// @notice Approve of a specific token ID for spending by spender via signature + /// @param spender The account that is being approved + /// @param tokenId The ID of the token that is being approved for spending + /// @param deadline The deadline timestamp by which the call must be mined for the approve to work + /// @param v Must produce valid secp256k1 signature from the holder along with `r` and `s` + /// @param r Must produce valid secp256k1 signature from the holder along with `v` and `s` + /// @param s Must produce valid secp256k1 signature from the holder along with `r` and `v` + function permit(address spender, uint256 tokenId, uint256 deadline, uint256 nonce, uint8 v, bytes32 r, bytes32 s) + external + payable; +} diff --git a/contracts/interfaces/IMiddlewareFactory.sol b/src/interfaces/IMiddlewareFactory.sol similarity index 100% rename from contracts/interfaces/IMiddlewareFactory.sol rename to src/interfaces/IMiddlewareFactory.sol diff --git a/contracts/interfaces/IMulticall.sol b/src/interfaces/IMulticall.sol similarity index 100% rename from contracts/interfaces/IMulticall.sol rename to src/interfaces/IMulticall.sol diff --git a/src/interfaces/IPositionManager.sol b/src/interfaces/IPositionManager.sol new file mode 100644 index 00000000..371aed93 --- /dev/null +++ b/src/interfaces/IPositionManager.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; + +interface IPositionManager { + error NotApproved(address caller); + error DeadlinePassed(); + error IncorrectPositionConfigForTokenId(uint256 tokenId); + error ClearExceedsMaxAmount(Currency currency, int256 amount, uint256 maxAmount); + + /// @notice Maps the ERC721 tokenId to a configId, which is a keccak256 hash of the position's pool key, and range (tickLower, tickUpper) + /// Enforces that a minted ERC721 token is tied to one range on one pool. + /// @param tokenId the ERC721 tokenId, assigned at mint + /// @return configId the hash of the position's poolkey, tickLower, and tickUpper + function positionConfigs(uint256 tokenId) external view returns (bytes32 configId); + + /// @notice Batches many liquidity modification calls to pool manager + /// @param payload is an encoding of actions, and parameters for those actions + /// @param deadline is the deadline for the batched actions to be executed + function modifyLiquidities(bytes calldata payload, uint256 deadline) external payable; + + function nextTokenId() external view returns (uint256); +} diff --git a/contracts/interfaces/IQuoter.sol b/src/interfaces/IQuoter.sol similarity index 93% rename from contracts/interfaces/IQuoter.sol rename to src/interfaces/IQuoter.sol index 8774e548..3b96bca2 100644 --- a/contracts/interfaces/IQuoter.sol +++ b/src/interfaces/IQuoter.sol @@ -11,7 +11,6 @@ import {PathKey} from "../libraries/PathKey.sol"; /// @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 { - error InvalidUnlockCallbackSender(); error InvalidLockCaller(); error InvalidQuoteBatchParams(); error InsufficientAmountOut(); @@ -27,7 +26,6 @@ interface IQuoter { struct QuoteExactSingleParams { PoolKey poolKey; bool zeroForOne; - address recipient; uint128 exactAmount; uint160 sqrtPriceLimitX96; bytes hookData; @@ -36,7 +34,6 @@ interface IQuoter { struct QuoteExactParams { Currency exactCurrency; PathKey[] path; - address recipient; uint128 exactAmount; } @@ -44,7 +41,6 @@ interface IQuoter { /// @param params The params for the quote, encoded as `QuoteExactInputSingleParams` /// poolKey The key for identifying a V4 pool /// zeroForOne If the swap is from currency0 to currency1 - /// recipient The intended recipient of the output tokens /// exactAmount The desired input amount /// sqrtPriceLimitX96 The price limit of the pool that cannot be exceeded by the swap /// hookData arbitrary hookData to pass into the associated hooks @@ -59,7 +55,6 @@ interface IQuoter { /// @param params the params for the quote, encoded as 'QuoteExactInputParams' /// currencyIn The input currency of the swap /// path The path of the swap encoded as PathKeys that contains currency, fee, tickSpacing, and hook info - /// recipient The intended recipient of the output tokens /// exactAmount The desired input amount /// @return deltaAmounts Delta amounts along the path resulted from the swap /// @return sqrtPriceX96AfterList List of the sqrt price after the swap for each pool in the path @@ -76,7 +71,6 @@ interface IQuoter { /// @param params The params for the quote, encoded as `QuoteExactOutputSingleParams` /// poolKey The key for identifying a V4 pool /// zeroForOne If the swap is from currency0 to currency1 - /// recipient The intended recipient of the output tokens /// exactAmount The desired input amount /// sqrtPriceLimitX96 The price limit of the pool that cannot be exceeded by the swap /// hookData arbitrary hookData to pass into the associated hooks @@ -91,7 +85,6 @@ interface IQuoter { /// @param params the params for the quote, encoded as 'QuoteExactOutputParams' /// currencyOut The output currency of the swap /// path The path of the swap encoded as PathKeys that contains currency, fee, tickSpacing, and hook info - /// recipient The intended recipient of the output tokens /// exactAmount The desired output amount /// @return deltaAmounts Delta amounts along the path resulted from the swap /// @return sqrtPriceX96AfterList List of the sqrt price after the swap for each pool in the path diff --git a/src/interfaces/IV4Router.sol b/src/interfaces/IV4Router.sol new file mode 100644 index 00000000..3dfc4d88 --- /dev/null +++ b/src/interfaces/IV4Router.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import {PathKey} from "../libraries/PathKey.sol"; + +/// @title IV4Router +/// @notice Interface containing all the structs and errors for different v4 swap types +interface IV4Router { + /// @notice Emitted when an exactInput swap does not receive its minAmountOut + error TooLittleReceived(); + /// @notice Emitted when an exactOutput is asked for more than its maxAmountIn + error TooMuchRequested(); + + /// @notice Parameters for a single-hop exact-input swap + struct ExactInputSingleParams { + PoolKey poolKey; + bool zeroForOne; + uint128 amountIn; + uint128 amountOutMinimum; + uint160 sqrtPriceLimitX96; + bytes hookData; + } + + /// @notice Parameters for a multi-hop exact-input swap + struct ExactInputParams { + Currency currencyIn; + PathKey[] path; + uint128 amountIn; + uint128 amountOutMinimum; + } + + /// @notice Parameters for a single-hop exact-output swap + struct ExactOutputSingleParams { + PoolKey poolKey; + bool zeroForOne; + uint128 amountOut; + uint128 amountInMaximum; + uint160 sqrtPriceLimitX96; + bytes hookData; + } + + /// @notice Parameters for a multi-hop exact-output swap + struct ExactOutputParams { + Currency currencyOut; + PathKey[] path; + uint128 amountOut; + uint128 amountInMaximum; + } +} diff --git a/contracts/interfaces/external/IERC20PermitAllowed.sol b/src/interfaces/external/IERC20PermitAllowed.sol similarity index 100% rename from contracts/interfaces/external/IERC20PermitAllowed.sol rename to src/interfaces/external/IERC20PermitAllowed.sol diff --git a/contracts/lens/Quoter.sol b/src/lens/Quoter.sol similarity index 84% rename from contracts/lens/Quoter.sol rename to src/lens/Quoter.sol index 9e9bfda2..fcf63d0c 100644 --- a/contracts/lens/Quoter.sol +++ b/src/lens/Quoter.sol @@ -14,8 +14,9 @@ import {IQuoter} from "../interfaces/IQuoter.sol"; import {PoolTicksCounter} from "../libraries/PoolTicksCounter.sol"; import {PathKey, PathKeyLib} from "../libraries/PathKey.sol"; import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; +import {SafeCallback} from "../base/SafeCallback.sol"; -contract Quoter is IQuoter, IUnlockCallback { +contract Quoter is IQuoter, SafeCallback { using Hooks for IHooks; using PoolIdLibrary for PoolKey; using PathKeyLib for PathKey; @@ -24,9 +25,6 @@ contract Quoter is IQuoter, IUnlockCallback { /// @dev cache used to check a safety condition in exact output swaps. uint128 private amountOutCached; - // v4 Singleton contract - IPoolManager public immutable manager; - /// @dev min valid reason is 3-words long /// @dev int128[2] + sqrtPriceX96After padded to 32bytes + intializeTicksLoaded padded to 32bytes uint256 internal constant MINIMUM_VALID_RESPONSE_LENGTH = 96; @@ -54,9 +52,7 @@ contract Quoter is IQuoter, IUnlockCallback { _; } - constructor(address _poolManager) { - manager = IPoolManager(_poolManager); - } + constructor(IPoolManager _poolManager) SafeCallback(_poolManager) {} /// @inheritdoc IQuoter function quoteExactInputSingle(QuoteExactSingleParams memory params) @@ -64,7 +60,7 @@ contract Quoter is IQuoter, IUnlockCallback { override returns (int128[] memory deltaAmounts, uint160 sqrtPriceX96After, uint32 initializedTicksLoaded) { - try manager.unlock(abi.encodeWithSelector(this._quoteExactInputSingle.selector, params)) {} + try poolManager.unlock(abi.encodeWithSelector(this._quoteExactInputSingle.selector, params)) {} catch (bytes memory reason) { return _handleRevertSingle(reason); } @@ -79,7 +75,7 @@ contract Quoter is IQuoter, IUnlockCallback { uint32[] memory initializedTicksLoadedList ) { - try manager.unlock(abi.encodeWithSelector(this._quoteExactInput.selector, params)) {} + try poolManager.unlock(abi.encodeWithSelector(this._quoteExactInput.selector, params)) {} catch (bytes memory reason) { return _handleRevert(reason); } @@ -91,7 +87,7 @@ contract Quoter is IQuoter, IUnlockCallback { override returns (int128[] memory deltaAmounts, uint160 sqrtPriceX96After, uint32 initializedTicksLoaded) { - try manager.unlock(abi.encodeWithSelector(this._quoteExactOutputSingle.selector, params)) {} + try poolManager.unlock(abi.encodeWithSelector(this._quoteExactOutputSingle.selector, params)) {} catch (bytes memory reason) { if (params.sqrtPriceLimitX96 == 0) delete amountOutCached; return _handleRevertSingle(reason); @@ -108,18 +104,13 @@ contract Quoter is IQuoter, IUnlockCallback { uint32[] memory initializedTicksLoadedList ) { - try manager.unlock(abi.encodeWithSelector(this._quoteExactOutput.selector, params)) {} + try poolManager.unlock(abi.encodeWithSelector(this._quoteExactOutput.selector, params)) {} catch (bytes memory reason) { return _handleRevert(reason); } } - /// @inheritdoc IUnlockCallback - function unlockCallback(bytes calldata data) external returns (bytes memory) { - if (msg.sender != address(manager)) { - revert InvalidUnlockCallbackSender(); - } - + function _unlockCallback(bytes calldata data) internal override returns (bytes memory) { (bool success, bytes memory returnData) = address(this).call(data); if (success) return returnData; if (returnData.length == 0) revert LockFailure(); @@ -164,7 +155,7 @@ contract Quoter is IQuoter, IUnlockCallback { } /// @dev quote an ExactInput swap along a path of tokens, then revert with the result - function _quoteExactInput(QuoteExactParams memory params) public selfOnly returns (bytes memory) { + function _quoteExactInput(QuoteExactParams calldata params) public selfOnly returns (bytes memory) { uint256 pathLength = params.path.length; QuoteResult memory result = QuoteResult({ @@ -177,7 +168,7 @@ contract Quoter is IQuoter, IUnlockCallback { for (uint256 i = 0; i < pathLength; i++) { (PoolKey memory poolKey, bool zeroForOne) = params.path[i].getPoolAndSwapDirection(i == 0 ? params.exactCurrency : cache.prevCurrency); - (, cache.tickBefore,,) = manager.getSlot0(poolKey.toId()); + (, cache.tickBefore,,) = poolManager.getSlot0(poolKey.toId()); (cache.curDeltas, cache.sqrtPriceX96After, cache.tickAfter) = _swap( poolKey, @@ -197,7 +188,7 @@ contract Quoter is IQuoter, IUnlockCallback { cache.prevCurrency = params.path[i].intermediateCurrency; result.sqrtPriceX96AfterList[i] = cache.sqrtPriceX96After; result.initializedTicksLoadedList[i] = - PoolTicksCounter.countInitializedTicksLoaded(manager, poolKey, cache.tickBefore, cache.tickAfter); + PoolTicksCounter.countInitializedTicksLoaded(poolManager, poolKey, cache.tickBefore, cache.tickAfter); } bytes memory r = abi.encode(result.deltaAmounts, result.sqrtPriceX96AfterList, result.initializedTicksLoadedList); @@ -207,8 +198,8 @@ contract Quoter is IQuoter, IUnlockCallback { } /// @dev quote an ExactInput swap on a pool, then revert with the result - function _quoteExactInputSingle(QuoteExactSingleParams memory params) public selfOnly returns (bytes memory) { - (, int24 tickBefore,,) = manager.getSlot0(params.poolKey.toId()); + function _quoteExactInputSingle(QuoteExactSingleParams calldata params) public selfOnly returns (bytes memory) { + (, int24 tickBefore,,) = poolManager.getSlot0(params.poolKey.toId()); (BalanceDelta deltas, uint160 sqrtPriceX96After, int24 tickAfter) = _swap( params.poolKey, @@ -224,7 +215,7 @@ contract Quoter is IQuoter, IUnlockCallback { deltaAmounts[1] = -deltas.amount1(); uint32 initializedTicksLoaded = - PoolTicksCounter.countInitializedTicksLoaded(manager, params.poolKey, tickBefore, tickAfter); + PoolTicksCounter.countInitializedTicksLoaded(poolManager, params.poolKey, tickBefore, tickAfter); bytes memory result = abi.encode(deltaAmounts, sqrtPriceX96After, initializedTicksLoaded); assembly { revert(add(0x20, result), mload(result)) @@ -232,7 +223,7 @@ contract Quoter is IQuoter, IUnlockCallback { } /// @dev quote an ExactOutput swap along a path of tokens, then revert with the result - function _quoteExactOutput(QuoteExactParams memory params) public selfOnly returns (bytes memory) { + function _quoteExactOutput(QuoteExactParams calldata params) public selfOnly returns (bytes memory) { uint256 pathLength = params.path.length; QuoteResult memory result = QuoteResult({ @@ -251,7 +242,7 @@ contract Quoter is IQuoter, IUnlockCallback { params.path[i - 1], i == pathLength ? params.exactCurrency : cache.prevCurrency ); - (, cache.tickBefore,,) = manager.getSlot0(poolKey.toId()); + (, cache.tickBefore,,) = poolManager.getSlot0(poolKey.toId()); (cache.curDeltas, cache.sqrtPriceX96After, cache.tickAfter) = _swap(poolKey, !oneForZero, int256(uint256(curAmountOut)), 0, params.path[i - 1].hookData); @@ -268,7 +259,7 @@ contract Quoter is IQuoter, IUnlockCallback { cache.prevCurrency = params.path[i - 1].intermediateCurrency; result.sqrtPriceX96AfterList[i - 1] = cache.sqrtPriceX96After; result.initializedTicksLoadedList[i - 1] = - PoolTicksCounter.countInitializedTicksLoaded(manager, poolKey, cache.tickBefore, cache.tickAfter); + PoolTicksCounter.countInitializedTicksLoaded(poolManager, poolKey, cache.tickBefore, cache.tickAfter); } bytes memory r = abi.encode(result.deltaAmounts, result.sqrtPriceX96AfterList, result.initializedTicksLoadedList); @@ -278,11 +269,11 @@ contract Quoter is IQuoter, IUnlockCallback { } /// @dev quote an ExactOutput swap on a pool, then revert with the result - function _quoteExactOutputSingle(QuoteExactSingleParams memory params) public selfOnly returns (bytes memory) { + function _quoteExactOutputSingle(QuoteExactSingleParams calldata params) public selfOnly returns (bytes memory) { // if no price limit has been specified, cache the output amount for comparison in the swap callback if (params.sqrtPriceLimitX96 == 0) amountOutCached = params.exactAmount; - (, int24 tickBefore,,) = manager.getSlot0(params.poolKey.toId()); + (, int24 tickBefore,,) = poolManager.getSlot0(params.poolKey.toId()); (BalanceDelta deltas, uint160 sqrtPriceX96After, int24 tickAfter) = _swap( params.poolKey, params.zeroForOne, @@ -298,7 +289,7 @@ contract Quoter is IQuoter, IUnlockCallback { deltaAmounts[1] = -deltas.amount1(); uint32 initializedTicksLoaded = - PoolTicksCounter.countInitializedTicksLoaded(manager, params.poolKey, tickBefore, tickAfter); + PoolTicksCounter.countInitializedTicksLoaded(poolManager, params.poolKey, tickBefore, tickAfter); bytes memory result = abi.encode(deltaAmounts, sqrtPriceX96After, initializedTicksLoaded); assembly { revert(add(0x20, result), mload(result)) @@ -312,9 +303,9 @@ contract Quoter is IQuoter, IUnlockCallback { bool zeroForOne, int256 amountSpecified, uint160 sqrtPriceLimitX96, - bytes memory hookData + bytes calldata hookData ) private returns (BalanceDelta deltas, uint160 sqrtPriceX96After, int24 tickAfter) { - deltas = manager.swap( + deltas = poolManager.swap( poolKey, IPoolManager.SwapParams({ zeroForOne: zeroForOne, @@ -327,7 +318,7 @@ contract Quoter is IQuoter, IUnlockCallback { if (amountOutCached != 0 && amountOutCached != uint128(zeroForOne ? deltas.amount1() : deltas.amount0())) { revert InsufficientAmountOut(); } - (sqrtPriceX96After, tickAfter,,) = manager.getSlot0(poolKey.toId()); + (sqrtPriceX96After, tickAfter,,) = poolManager.getSlot0(poolKey.toId()); } /// @dev return either the sqrtPriceLimit from user input, or the max/min value possible depending on trade direction diff --git a/src/lens/StateView.sol b/src/lens/StateView.sol new file mode 100644 index 00000000..e3cea895 --- /dev/null +++ b/src/lens/StateView.sol @@ -0,0 +1,189 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.20; + +import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; +import {PoolId} from "@uniswap/v4-core/src/types/PoolId.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {Position} from "@uniswap/v4-core/src/libraries/Position.sol"; + +import {ImmutableState} from "../base/ImmutableState.sol"; + +/// @notice A view only contract wrapping the StateLibrary.sol library for reading storage in v4-core. +contract StateView is ImmutableState { + using StateLibrary for IPoolManager; + + constructor(IPoolManager _poolManager) ImmutableState(_poolManager) {} + + /** + * @notice Get Slot0 of the pool: sqrtPriceX96, tick, protocolFee, lpFee + * @dev Corresponds to pools[poolId].slot0 + * @param poolId The ID of the pool. + * @return sqrtPriceX96 The square root of the price of the pool, in Q96 precision. + * @return tick The current tick of the pool. + * @return protocolFee The protocol fee of the pool. + * @return lpFee The swap fee of the pool. + */ + function getSlot0(PoolId poolId) + external + view + returns (uint160 sqrtPriceX96, int24 tick, uint24 protocolFee, uint24 lpFee) + { + return poolManager.getSlot0(poolId); + } + + /** + * @notice Retrieves the tick information of a pool at a specific tick. + * @dev Corresponds to pools[poolId].ticks[tick] + * @param poolId The ID of the pool. + * @param tick The tick to retrieve information for. + * @return liquidityGross The total position liquidity that references this tick + * @return liquidityNet The amount of net liquidity added (subtracted) when tick is crossed from left to right (right to left) + * @return feeGrowthOutside0X128 fee growth per unit of liquidity on the _other_ side of this tick (relative to the current tick) + * @return feeGrowthOutside1X128 fee growth per unit of liquidity on the _other_ side of this tick (relative to the current tick) + */ + function getTickInfo(PoolId poolId, int24 tick) + external + view + returns ( + uint128 liquidityGross, + int128 liquidityNet, + uint256 feeGrowthOutside0X128, + uint256 feeGrowthOutside1X128 + ) + { + return poolManager.getTickInfo(poolId, tick); + } + + /** + * @notice Retrieves the liquidity information of a pool at a specific tick. + * @dev Corresponds to pools[poolId].ticks[tick].liquidityGross and pools[poolId].ticks[tick].liquidityNet. A more gas efficient version of getTickInfo + * @param poolId The ID of the pool. + * @param tick The tick to retrieve liquidity for. + * @return liquidityGross The total position liquidity that references this tick + * @return liquidityNet The amount of net liquidity added (subtracted) when tick is crossed from left to right (right to left) + */ + function getTickLiquidity(PoolId poolId, int24 tick) + external + view + returns (uint128 liquidityGross, int128 liquidityNet) + { + return poolManager.getTickLiquidity(poolId, tick); + } + + /** + * @notice Retrieves the fee growth outside a tick range of a pool + * @dev Corresponds to pools[poolId].ticks[tick].feeGrowthOutside0X128 and pools[poolId].ticks[tick].feeGrowthOutside1X128. A more gas efficient version of getTickInfo + * @param poolId The ID of the pool. + * @param tick The tick to retrieve fee growth for. + * @return feeGrowthOutside0X128 fee growth per unit of liquidity on the _other_ side of this tick (relative to the current tick) + * @return feeGrowthOutside1X128 fee growth per unit of liquidity on the _other_ side of this tick (relative to the current tick) + */ + function getTickFeeGrowthOutside(PoolId poolId, int24 tick) + external + view + returns (uint256 feeGrowthOutside0X128, uint256 feeGrowthOutside1X128) + { + return poolManager.getTickFeeGrowthOutside(poolId, tick); + } + + /** + * @notice Retrieves the global fee growth of a pool. + * @dev Corresponds to pools[poolId].feeGrowthGlobal0X128 and pools[poolId].feeGrowthGlobal1X128 + * @param poolId The ID of the pool. + * @return feeGrowthGlobal0 The global fee growth for token0. + * @return feeGrowthGlobal1 The global fee growth for token1. + */ + function getFeeGrowthGlobals(PoolId poolId) + external + view + returns (uint256 feeGrowthGlobal0, uint256 feeGrowthGlobal1) + { + return poolManager.getFeeGrowthGlobals(poolId); + } + + /** + * @notice Retrieves total the liquidity of a pool. + * @dev Corresponds to pools[poolId].liquidity + * @param poolId The ID of the pool. + * @return liquidity The liquidity of the pool. + */ + function getLiquidity(PoolId poolId) external view returns (uint128 liquidity) { + return poolManager.getLiquidity(poolId); + } + + /** + * @notice Retrieves the tick bitmap of a pool at a specific tick. + * @dev Corresponds to pools[poolId].tickBitmap[tick] + * @param poolId The ID of the pool. + * @param tick The tick to retrieve the bitmap for. + * @return tickBitmap The bitmap of the tick. + */ + function getTickBitmap(PoolId poolId, int16 tick) external view returns (uint256 tickBitmap) { + return poolManager.getTickBitmap(poolId, tick); + } + + /** + * @notice Retrieves the position info without needing to calculate the `positionId`. + * @dev Corresponds to pools[poolId].positions[positionId] + * @param poolId The ID of the pool. + * @param owner The owner of the liquidity position. + * @param tickLower The lower tick of the liquidity range. + * @param tickUpper The upper tick of the liquidity range. + * @param salt The bytes32 randomness to further distinguish position state. + * @return liquidity The liquidity of the position. + * @return feeGrowthInside0LastX128 The fee growth inside the position for token0. + * @return feeGrowthInside1LastX128 The fee growth inside the position for token1. + */ + function getPositionInfo(PoolId poolId, address owner, int24 tickLower, int24 tickUpper, bytes32 salt) + external + view + returns (uint128 liquidity, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128) + { + return poolManager.getPositionInfo(poolId, owner, tickLower, tickUpper, salt); + } + + /** + * @notice Retrieves the position information of a pool at a specific position ID. + * @dev Corresponds to pools[poolId].positions[positionId] + * @param poolId The ID of the pool. + * @param positionId The ID of the position. + * @return liquidity The liquidity of the position. + * @return feeGrowthInside0LastX128 The fee growth inside the position for token0. + * @return feeGrowthInside1LastX128 The fee growth inside the position for token1. + */ + function getPositionInfo(PoolId poolId, bytes32 positionId) + external + view + returns (uint128 liquidity, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128) + { + return poolManager.getPositionInfo(poolId, positionId); + } + + /** + * @notice Retrieves the liquidity of a position. + * @dev Corresponds to pools[poolId].positions[positionId].liquidity. More gas efficient for just retrieiving liquidity as compared to getPositionInfo + * @param poolId The ID of the pool. + * @param positionId The ID of the position. + * @return liquidity The liquidity of the position. + */ + function getPositionLiquidity(PoolId poolId, bytes32 positionId) external view returns (uint128 liquidity) { + return poolManager.getPositionLiquidity(poolId, positionId); + } + + /** + * @notice Calculate the fee growth inside a tick range of a pool + * @dev pools[poolId].feeGrowthInside0LastX128 in Position.Info is cached and can become stale. This function will calculate the up to date feeGrowthInside + * @param poolId The ID of the pool. + * @param tickLower The lower tick of the range. + * @param tickUpper The upper tick of the range. + * @return feeGrowthInside0X128 The fee growth inside the tick range for token0. + * @return feeGrowthInside1X128 The fee growth inside the tick range for token1. + */ + function getFeeGrowthInside(PoolId poolId, int24 tickLower, int24 tickUpper) + external + view + returns (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) + { + return poolManager.getFeeGrowthInside(poolId, tickLower, tickUpper); + } +} diff --git a/src/libraries/Actions.sol b/src/libraries/Actions.sol new file mode 100644 index 00000000..132fde68 --- /dev/null +++ b/src/libraries/Actions.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.24; + +/// @notice Library to define different pool actions. +/// @dev These are suggested common commands, however additional commands should be defined as required +library Actions { + // pool actions + // liquidity actions + uint256 constant INCREASE_LIQUIDITY = 0x00; + uint256 constant DECREASE_LIQUIDITY = 0x01; + uint256 constant MINT_POSITION = 0x02; + uint256 constant BURN_POSITION = 0x03; + // 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; + // donate + uint256 constant DONATE = 0x08; + + // closing deltas on the pool manager + // settling + uint256 constant SETTLE = 0x10; + uint256 constant SETTLE_ALL = 0x11; + uint256 constant SETTLE_WITH_BALANCE = 0x12; + // taking + uint256 constant TAKE = 0x13; + uint256 constant TAKE_ALL = 0x14; + uint256 constant TAKE_PORTION = 0x15; + + uint256 constant CLOSE_CURRENCY = 0x16; + uint256 constant CLOSE_PAIR = 0x17; + uint256 constant CLEAR = 0x18; + uint256 constant SWEEP = 0x19; + + // minting/burning 6909s to close deltas + uint256 constant MINT_6909 = 0x20; + uint256 constant BURN_6909 = 0x21; +} diff --git a/src/libraries/CalldataDecoder.sol b/src/libraries/CalldataDecoder.sol new file mode 100644 index 00000000..b2ea3c38 --- /dev/null +++ b/src/libraries/CalldataDecoder.sol @@ -0,0 +1,227 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.0; + +import {PositionConfig} from "./PositionConfig.sol"; +import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; +import {IV4Router} from "../interfaces/IV4Router.sol"; + +/// @title Library for abi decoding in calldata +library CalldataDecoder { + using CalldataDecoder for bytes; + + error SliceOutOfBounds(); + + /// @notice equivalent to SliceOutOfBounds.selector + bytes4 constant SLICE_ERROR_SELECTOR = 0x3b99b53d; + + /// @dev equivalent to: abi.decode(params, (uint256[], bytes[])) in calldata + function decodeActionsRouterParams(bytes calldata _bytes) + internal + pure + returns (bytes calldata actions, bytes[] calldata params) + { + assembly ("memory-safe") { + // The offset of the 0th element is 0, which stores the offset of the length pointer of actions array. + // The offset of the 1st element is 32, which stores the offset of the length pointer of params array. + let actionsPtr := add(_bytes.offset, calldataload(_bytes.offset)) + let paramsPtr := add(_bytes.offset, calldataload(add(_bytes.offset, 0x20))) + + // The length is stored as the first element + actions.length := calldataload(actionsPtr) + params.length := calldataload(paramsPtr) + + // The actual data is stored in the slot after the length + actions.offset := add(actionsPtr, 0x20) + params.offset := add(paramsPtr, 0x20) + + // Calculate how far `params` is into the provided bytes + let relativeOffset := sub(params.offset, _bytes.offset) + // Check that that isnt longer than the bytes themselves, or revert + if lt(_bytes.length, add(params.length, relativeOffset)) { + mstore(0, SLICE_ERROR_SELECTOR) + revert(0, 0x04) + } + } + } + + /// @dev equivalent to: abi.decode(params, (uint256, PositionConfig, uint256, uint128, uint128, bytes)) in calldata + function decodeModifyLiquidityParams(bytes calldata params) + internal + pure + returns ( + uint256 tokenId, + PositionConfig calldata config, + uint256 liquidity, + uint128 amount0, + uint128 amount1, + bytes calldata hookData + ) + { + assembly ("memory-safe") { + tokenId := calldataload(params.offset) + config := add(params.offset, 0x20) + liquidity := calldataload(add(params.offset, 0x100)) + amount0 := calldataload(add(params.offset, 0x120)) + amount1 := calldataload(add(params.offset, 0x140)) + } + hookData = params.toBytes(11); + } + + /// @dev equivalent to: abi.decode(params, (PositionConfig, uint256, uint128, uint128, address, bytes)) in calldata + function decodeMintParams(bytes calldata params) + internal + pure + returns ( + PositionConfig calldata config, + uint256 liquidity, + uint128 amount0Max, + uint128 amount1Max, + address owner, + bytes calldata hookData + ) + { + assembly ("memory-safe") { + config := params.offset + liquidity := calldataload(add(params.offset, 0xe0)) + amount0Max := calldataload(add(params.offset, 0x100)) + amount1Max := calldataload(add(params.offset, 0x120)) + owner := calldataload(add(params.offset, 0x140)) + } + hookData = params.toBytes(11); + } + + /// @dev equivalent to: abi.decode(params, (uint256, PositionConfig, uint128, uint128, bytes)) in calldata + function decodeBurnParams(bytes calldata params) + internal + pure + returns ( + uint256 tokenId, + PositionConfig calldata config, + uint128 amount0Min, + uint128 amount1Min, + bytes calldata hookData + ) + { + assembly ("memory-safe") { + tokenId := calldataload(params.offset) + config := add(params.offset, 0x20) + amount0Min := calldataload(add(params.offset, 0x100)) + amount1Min := calldataload(add(params.offset, 0x120)) + } + hookData = params.toBytes(10); + } + + /// @dev equivalent to: abi.decode(params, (IV4Router.ExactInputParams)) + function decodeSwapExactInParams(bytes calldata params) + internal + pure + returns (IV4Router.ExactInputParams calldata swapParams) + { + // ExactInputParams is a variable length struct so we just have to look up its location + assembly ("memory-safe") { + swapParams := add(params.offset, calldataload(params.offset)) + } + } + + /// @dev equivalent to: abi.decode(params, (IV4Router.ExactInputSingleParams)) + function decodeSwapExactInSingleParams(bytes calldata params) + internal + pure + returns (IV4Router.ExactInputSingleParams calldata swapParams) + { + // ExactInputSingleParams is a variable length struct so we just have to look up its location + assembly ("memory-safe") { + swapParams := add(params.offset, calldataload(params.offset)) + } + } + + /// @dev equivalent to: abi.decode(params, (IV4Router.ExactOutputParams)) + function decodeSwapExactOutParams(bytes calldata params) + internal + pure + returns (IV4Router.ExactOutputParams calldata swapParams) + { + // ExactOutputParams is a variable length struct so we just have to look up its location + assembly ("memory-safe") { + swapParams := add(params.offset, calldataload(params.offset)) + } + } + + /// @dev equivalent to: abi.decode(params, (IV4Router.ExactInputSingleParams)) + function decodeSwapExactOutSingleParams(bytes calldata params) + internal + pure + returns (IV4Router.ExactOutputSingleParams calldata swapParams) + { + // ExactOutputSingleParams is a variable length struct so we just have to look up its location + assembly ("memory-safe") { + swapParams := add(params.offset, calldataload(params.offset)) + } + } + + /// @dev equivalent to: abi.decode(params, (Currency)) in calldata + function decodeCurrency(bytes calldata params) internal pure returns (Currency currency) { + assembly ("memory-safe") { + currency := calldataload(params.offset) + } + } + + /// @dev equivalent to: abi.decode(params, (Currency, address)) in calldata + function decodeCurrencyAndAddress(bytes calldata params) + internal + pure + returns (Currency currency, address _address) + { + assembly ("memory-safe") { + currency := calldataload(params.offset) + _address := calldataload(add(params.offset, 0x20)) + } + } + + /// @dev equivalent to: abi.decode(params, (Currency, uint256)) in calldata + function decodeCurrencyAndUint256(bytes calldata params) + internal + pure + returns (Currency currency, uint256 amount) + { + assembly ("memory-safe") { + currency := calldataload(params.offset) + amount := calldataload(add(params.offset, 0x20)) + } + } + + /// @notice Decode the `_arg`-th element in `_bytes` as a dynamic array + /// @dev The decoding of `length` and `offset` is universal, + /// whereas the type declaration of `res` instructs the compiler how to read it. + /// @param _bytes The input bytes string to slice + /// @param _arg The index of the argument to extract + /// @return length Length of the array + /// @return offset Pointer to the data part of the array + function toLengthOffset(bytes calldata _bytes, uint256 _arg) + internal + pure + returns (uint256 length, uint256 offset) + { + uint256 relativeOffset; + assembly ("memory-safe") { + // The offset of the `_arg`-th element is `32 * arg`, which stores the offset of the length pointer. + // shl(5, x) is equivalent to mul(32, x) + let lengthPtr := add(_bytes.offset, calldataload(add(_bytes.offset, shl(5, _arg)))) + length := calldataload(lengthPtr) + offset := add(lengthPtr, 0x20) + relativeOffset := sub(offset, _bytes.offset) + } + if (_bytes.length < length + relativeOffset) revert SliceOutOfBounds(); + } + + /// @notice Decode the `_arg`-th element in `_bytes` as `bytes` + /// @param _bytes The input bytes string to extract a bytes string from + /// @param _arg The index of the argument to extract + function toBytes(bytes calldata _bytes, uint256 _arg) internal pure returns (bytes calldata res) { + (uint256 length, uint256 offset) = toLengthOffset(_bytes, _arg); + assembly ("memory-safe") { + res.length := length + res.offset := offset + } + } +} diff --git a/src/libraries/ChainId.sol b/src/libraries/ChainId.sol new file mode 100644 index 00000000..7e67989c --- /dev/null +++ b/src/libraries/ChainId.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity >=0.7.0; + +/// @title Function for getting the current chain ID +library ChainId { + /// @dev Gets the current chain ID + /// @return chainId The current chain ID + function get() internal view returns (uint256 chainId) { + assembly { + chainId := chainid() + } + } +} diff --git a/src/libraries/CurrencyDeltas.sol b/src/libraries/CurrencyDeltas.sol new file mode 100644 index 00000000..2a7b85f8 --- /dev/null +++ b/src/libraries/CurrencyDeltas.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; +import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; + +/// @title Currency Deltas +/// @notice Fetch two currency deltas in a single call +library CurrencyDeltas { + using SafeCast for int256; + + /// @notice Get the current delta for a caller in the two given currencies + /// @param _caller The address of the caller + /// @param currency0 The currency to lookup the delta + /// @param currency1 The other currency to lookup the delta + /// @return BalanceDelta The delta of the two currencies packed + /// amount0 corresponding to currency0 and amount1 corresponding to currency1 + function currencyDeltas(IPoolManager manager, address _caller, Currency currency0, Currency currency1) + internal + view + returns (BalanceDelta) + { + bytes32 tloadSlot0; + bytes32 tloadSlot1; + assembly { + mstore(0, _caller) + mstore(32, currency0) + tloadSlot0 := keccak256(0, 64) + + mstore(0, _caller) + mstore(32, currency1) + tloadSlot1 := keccak256(0, 64) + } + bytes32[] memory slots = new bytes32[](2); + slots[0] = tloadSlot0; + slots[1] = tloadSlot1; + bytes32[] memory result = manager.exttload(slots); + return toBalanceDelta(int256(uint256(result[0])).toInt128(), int256(uint256(result[1])).toInt128()); + } +} diff --git a/src/libraries/HookFees.sol b/src/libraries/HookFees.sol new file mode 100644 index 00000000..38d90092 --- /dev/null +++ b/src/libraries/HookFees.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import {BalanceDelta, BalanceDeltaLibrary} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; + +/// @title Safe casting methods +/// @notice Contains methods for safely casting between types +/// TODO after audits move this function to core's SafeCast.sol! +library HookFees { + using BalanceDeltaLibrary for BalanceDelta; + + uint256 public constant MAX_BIPS = 10_000; + + /// @notice Calculates fee from a BalanceDelta and a percent + /// @param delta The same delta from afterRemoveLiquidity + /// @param maxFeeBips The maximum fee in basis points + function calculateFeesFrom(BalanceDelta delta, uint256 maxFeeBips) internal pure returns (int256, int256) { + unchecked { + return ( + int128(int256(uint256(int256(delta.amount0())) * maxFeeBips / MAX_BIPS)), + int128(int256(uint256(int256(delta.amount1())) * maxFeeBips / MAX_BIPS)) + ); + } + } +} diff --git a/src/libraries/Locker.sol b/src/libraries/Locker.sol new file mode 100644 index 00000000..713779f1 --- /dev/null +++ b/src/libraries/Locker.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.24; + +/// @notice This is a temporary library that allows us to use transient storage (tstore/tload) +/// TODO: This library can be deleted when we have the transient keyword support in solidity. +library Locker { + // The slot holding the locker state, transiently. bytes32(uint256(keccak256("LockedBy")) - 1) + bytes32 constant LOCKED_BY_SLOT = 0x0aedd6bde10e3aa2adec092b02a3e3e805795516cda41f27aa145b8f300af87a; + + function set(address locker) internal { + assembly { + tstore(LOCKED_BY_SLOT, locker) + } + } + + function get() internal view returns (address locker) { + assembly { + locker := tload(LOCKED_BY_SLOT) + } + } +} diff --git a/contracts/libraries/PathKey.sol b/src/libraries/PathKey.sol similarity index 69% rename from contracts/libraries/PathKey.sol rename to src/libraries/PathKey.sol index f9d5da33..a286076d 100644 --- a/contracts/libraries/PathKey.sol +++ b/src/libraries/PathKey.sol @@ -1,5 +1,4 @@ //SPDX-License-Identifier: UNLICENSED - pragma solidity ^0.8.20; import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; @@ -15,14 +14,14 @@ struct PathKey { } library PathKeyLib { - function getPoolAndSwapDirection(PathKey memory params, Currency currencyIn) + function getPoolAndSwapDirection(PathKey calldata params, Currency currencyIn) internal pure returns (PoolKey memory poolKey, bool zeroForOne) { - (Currency currency0, Currency currency1) = currencyIn < params.intermediateCurrency - ? (currencyIn, params.intermediateCurrency) - : (params.intermediateCurrency, currencyIn); + Currency currencyOut = params.intermediateCurrency; + (Currency currency0, Currency currency1) = + currencyIn < currencyOut ? (currencyIn, currencyOut) : (currencyOut, currencyIn); zeroForOne = currencyIn == currency0; poolKey = PoolKey(currency0, currency1, params.fee, params.tickSpacing, params.hooks); diff --git a/contracts/libraries/PoolTicksCounter.sol b/src/libraries/PoolTicksCounter.sol similarity index 99% rename from contracts/libraries/PoolTicksCounter.sol rename to src/libraries/PoolTicksCounter.sol index 60fdbbe5..7420ffd5 100644 --- a/contracts/libraries/PoolTicksCounter.sol +++ b/src/libraries/PoolTicksCounter.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity >=0.8.20; -import {PoolGetters} from "./PoolGetters.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; diff --git a/src/libraries/PositionConfig.sol b/src/libraries/PositionConfig.sol new file mode 100644 index 00000000..d3bd8e24 --- /dev/null +++ b/src/libraries/PositionConfig.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.24; + +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; + +// A PositionConfig is the input for creating and modifying a Position in core, set per tokenId +struct PositionConfig { + PoolKey poolKey; + int24 tickLower; + int24 tickUpper; +} + +/// @notice Library for computing the configId given a PositionConfig +library PositionConfigLibrary { + function toId(PositionConfig calldata config) internal pure returns (bytes32 id) { + // id = keccak256(abi.encodePacked(currency0, currency1, fee, tickSpacing, hooks, tickLower, tickUpper))) + assembly ("memory-safe") { + let fmp := mload(0x40) + mstore(add(fmp, 0x34), calldataload(add(config, 0xc0))) // tickUpper: [0x51, 0x54) + mstore(add(fmp, 0x31), calldataload(add(config, 0xa0))) // tickLower: [0x4E, 0x51) + mstore(add(fmp, 0x2E), calldataload(add(config, 0x80))) // hooks: [0x3A, 0x4E) + mstore(add(fmp, 0x1A), calldataload(add(config, 0x60))) // tickSpacing: [0x37, 0x3A) + mstore(add(fmp, 0x17), calldataload(add(config, 0x40))) // fee: [0x34, 0x37) + mstore(add(fmp, 0x14), calldataload(add(config, 0x20))) // currency1: [0x20, 0x34) + mstore(fmp, calldataload(config)) // currency0: [0x0c, 0x20) + + id := keccak256(add(fmp, 0x0c), 0x48) // len is 72 bytes + + // now clean the memory we used + mstore(add(fmp, 0x40), 0) // fmp+0x40 held hooks (14 bytes), tickLower, tickUpper + mstore(add(fmp, 0x20), 0) // fmp+0x20 held currency1, fee, tickSpacing, hooks (6 bytes) + mstore(fmp, 0) // fmp held currency0 + } + } +} diff --git a/src/libraries/SafeCast.sol b/src/libraries/SafeCast.sol new file mode 100644 index 00000000..ae16b2d1 --- /dev/null +++ b/src/libraries/SafeCast.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import {CustomRevert} from "@uniswap/v4-core/src/libraries/CustomRevert.sol"; + +/// @title Safe casting methods +/// @notice Contains methods for safely casting between types +/// TODO after audits move this function to core's SafeCast.sol! +library SafeCastTemp { + using CustomRevert for bytes4; + + error SafeCastOverflow(); + + /// @notice Cast a int128 to a uint128, revert on overflow or underflow + /// @param x The int128 to be casted + /// @return y The casted integer, now type uint128 + function toUint128(int128 x) internal pure returns (uint128 y) { + if (x < 0) SafeCastOverflow.selector.revertWith(); + y = uint128(x); + } +} diff --git a/src/libraries/SlippageCheck.sol b/src/libraries/SlippageCheck.sol new file mode 100644 index 00000000..efeafca9 --- /dev/null +++ b/src/libraries/SlippageCheck.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; + +/// @title Slippage Check Library +/// @notice a library for checking if a delta exceeds a maximum ceiling or fails to meet a minimum floor +library SlippageCheckLibrary { + error MaximumAmountExceeded(); + error MinimumAmountInsufficient(); + + /// @notice Revert if one or both deltas does not meet a minimum output + /// @dev to be used when removing liquidity to guarantee a minimum output + function validateMinOut(BalanceDelta delta, uint128 amount0Min, uint128 amount1Min) internal pure { + if (uint128(delta.amount0()) < amount0Min || uint128(delta.amount1()) < amount1Min) { + revert MinimumAmountInsufficient(); + } + } + + /// @notice Revert if one or both deltas exceeds a maximum input + /// @dev to be used when minting liquidity to guarantee a maximum input + function validateMaxIn(BalanceDelta delta, uint128 amount0Max, uint128 amount1Max) internal pure { + if (uint128(-delta.amount0()) > amount0Max || uint128(-delta.amount1()) > amount1Max) { + revert MaximumAmountExceeded(); + } + } + + /// @notice Revert if one or both deltas exceeds a maximum input + /// @dev When increasing liquidity, delta can be positive when excess fees need to be collected + /// in those cases, slippage checks are not required + function validateMaxInNegative(BalanceDelta delta, uint128 amount0Max, uint128 amount1Max) internal pure { + if ( + delta.amount0() < 0 && amount0Max < uint128(-delta.amount0()) + || delta.amount1() < 0 && amount1Max < uint128(-delta.amount1()) + ) revert MaximumAmountExceeded(); + } +} diff --git a/src/middleware/ABOUT_MIDDLEWARE.md b/src/middleware/ABOUT_MIDDLEWARE.md new file mode 100644 index 00000000..d7978934 --- /dev/null +++ b/src/middleware/ABOUT_MIDDLEWARE.md @@ -0,0 +1,80 @@ +# Middleware Docs +Hooks can be made to do bad things. Because anyone can create their own arbitrary logic for a hook contract, it's difficult for third parties to decide which hooks are "safe" and which are "dangerous". We propose a hook middleware that performs sanity checks on the result of a hooks to block malicious actions. + +### Implementation +Middleware factory creates middlewares. Each middleware is the hook and points to another hook as the implementation. + +https://github.com/user-attachments/assets/a7016ed2-2863-42aa-bc69-54fe2549e016 + +This allows for some convenient configurations where a pool can use a hook and another pool can use the middlewared hook. + +middleware configurations + +*all valid configurations that can be deployed. animations for illustration only.* + +Best of all, attaching a middleware to a hook is easy and usually requires no extra coding. + +### Caveats +- (because of the proxy pattern) constructors will never be called, so it may be necessary to revise the implementation contract to use an initialize function if the constructor needs to set non-immutable variables. +- let's say hook A calls a permissioned function on external contract E. a middleware pointing to hook A would then not be able to call contract E. + +### Deployment +Developers should mine a salt to generate the correct flags for the middleware. While not strictly required, it’s recommended to match the hook’s flags with the middleware’s flags. + +# Middleware Remove +An incorrectly written or malicious hook could revert while removing liquidity, potentially bricking funds or holding user funds hostage. A hook may also take a significant amount of deltas when removing liquidity, trapping the user into a high withdraw fee. + +MiddlewareRemove is one possible middleware, designed to catch this problem. + +It has two key properties: +1. If an implementation call violates a principle (defined below) the entire implementation call undoes itself, and the action proceeds as if it never was called. + - eg: a user withdraws, the beforeRemove reverts, but the afterRemove succeeds. The withdrawal proceeds, running only the afterRemove hook. +3. If an implementation call does not violate principals, the user can not purpousely force a vanilla withdraw. + - eg: a FeeTaking hook takes a 1% fee and the middleware specifies a 100 maxFeeBips. A user cannot alter the pool state to purpousely circumvent the hook. + +### Implementation +`MiddlewareRemoveFactory` takes a parameter `maxFeeBips` and deploys either a `MiddlewareRemoveNoDeltas` (0 fee) or `MiddlewareRemove` contract (capped fee). This middleware checks for the following: + +- **Reverts:** Implementation calls can not revert +- **Gas Limit:** Implementation calls can not use more than 5 million gas units +- **Function Selector:** Implementation calls must return the correct function selector, either `BaseHook.beforeRemoveLiquidity.selector` or `BaseHook.afterRemoveLiquidity.selector` +- **Correct Modification of Deltas:** Implementation calls can not modify any deltas during beforeRemoveLiquidity. + +| MiddlewareRemoveNoDeltas | MiddlewareRemove | +| --- | --- | +| can not modify any deltas during beforeAfterLiquidity | can only modify deltas attributed to the hook or caller, only on the two currencies of the pair | +| | the deltas modified must match the returned BalanceDelta | +| | this amount is capped at an immutable percent of user output specified by maxFeeBips. _eg: if a user removes 1000 USDC and 1 ETH and maxFeeBips is 100, the hook can take maximum 10 USDC and/or 0.01 ETH._ | + +If any of these conditions are violated, the contract will skip the hook call entirely. + +### Rationale +While routers can protect against front-running during swaps and adding liquidity, they cannot prevent a hook from withholding tokens. This onchain middleware wraps every case that could cause a withdrawal to fail. Although this middleware does not prevent the hook from swapping before a user removes liquidity (which may change the token composition withdrawn), such behavior is not necessarily malicious towards the user. + +The `maxFeeBips` parameter provides developers with greater flexibility, allowing them to set a clear cap on deltas they are allowed to take from the user. + +### Deployment Parameters +- **implementation** +- **maxFeeBips:** An immutable value that caps the amount of deltas returned by the hook, providing a safeguard against excessive fees. + +### Deployment Example +```solidity +uint256 maxFeeBips = 0; +(, bytes32 salt) = MiddlewareMiner.find(address(factory), flags, address(manager), implementation, maxFeeBips); +address hookAddress = factory.createMiddleware(implementation, maxFeeBips, salt); + +``` + +### Gas Snapshots +| | Unprotected | Protected | Diff | +| --- | --- | --- | --- | +| Before + After remove (only proxy) | 124822 | 128379 | 3557 | +| Before + After remove (OVERRIDE) | 124822 | 133820 | 8998 | +| Before + After remove | 124822 | 135757 | 10935 | +| Before + After remove + returns deltas | 124851 | 138303 | 13452 | +| Before + After remove + takes fee | 181009 | 197499 | 16490 | + +### Override +There is a small gas overhead when using the middleware. + +An advanced caller who is confident that the checks will pass can skip them by passing a hookData starting with OVERRIDE_BYTES. The remaining bytes will then be used to do a standard hook call. diff --git a/contracts/middleware/BaseMiddleware.sol b/src/middleware/BaseMiddleware.sol similarity index 65% rename from contracts/middleware/BaseMiddleware.sol rename to src/middleware/BaseMiddleware.sol index 2b20a056..0bc8f797 100644 --- a/contracts/middleware/BaseMiddleware.sol +++ b/src/middleware/BaseMiddleware.sol @@ -12,28 +12,19 @@ import {Proxy} from "@openzeppelin/contracts/proxy/Proxy.sol"; abstract contract BaseMiddleware is Proxy { /// @notice The address of the pool manager /// @dev Use in middleware implementations to access the pool manager - IPoolManager public immutable manager; + IPoolManager public immutable poolManager; /// @notice The address of the implementation contract. All calls to this contract will be forwarded to implementation. address public immutable implementation; error FlagsMismatch(); - constructor(IPoolManager _manager, address _impl) { - _ensureValidFlags(_impl); - manager = _manager; + constructor(IPoolManager _poolManager, address _impl) { + poolManager = _poolManager; implementation = _impl; } function _implementation() internal view override returns (address) { return implementation; } - - /// @notice Ensure that the implementation contract has the correct hook flags. - /// @dev Override to enforce hook flags. - function _ensureValidFlags(address _impl) internal view virtual { - if (uint160(address(this)) & Hooks.ALL_HOOK_MASK != uint160(_impl) & Hooks.ALL_HOOK_MASK) { - revert FlagsMismatch(); - } - } } diff --git a/src/middleware/BaseRemove.sol b/src/middleware/BaseRemove.sol new file mode 100644 index 00000000..f2bba33e --- /dev/null +++ b/src/middleware/BaseRemove.sol @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {BalanceDelta, BalanceDeltaLibrary} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {Proxy} from "@openzeppelin/contracts/proxy/Proxy.sol"; +import {BaseMiddleware} from "./BaseMiddleware.sol"; +import {BaseHook} from "../../src/base/hooks/BaseHook.sol"; +import {BalanceDeltaLibrary} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; +import {TransientStateLibrary} from "@uniswap/v4-core/src/libraries/TransientStateLibrary.sol"; + +abstract contract BaseRemove is BaseMiddleware { + using TransientStateLibrary for IPoolManager; + + /// @notice Thrown when hook permissions are forbidden + /// @param hooks The address of this contract + error HookPermissionForbidden(address hooks); + + /// @notice Thrown when there are nonzero deltas before the hook is called + /// @dev Settle previous deltas before removing liquidity + error MustResolveDeltasBeforeRemove(); + + /// @notice Thrown when the implementation modified deltas + error ImplementationModifiedDeltas(); + + /// @notice Thrown when the implementation call failed + error FailedImplementationCall(); + + bytes internal constant ZERO_BYTES = bytes(""); + uint256 public constant GAS_LIMIT = 5_000_000; + + /// @notice Value is keccak256("override") - 1 + /// @dev Use this hookData to override checks and save gas + bytes32 public constant OVERRIDE_BYTES = 0x23b70c8dec38c3dec67a5596870027b04c4058cb3ac57b4e589bf628ac6669e7; + + /// @param _poolManager The address of the pool manager + /// @param _impl The address of the implementation contract + constructor(IPoolManager _poolManager, address _impl) BaseMiddleware(_poolManager, _impl) { + _ensureValidFlags(); + } + + /// @notice The hook called before liquidity is removed. Ensures zero nonzeroDeltas + /// @param sender The initial msg.sender for the remove liquidity call + /// @param key The key for the pool + /// @param params The parameters for removing liquidity + /// @param hookData Arbitrary data handed into the PoolManager by the liquidty provider to be be passed on to the hook + /// Can call with OVERRIDE_BYTES to override checks + /// @return bytes4 The function selector for the hook + function beforeRemoveLiquidity( + address sender, + PoolKey calldata key, + IPoolManager.ModifyLiquidityParams calldata params, + bytes calldata hookData + ) external returns (bytes4) { + if (bytes32(hookData) == OVERRIDE_BYTES) { + (bool success, bytes memory returnData) = address(implementation).delegatecall( + abi.encodeWithSelector(this.beforeRemoveLiquidity.selector, sender, key, params, hookData[32:]) + ); + if (!success) { + revert FailedImplementationCall(); + } + return abi.decode(returnData, (bytes4)); + } + if (poolManager.getNonzeroDeltaCount() != 0) { + revert MustResolveDeltasBeforeRemove(); + } + address(this).delegatecall{gas: GAS_LIMIT}( + abi.encodeWithSelector(this._beforeRemoveLiquidity.selector, msg.data) + ); + return BaseHook.beforeRemoveLiquidity.selector; + } + + /// @notice Middleware function that reverts if the implementation modified deltas + /// @param data The calldata from beforeRemoveLiquidity + function _beforeRemoveLiquidity(bytes calldata data) external { + (bool success, bytes memory returnData) = address(implementation).delegatecall(data); + if (!success) { + revert FailedImplementationCall(); + } + (bytes4 selector) = abi.decode(returnData, (bytes4)); + if (selector != BaseHook.beforeRemoveLiquidity.selector) { + revert Hooks.InvalidHookResponse(); + } + if (poolManager.getNonzeroDeltaCount() != 0) { + revert ImplementationModifiedDeltas(); + } + } + + /// @notice The hook called after liquidity is removed + /// @param sender The initial msg.sender for the remove liquidity call + /// @param key The key for the pool + /// @param params The parameters for removing liquidity + /// @param delta The caller's balance delta after removing liquidity + /// @param hookData Arbitrary data handed into the PoolManager by the liquidty provider to be be passed on to the hook + /// Can call with OVERRIDE_BYTES to override checks + /// @return bytes4 The function selector for the hook + /// @return BalanceDelta The hook's delta in token0 and token1. Positive: the hook is owed/took currency, negative: the hook owes/sent currency + function afterRemoveLiquidity( + address sender, + PoolKey calldata key, + IPoolManager.ModifyLiquidityParams calldata params, + BalanceDelta delta, + bytes calldata hookData + ) external virtual returns (bytes4, BalanceDelta); + + /// @notice Ensure that the implementation contract has the correct hook flags + /// @dev Override to enforce hook flags + function _ensureValidFlags() internal view virtual {} +} diff --git a/src/middleware/MiddlewareRemove.sol b/src/middleware/MiddlewareRemove.sol new file mode 100644 index 00000000..b858d672 --- /dev/null +++ b/src/middleware/MiddlewareRemove.sol @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {BalanceDelta, BalanceDeltaLibrary} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {Proxy} from "@openzeppelin/contracts/proxy/Proxy.sol"; +import {BaseRemove} from "./BaseRemove.sol"; +import {BaseHook} from "../../src/base/hooks/BaseHook.sol"; +import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; +import {CustomRevert} from "@uniswap/v4-core/src/libraries/CustomRevert.sol"; +import {TransientStateLibrary} from "@uniswap/v4-core/src/libraries/TransientStateLibrary.sol"; +import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; +import {HookFees} from "../../src/libraries/HookFees.sol"; + +contract MiddlewareRemove is BaseRemove { + using CustomRevert for bytes4; + using Hooks for IHooks; + using TransientStateLibrary for IPoolManager; + + /// @notice Thrown when the implementation takes more fees than the max fee + error TookTooMuchFee(); + + /// @notice Thrown when the implementation returns different deltas than it modified + error DeltasReturnMismatch(); + + /// @notice Thrown when the implementation modifies deltas not of the hook or caller + error InvalidDeltasOwner(); + + /// @notice Thrown when maxFeeBips is set to a value greater than 10,000 + error MaxFeeBipsTooHigh(); + + uint256 public immutable maxFeeBips; + + /// @param _poolManager The address of the pool manager + /// @param _impl The address of the implementation contract + /// @param _maxFeeBips The maximum fee in basis points the hook is allowed to charge on removeLiquidity + constructor(IPoolManager _poolManager, address _impl, uint256 _maxFeeBips) BaseRemove(_poolManager, _impl) { + if (_maxFeeBips > HookFees.MAX_BIPS) revert MaxFeeBipsTooHigh(); + maxFeeBips = _maxFeeBips; + } + + /// @notice The hook called after liquidity is removed. Ensures valid deltas + /// @inheritdoc BaseRemove + function afterRemoveLiquidity( + address sender, + PoolKey calldata key, + IPoolManager.ModifyLiquidityParams calldata params, + BalanceDelta delta, + bytes calldata hookData + ) external override returns (bytes4, BalanceDelta) { + if (bytes32(hookData) == OVERRIDE_BYTES) { + (, bytes memory implReturnData) = address(implementation).delegatecall( + abi.encodeWithSelector(this.afterRemoveLiquidity.selector, sender, key, params, delta, hookData[32:]) + ); + return abi.decode(implReturnData, (bytes4, BalanceDelta)); + } + (bool success, bytes memory returnData) = address(this).delegatecall{gas: GAS_LIMIT}( + abi.encodeWithSelector(this._afterRemoveLiquidity.selector, sender, key, params, delta, hookData) + ); + if (success) { + return (BaseHook.afterRemoveLiquidity.selector, abi.decode(returnData, (BalanceDelta))); + } else { + return (BaseHook.afterRemoveLiquidity.selector, BalanceDeltaLibrary.ZERO_DELTA); + } + } + + /// @notice Middleware function that reverts if the implementation modified deltas incorrectly + /// @param sender The same sender from afterRemoveLiquidity + /// @param key The same key from afterRemoveLiquidity + /// @param params The same params from afterRemoveLiquidity + /// @param delta The same delta from afterRemoveLiquidity + /// @param hookData The same hookData from afterRemoveLiquidity + function _afterRemoveLiquidity( + address sender, + PoolKey calldata key, + IPoolManager.ModifyLiquidityParams calldata params, + BalanceDelta delta, + bytes calldata hookData + ) external returns (BalanceDelta) { + (bool success, bytes memory returnData) = address(implementation).delegatecall( + abi.encodeWithSelector(this.afterRemoveLiquidity.selector, sender, key, params, delta, hookData) + ); + if (!success) { + revert FailedImplementationCall(); + } + (bytes4 selector, BalanceDelta returnDelta) = abi.decode(returnData, (bytes4, BalanceDelta)); + if (selector != BaseHook.afterRemoveLiquidity.selector) { + revert Hooks.InvalidHookResponse(); + } + uint256 unaccountedNonzeroDeltas = poolManager.getNonzeroDeltaCount(); + if (unaccountedNonzeroDeltas == 0 && returnDelta == BalanceDeltaLibrary.ZERO_DELTA) { + return returnDelta; + } + (int256 fee0, int256 fee1) = HookFees.calculateFeesFrom(delta, maxFeeBips); + if (returnDelta.amount0() > fee0 || returnDelta.amount1() > fee1) { + revert TookTooMuchFee(); + } + returnDelta - delta; // revert on overflow + + unaccountedNonzeroDeltas = + validateAndCountDeltas(key.currency0, returnDelta.amount0(), unaccountedNonzeroDeltas); + unaccountedNonzeroDeltas = + validateAndCountDeltas(key.currency1, returnDelta.amount1(), unaccountedNonzeroDeltas); + + if (unaccountedNonzeroDeltas == 0) { + return returnDelta; + } + + // if the hook settled the caller's deltas + if (poolManager.currencyDelta(sender, key.currency0) != 0) { + unchecked { + unaccountedNonzeroDeltas--; + } + } + if (poolManager.currencyDelta(sender, key.currency1) != 0) { + unchecked { + unaccountedNonzeroDeltas--; + } + } + if (unaccountedNonzeroDeltas == 0) { + return returnDelta; + } + revert InvalidDeltasOwner(); + } + + function validateAndCountDeltas(Currency currency, int128 returnAmount, uint256 unaccountedNonzeroDeltas) + internal + view + returns (uint256) + { + int256 hookDelta = poolManager.currencyDelta(address(this), currency); + unchecked { + // unchecked negation is safe because even if hookDelta is int256.min, returnAmount can not be int256.min + if (-hookDelta != returnAmount) { + revert DeltasReturnMismatch(); + } + if (hookDelta != 0) { + unaccountedNonzeroDeltas--; + } + } + return unaccountedNonzeroDeltas; + } + + function _ensureValidFlags() internal view virtual override { + if (!IHooks(address(this)).hasPermission(Hooks.AFTER_REMOVE_LIQUIDITY_RETURNS_DELTA_FLAG)) { + HookPermissionForbidden.selector.revertWith(address(this)); + } + } +} diff --git a/src/middleware/MiddlewareRemoveFactory.sol b/src/middleware/MiddlewareRemoveFactory.sol new file mode 100644 index 00000000..0b6c8346 --- /dev/null +++ b/src/middleware/MiddlewareRemoveFactory.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {MiddlewareRemove} from "./MiddlewareRemove.sol"; +import {MiddlewareRemoveNoDeltas} from "./MiddlewareRemoveNoDeltas.sol"; + +contract MiddlewareRemoveFactory { + event MiddlewareCreated(address implementation, address middleware, uint256 maxFeeBips); + + mapping(address => address) private _implementations; + mapping(address => uint256) private _maxFeeBips; + + IPoolManager public immutable poolManager; + + constructor(IPoolManager _poolManager) { + poolManager = _poolManager; + } + + /** + * @notice Get the implementation address for a given middleware. + * @param middleware The address of the middleware. + * @return implementation The address of the implementation. + */ + function getImplementation(address middleware) external view returns (address implementation) { + return _implementations[middleware]; + } + + /** + * @notice Get the max fee in basis points for a given middleware. + * @param middleware The address of the middleware. + * @return maxFeeBips The maximum fee in basis points the hook is allowed to charge on removeLiquidity. + */ + function getMaxFeeBips(address middleware) external view returns (uint256 maxFeeBips) { + return _maxFeeBips[middleware]; + } + + /** + * @notice Create a new middlewareRemove contract. + * @param implementation The address of the implementation or an existing hook. + * @param maxFeeBips The maximum fee in basis points the hook is allowed to charge on removeLiquidity. + * @param salt The salt for deploying to the right flags. + * @return middleware The address of the newly created middlewareRemove contract. + */ + function createMiddleware(address implementation, uint256 maxFeeBips, bytes32 salt) + external + returns (address middleware) + { + if (maxFeeBips == 0) { + middleware = address(new MiddlewareRemoveNoDeltas{salt: salt}(poolManager, implementation)); + } else { + middleware = address(new MiddlewareRemove{salt: salt}(poolManager, implementation, maxFeeBips)); + } + _implementations[middleware] = implementation; + _maxFeeBips[middleware] = maxFeeBips; + emit MiddlewareCreated(implementation, middleware, maxFeeBips); + } +} diff --git a/src/middleware/MiddlewareRemoveNoDeltas.sol b/src/middleware/MiddlewareRemoveNoDeltas.sol new file mode 100644 index 00000000..12f74bfa --- /dev/null +++ b/src/middleware/MiddlewareRemoveNoDeltas.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {BalanceDelta, BalanceDeltaLibrary} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {Proxy} from "@openzeppelin/contracts/proxy/Proxy.sol"; +import {BaseRemove} from "./BaseRemove.sol"; +import {BaseHook} from "../../src/base/hooks/BaseHook.sol"; +import {BalanceDeltaLibrary} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; +import {CustomRevert} from "@uniswap/v4-core/src/libraries/CustomRevert.sol"; +import {TransientStateLibrary} from "@uniswap/v4-core/src/libraries/TransientStateLibrary.sol"; + +contract MiddlewareRemoveNoDeltas is BaseRemove { + using CustomRevert for bytes4; + using Hooks for IHooks; + using TransientStateLibrary for IPoolManager; + + /// @param _poolManager The address of the pool manager + /// @param _impl The address of the implementation contract + constructor(IPoolManager _poolManager, address _impl) BaseRemove(_poolManager, _impl) {} + + /// @notice The hook called after liquidity is removed. Ensures zero nonzeroDeltas + /// @inheritdoc BaseRemove + function afterRemoveLiquidity( + address sender, + PoolKey calldata key, + IPoolManager.ModifyLiquidityParams calldata params, + BalanceDelta delta, + bytes calldata hookData + ) external override returns (bytes4, BalanceDelta) { + if (bytes32(hookData) == OVERRIDE_BYTES) { + (bool success, bytes memory returnData) = address(implementation).delegatecall( + abi.encodeWithSelector(this.afterRemoveLiquidity.selector, sender, key, params, delta, hookData[32:]) + ); + if (!success) { + revert FailedImplementationCall(); + } + return abi.decode(returnData, (bytes4, BalanceDelta)); + } + address(this).delegatecall{gas: GAS_LIMIT}( + abi.encodeWithSelector(this._afterRemoveLiquidity.selector, msg.data) + ); + return (BaseHook.afterRemoveLiquidity.selector, BalanceDeltaLibrary.ZERO_DELTA); + } + + /// @notice Middleware function that reverts if the implementation modified deltas + /// @param data The calldata from afterRemoveLiquidity + function _afterRemoveLiquidity(bytes calldata data) external { + (bool success, bytes memory returnData) = address(implementation).delegatecall(data); + if (!success) { + revert FailedImplementationCall(); + } + (bytes4 selector, BalanceDelta returnDelta) = abi.decode(returnData, (bytes4, BalanceDelta)); + if (selector != BaseHook.afterRemoveLiquidity.selector) { + revert Hooks.InvalidHookResponse(); + } + if (poolManager.getNonzeroDeltaCount() != 0 || returnDelta != BalanceDeltaLibrary.ZERO_DELTA) { + revert ImplementationModifiedDeltas(); + } + } + + function _ensureValidFlags() internal view virtual override { + if (IHooks(address(this)).hasPermission(Hooks.AFTER_REMOVE_LIQUIDITY_RETURNS_DELTA_FLAG)) { + HookPermissionForbidden.selector.revertWith(address(this)); + } + } +} diff --git a/test/BaseActionsRouter.t.sol b/test/BaseActionsRouter.t.sol new file mode 100644 index 00000000..fd7b3c14 --- /dev/null +++ b/test/BaseActionsRouter.t.sol @@ -0,0 +1,140 @@ +//SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import {MockBaseActionsRouter} from "./mocks/MockBaseActionsRouter.sol"; +import {Planner, Plan} from "./shared/Planner.sol"; +import {Actions} from "../src/libraries/Actions.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {Test} from "forge-std/Test.sol"; +import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; +import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; + +contract BaseActionsRouterTest is Test, Deployers, GasSnapshot { + using Planner for Plan; + + MockBaseActionsRouter router; + + function setUp() public { + deployFreshManager(); + router = new MockBaseActionsRouter(manager); + } + + function test_swap_suceeds() public { + Plan memory plan = Planner.init(); + for (uint256 i = 0; i < 10; i++) { + plan.add(Actions.SWAP_EXACT_IN, ""); + } + + bytes memory data = plan.encode(); + + assertEq(router.swapCount(), 0); + + router.executeActions(data); + snapLastCall("BaseActionsRouter_mock10commands"); + assertEq(router.swapCount(), 10); + } + + function test_increaseLiquidity_suceeds() public { + Plan memory plan = Planner.init(); + for (uint256 i = 0; i < 10; i++) { + plan.add(Actions.INCREASE_LIQUIDITY, ""); + } + + assertEq(router.increaseLiqCount(), 0); + + bytes memory data = plan.encode(); + router.executeActions(data); + assertEq(router.increaseLiqCount(), 10); + } + + function test_decreaseLiquidity_suceeds() public { + Plan memory plan = Planner.init(); + for (uint256 i = 0; i < 10; i++) { + plan.add(Actions.DECREASE_LIQUIDITY, ""); + } + + assertEq(router.decreaseLiqCount(), 0); + + bytes memory data = plan.encode(); + router.executeActions(data); + assertEq(router.decreaseLiqCount(), 10); + } + + function test_donate_suceeds() public { + Plan memory plan = Planner.init(); + for (uint256 i = 0; i < 10; i++) { + plan.add(Actions.DONATE, ""); + } + + assertEq(router.donateCount(), 0); + + bytes memory data = plan.encode(); + router.executeActions(data); + assertEq(router.donateCount(), 10); + } + + function test_clear_suceeds() public { + Plan memory plan = Planner.init(); + for (uint256 i = 0; i < 10; i++) { + plan.add(Actions.CLEAR, ""); + } + + assertEq(router.clearCount(), 0); + + bytes memory data = plan.encode(); + router.executeActions(data); + assertEq(router.clearCount(), 10); + } + + function test_settle_suceeds() public { + Plan memory plan = Planner.init(); + for (uint256 i = 0; i < 10; i++) { + plan.add(Actions.SETTLE, ""); + } + + assertEq(router.settleCount(), 0); + + bytes memory data = plan.encode(); + router.executeActions(data); + assertEq(router.settleCount(), 10); + } + + function test_take_suceeds() public { + Plan memory plan = Planner.init(); + for (uint256 i = 0; i < 10; i++) { + plan.add(Actions.TAKE, ""); + } + + assertEq(router.takeCount(), 0); + + bytes memory data = plan.encode(); + router.executeActions(data); + assertEq(router.takeCount(), 10); + } + + function test_mint_suceeds() public { + Plan memory plan = Planner.init(); + for (uint256 i = 0; i < 10; i++) { + plan.add(Actions.MINT_6909, ""); + } + + assertEq(router.mintCount(), 0); + + bytes memory data = plan.encode(); + router.executeActions(data); + assertEq(router.mintCount(), 10); + } + + function test_burn_suceeds() public { + Plan memory plan = Planner.init(); + for (uint256 i = 0; i < 10; i++) { + plan.add(Actions.BURN_6909, ""); + } + + assertEq(router.burnCount(), 0); + + bytes memory data = plan.encode(); + router.executeActions(data); + assertEq(router.burnCount(), 10); + } +} diff --git a/test/BaseMiddlewareFactory.t.sol b/test/BaseMiddlewareFactory.t.sol index 885e4e1f..66e1b18b 100644 --- a/test/BaseMiddlewareFactory.t.sol +++ b/test/BaseMiddlewareFactory.t.sol @@ -11,13 +11,13 @@ import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {HookEnabledSwapRouter} from "./utils/HookEnabledSwapRouter.sol"; import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; -import {console} from "../../../lib/forge-std/src/console.sol"; -import {BaseMiddleware} from "./../contracts/middleware/BaseMiddleware.sol"; +import {console} from "../../../lib/v4-core/lib/forge-std/src/console.sol"; +import {BaseMiddleware} from "./../src/middleware/BaseMiddleware.sol"; import {BaseMiddlewareImplementation} from "./middleware/BaseMiddlewareImplemenation.sol"; import {BaseMiddlewareFactoryImplementation} from "./middleware/BaseMiddlewareFactoryImplementation.sol"; import {HookMiner} from "./utils/HookMiner.sol"; import {HooksCounter} from "./middleware/HooksCounter.sol"; -import {SafeCallback} from "./../contracts/base/SafeCallback.sol"; +import {SafeCallback} from "./../src/base/SafeCallback.sol"; contract BaseMiddlewareFactoryTest is Test, Deployers { HookEnabledSwapRouter router; @@ -72,32 +72,8 @@ contract BaseMiddlewareFactoryTest is Test, Deployers { factory.createMiddleware(address(hookscounter), salt); } - function testRevertOnIncorrectFlags() public { - HooksCounter hookscounter2 = HooksCounter(address(HOOKSCOUNTER_FLAGS)); - vm.etch(address(hookscounter), address(new HooksCounter(manager)).code); - uint160 incorrectFlags = uint160(Hooks.BEFORE_INITIALIZE_FLAG); - - (address hookAddress, bytes32 salt) = HookMiner.find( - address(factory), - incorrectFlags, - type(BaseMiddlewareImplementation).creationCode, - abi.encode(address(manager), address(hookscounter2)) - ); - address implementation = address(hookscounter2); - vm.expectRevert(BaseMiddleware.FlagsMismatch.selector); - factory.createMiddleware(implementation, salt); - } - - function testRevertOnIncorrectFlagsMined() public { - HooksCounter hookscounter2 = HooksCounter(address(HOOKSCOUNTER_FLAGS)); - vm.etch(address(hookscounter), address(new HooksCounter(manager)).code); - address implementation = address(hookscounter2); - vm.expectRevert(BaseMiddleware.FlagsMismatch.selector); - factory.createMiddleware(implementation, bytes32("who needs to mine a salt?")); - } - function testRevertOnIncorrectCaller() public { - vm.expectRevert(SafeCallback.NotManager.selector); + vm.expectRevert(SafeCallback.NotPoolManager.selector); hookscounter.afterDonate(address(this), key, 0, 0, ZERO_BYTES); } diff --git a/test/DeltaResolver.t.sol b/test/DeltaResolver.t.sol new file mode 100644 index 00000000..0e791e73 --- /dev/null +++ b/test/DeltaResolver.t.sol @@ -0,0 +1,43 @@ +//SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import {Test} from "forge-std/Test.sol"; +import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; +import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; +import {ERC20} from "solmate/src/tokens/ERC20.sol"; + +import {MockDeltaResolver} from "./mocks/MockDeltaResolver.sol"; + +contract DeltaResolverTest is Test, Deployers, GasSnapshot { + using CurrencyLibrary for Currency; + + MockDeltaResolver resolver; + + function setUp() public { + initializeManagerRoutersAndPoolsWithLiq(IHooks(address(0))); + resolver = new MockDeltaResolver(manager); + } + + function test_settle_native_succeeds(uint256 amount) public { + amount = bound(amount, 1, address(manager).balance); + + resolver.executeTest(CurrencyLibrary.NATIVE, amount); + + // check `pay` was not called + assertEq(resolver.payCallCount(), 0); + } + + function test_settle_token_succeeds(uint256 amount) public { + amount = bound(amount, 1, currency0.balanceOf(address(manager))); + + // the tokens will be taken to this contract, so an approval is needed for the settle + ERC20(Currency.unwrap(currency0)).approve(address(resolver), type(uint256).max); + + resolver.executeTest(currency0, amount); + + // check `pay` was called + assertEq(resolver.payCallCount(), 1); + } +} diff --git a/test/FullRange.t.sol b/test/FullRange.t.sol deleted file mode 100644 index 5edec106..00000000 --- a/test/FullRange.t.sol +++ /dev/null @@ -1,772 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; - -import {Test} from "forge-std/Test.sol"; -import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; -import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; -import {FullRange} from "../contracts/hooks/examples/FullRange.sol"; -import {FullRangeImplementation} from "./shared/implementation/FullRangeImplementation.sol"; -import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; -import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; -import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol"; -import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; -import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; -import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; -import {PoolModifyLiquidityTest} from "@uniswap/v4-core/src/test/PoolModifyLiquidityTest.sol"; -import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol"; -import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; -import {UniswapV4ERC20} from "../contracts/libraries/UniswapV4ERC20.sol"; -import {FullMath} from "@uniswap/v4-core/src/libraries/FullMath.sol"; -import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; -import {HookEnabledSwapRouter} from "./utils/HookEnabledSwapRouter.sol"; -import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; - -contract TestFullRange is Test, Deployers, GasSnapshot { - using PoolIdLibrary for PoolKey; - using SafeCast for uint256; - using CurrencyLibrary for Currency; - using StateLibrary for IPoolManager; - - event Initialize( - PoolId poolId, - Currency indexed currency0, - Currency indexed currency1, - uint24 fee, - int24 tickSpacing, - IHooks hooks - ); - event ModifyPosition( - PoolId indexed poolId, address indexed sender, int24 tickLower, int24 tickUpper, int256 liquidityDelta - ); - event Swap( - PoolId indexed id, - address sender, - int128 amount0, - int128 amount1, - uint160 sqrtPriceX96, - uint128 liquidity, - int24 tick, - uint24 fee - ); - - HookEnabledSwapRouter router; - /// @dev Min tick for full range with tick spacing of 60 - int24 internal constant MIN_TICK = -887220; - /// @dev Max tick for full range with tick spacing of 60 - int24 internal constant MAX_TICK = -MIN_TICK; - - int24 constant TICK_SPACING = 60; - uint16 constant LOCKED_LIQUIDITY = 1000; - uint256 constant MAX_DEADLINE = 12329839823; - uint256 constant MAX_TICK_LIQUIDITY = 11505069308564788430434325881101412; - uint8 constant DUST = 30; - - MockERC20 token0; - MockERC20 token1; - MockERC20 token2; - - FullRangeImplementation fullRange = FullRangeImplementation( - address(uint160(Hooks.BEFORE_INITIALIZE_FLAG | Hooks.BEFORE_ADD_LIQUIDITY_FLAG | Hooks.BEFORE_SWAP_FLAG)) - ); - - PoolId id; - - PoolKey key2; - PoolId id2; - - // For a pool that gets initialized with liquidity in setUp() - PoolKey keyWithLiq; - PoolId idWithLiq; - - function setUp() public { - deployFreshManagerAndRouters(); - router = new HookEnabledSwapRouter(manager); - MockERC20[] memory tokens = deployTokens(3, 2 ** 128); - token0 = tokens[0]; - token1 = tokens[1]; - token2 = tokens[2]; - - FullRangeImplementation impl = new FullRangeImplementation(manager, fullRange); - vm.etch(address(fullRange), address(impl).code); - - key = createPoolKey(token0, token1); - id = key.toId(); - - key2 = createPoolKey(token1, token2); - id2 = key.toId(); - - keyWithLiq = createPoolKey(token0, token2); - idWithLiq = keyWithLiq.toId(); - - token0.approve(address(fullRange), type(uint256).max); - token1.approve(address(fullRange), type(uint256).max); - token2.approve(address(fullRange), type(uint256).max); - token0.approve(address(router), type(uint256).max); - token1.approve(address(router), type(uint256).max); - token2.approve(address(router), type(uint256).max); - - initPool(keyWithLiq.currency0, keyWithLiq.currency1, fullRange, 3000, SQRT_PRICE_1_1, ZERO_BYTES); - fullRange.addLiquidity( - FullRange.AddLiquidityParams( - keyWithLiq.currency0, - keyWithLiq.currency1, - 3000, - 100 ether, - 100 ether, - 99 ether, - 99 ether, - address(this), - MAX_DEADLINE - ) - ); - } - - function testFullRange_beforeInitialize_AllowsPoolCreation() public { - PoolKey memory testKey = key; - - vm.expectEmit(true, true, true, true); - emit Initialize(id, testKey.currency0, testKey.currency1, testKey.fee, testKey.tickSpacing, testKey.hooks); - - snapStart("FullRangeInitialize"); - manager.initialize(testKey, SQRT_PRICE_1_1, ZERO_BYTES); - snapEnd(); - - (, address liquidityToken) = fullRange.poolInfo(id); - - assertFalse(liquidityToken == address(0)); - } - - function testFullRange_beforeInitialize_RevertsIfWrongSpacing() public { - PoolKey memory wrongKey = PoolKey(key.currency0, key.currency1, 0, TICK_SPACING + 1, fullRange); - - vm.expectRevert(FullRange.TickSpacingNotDefault.selector); - manager.initialize(wrongKey, SQRT_PRICE_1_1, ZERO_BYTES); - } - - function testFullRange_addLiquidity_InitialAddSucceeds() public { - manager.initialize(key, SQRT_PRICE_1_1, ZERO_BYTES); - - uint256 prevBalance0 = key.currency0.balanceOf(address(this)); - uint256 prevBalance1 = key.currency1.balanceOf(address(this)); - - FullRange.AddLiquidityParams memory addLiquidityParams = FullRange.AddLiquidityParams( - key.currency0, key.currency1, 3000, 10 ether, 10 ether, 9 ether, 9 ether, address(this), MAX_DEADLINE - ); - - snapStart("FullRangeAddInitialLiquidity"); - fullRange.addLiquidity(addLiquidityParams); - snapEnd(); - - (bool hasAccruedFees, address liquidityToken) = fullRange.poolInfo(id); - uint256 liquidityTokenBal = UniswapV4ERC20(liquidityToken).balanceOf(address(this)); - - assertEq(manager.getLiquidity(id), liquidityTokenBal + LOCKED_LIQUIDITY); - - assertEq(key.currency0.balanceOf(address(this)), prevBalance0 - 10 ether); - assertEq(key.currency1.balanceOf(address(this)), prevBalance1 - 10 ether); - - assertEq(liquidityTokenBal, 10 ether - LOCKED_LIQUIDITY); - assertEq(hasAccruedFees, false); - } - - function testFullRange_addLiquidity_InitialAddFuzz(uint256 amount) public { - manager.initialize(key, SQRT_PRICE_1_1, ZERO_BYTES); - if (amount <= LOCKED_LIQUIDITY) { - vm.expectRevert(FullRange.LiquidityDoesntMeetMinimum.selector); - fullRange.addLiquidity( - FullRange.AddLiquidityParams( - key.currency0, key.currency1, 3000, amount, amount, amount, amount, address(this), MAX_DEADLINE - ) - ); - } else if (amount > MAX_TICK_LIQUIDITY) { - vm.expectRevert(); - fullRange.addLiquidity( - FullRange.AddLiquidityParams( - key.currency0, key.currency1, 3000, amount, amount, amount, amount, address(this), MAX_DEADLINE - ) - ); - } else { - fullRange.addLiquidity( - FullRange.AddLiquidityParams( - key.currency0, key.currency1, 3000, amount, amount, 0, 0, address(this), MAX_DEADLINE - ) - ); - - (bool hasAccruedFees, address liquidityToken) = fullRange.poolInfo(id); - uint256 liquidityTokenBal = UniswapV4ERC20(liquidityToken).balanceOf(address(this)); - - assertEq(manager.getLiquidity(id), liquidityTokenBal + LOCKED_LIQUIDITY); - assertEq(hasAccruedFees, false); - } - } - - function testFullRange_addLiquidity_SubsequentAdd() public { - uint256 prevBalance0 = keyWithLiq.currency0.balanceOfSelf(); - uint256 prevBalance1 = keyWithLiq.currency1.balanceOfSelf(); - - (, address liquidityToken) = fullRange.poolInfo(idWithLiq); - uint256 prevLiquidityTokenBal = UniswapV4ERC20(liquidityToken).balanceOf(address(this)); - - FullRange.AddLiquidityParams memory addLiquidityParams = FullRange.AddLiquidityParams( - keyWithLiq.currency0, - keyWithLiq.currency1, - 3000, - 10 ether, - 10 ether, - 9 ether, - 9 ether, - address(this), - MAX_DEADLINE - ); - - snapStart("FullRangeAddLiquidity"); - fullRange.addLiquidity(addLiquidityParams); - snapEnd(); - - (bool hasAccruedFees,) = fullRange.poolInfo(idWithLiq); - uint256 liquidityTokenBal = UniswapV4ERC20(liquidityToken).balanceOf(address(this)); - - assertEq(manager.getLiquidity(idWithLiq), liquidityTokenBal + LOCKED_LIQUIDITY); - - assertEq(keyWithLiq.currency0.balanceOfSelf(), prevBalance0 - 10 ether); - assertEq(keyWithLiq.currency1.balanceOfSelf(), prevBalance1 - 10 ether); - - assertEq(liquidityTokenBal, prevLiquidityTokenBal + 10 ether); - assertEq(hasAccruedFees, false); - } - - function testFullRange_addLiquidity_FailsIfNoPool() public { - vm.expectRevert(FullRange.PoolNotInitialized.selector); - fullRange.addLiquidity( - FullRange.AddLiquidityParams( - key.currency0, key.currency1, 0, 10 ether, 10 ether, 9 ether, 9 ether, address(this), MAX_DEADLINE - ) - ); - } - - function testFullRange_addLiquidity_SwapThenAddSucceeds() public { - manager.initialize(key, SQRT_PRICE_1_1, ZERO_BYTES); - - uint256 prevBalance0 = key.currency0.balanceOf(address(this)); - uint256 prevBalance1 = key.currency1.balanceOf(address(this)); - (, address liquidityToken) = fullRange.poolInfo(id); - - fullRange.addLiquidity( - FullRange.AddLiquidityParams( - key.currency0, key.currency1, 3000, 10 ether, 10 ether, 9 ether, 9 ether, address(this), MAX_DEADLINE - ) - ); - - uint256 liquidityTokenBal = UniswapV4ERC20(liquidityToken).balanceOf(address(this)); - - assertEq(manager.getLiquidity(id), liquidityTokenBal + LOCKED_LIQUIDITY); - assertEq(liquidityTokenBal, 10 ether - LOCKED_LIQUIDITY); - assertEq(key.currency0.balanceOf(address(this)), prevBalance0 - 10 ether); - assertEq(key.currency1.balanceOf(address(this)), prevBalance1 - 10 ether); - - vm.expectEmit(true, true, true, true); - emit Swap( - id, address(router), -1 ether, 906610893880149131, 72045250990510446115798809072, 10 ether, -1901, 3000 - ); - - IPoolManager.SwapParams memory params = - IPoolManager.SwapParams({zeroForOne: true, amountSpecified: -1 ether, sqrtPriceLimitX96: SQRT_PRICE_1_2}); - HookEnabledSwapRouter.TestSettings memory settings = - HookEnabledSwapRouter.TestSettings({takeClaims: false, settleUsingBurn: false}); - - snapStart("FullRangeSwap"); - router.swap(key, params, settings, ZERO_BYTES); - snapEnd(); - - (bool hasAccruedFees,) = fullRange.poolInfo(id); - - assertEq(key.currency0.balanceOf(address(this)), prevBalance0 - 10 ether - 1 ether); - assertEq(key.currency1.balanceOf(address(this)), prevBalance1 - 9093389106119850869); - assertEq(hasAccruedFees, true); - - fullRange.addLiquidity( - FullRange.AddLiquidityParams( - key.currency0, key.currency1, 3000, 5 ether, 5 ether, 4 ether, 4 ether, address(this), MAX_DEADLINE - ) - ); - - (hasAccruedFees,) = fullRange.poolInfo(id); - liquidityTokenBal = UniswapV4ERC20(liquidityToken).balanceOf(address(this)); - - assertEq(manager.getLiquidity(id), liquidityTokenBal + LOCKED_LIQUIDITY); - assertEq(liquidityTokenBal, 14546694553059925434 - LOCKED_LIQUIDITY); - assertEq(hasAccruedFees, true); - } - - function testFullRange_addLiquidity_FailsIfTooMuchSlippage() public { - manager.initialize(key, SQRT_PRICE_1_1, ZERO_BYTES); - - fullRange.addLiquidity( - FullRange.AddLiquidityParams( - key.currency0, key.currency1, 3000, 10 ether, 10 ether, 10 ether, 10 ether, address(this), MAX_DEADLINE - ) - ); - - IPoolManager.SwapParams memory params = - IPoolManager.SwapParams({zeroForOne: true, amountSpecified: 1000 ether, sqrtPriceLimitX96: SQRT_PRICE_1_2}); - HookEnabledSwapRouter.TestSettings memory settings = - HookEnabledSwapRouter.TestSettings({takeClaims: false, settleUsingBurn: false}); - - router.swap(key, params, settings, ZERO_BYTES); - - vm.expectRevert(FullRange.TooMuchSlippage.selector); - fullRange.addLiquidity( - FullRange.AddLiquidityParams( - key.currency0, key.currency1, 3000, 10 ether, 10 ether, 10 ether, 10 ether, address(this), MAX_DEADLINE - ) - ); - } - - function testFullRange_swap_TwoSwaps() public { - PoolKey memory testKey = key; - manager.initialize(testKey, SQRT_PRICE_1_1, ZERO_BYTES); - - fullRange.addLiquidity( - FullRange.AddLiquidityParams( - key.currency0, key.currency1, 3000, 10 ether, 10 ether, 9 ether, 9 ether, address(this), MAX_DEADLINE - ) - ); - - IPoolManager.SwapParams memory params = - IPoolManager.SwapParams({zeroForOne: true, amountSpecified: 1 ether, sqrtPriceLimitX96: SQRT_PRICE_1_2}); - HookEnabledSwapRouter.TestSettings memory settings = - HookEnabledSwapRouter.TestSettings({takeClaims: false, settleUsingBurn: false}); - - snapStart("FullRangeFirstSwap"); - router.swap(testKey, params, settings, ZERO_BYTES); - snapEnd(); - - (bool hasAccruedFees,) = fullRange.poolInfo(id); - assertEq(hasAccruedFees, true); - - snapStart("FullRangeSecondSwap"); - router.swap(testKey, params, settings, ZERO_BYTES); - snapEnd(); - - (hasAccruedFees,) = fullRange.poolInfo(id); - assertEq(hasAccruedFees, true); - } - - function testFullRange_swap_TwoPools() public { - manager.initialize(key, SQRT_PRICE_1_1, ZERO_BYTES); - manager.initialize(key2, SQRT_PRICE_1_1, ZERO_BYTES); - - fullRange.addLiquidity( - FullRange.AddLiquidityParams( - key.currency0, key.currency1, 3000, 10 ether, 10 ether, 9 ether, 9 ether, address(this), MAX_DEADLINE - ) - ); - fullRange.addLiquidity( - FullRange.AddLiquidityParams( - key2.currency0, key2.currency1, 3000, 10 ether, 10 ether, 9 ether, 9 ether, address(this), MAX_DEADLINE - ) - ); - - IPoolManager.SwapParams memory params = - IPoolManager.SwapParams({zeroForOne: true, amountSpecified: 10000000, sqrtPriceLimitX96: SQRT_PRICE_1_2}); - - HookEnabledSwapRouter.TestSettings memory testSettings = - HookEnabledSwapRouter.TestSettings({takeClaims: false, settleUsingBurn: false}); - - router.swap(key, params, testSettings, ZERO_BYTES); - router.swap(key2, params, testSettings, ZERO_BYTES); - - (bool hasAccruedFees,) = fullRange.poolInfo(id); - assertEq(hasAccruedFees, true); - - (hasAccruedFees,) = fullRange.poolInfo(id2); - assertEq(hasAccruedFees, true); - } - - function testFullRange_removeLiquidity_InitialRemoveSucceeds() public { - uint256 prevBalance0 = keyWithLiq.currency0.balanceOfSelf(); - uint256 prevBalance1 = keyWithLiq.currency1.balanceOfSelf(); - - (, address liquidityToken) = fullRange.poolInfo(idWithLiq); - - UniswapV4ERC20(liquidityToken).approve(address(fullRange), type(uint256).max); - - FullRange.RemoveLiquidityParams memory removeLiquidityParams = - FullRange.RemoveLiquidityParams(keyWithLiq.currency0, keyWithLiq.currency1, 3000, 1 ether, MAX_DEADLINE); - - snapStart("FullRangeRemoveLiquidity"); - fullRange.removeLiquidity(removeLiquidityParams); - snapEnd(); - - (bool hasAccruedFees,) = fullRange.poolInfo(idWithLiq); - uint256 liquidityTokenBal = UniswapV4ERC20(liquidityToken).balanceOf(address(this)); - - assertEq(manager.getLiquidity(idWithLiq), liquidityTokenBal + LOCKED_LIQUIDITY); - assertEq(UniswapV4ERC20(liquidityToken).balanceOf(address(this)), 99 ether - LOCKED_LIQUIDITY + 5); - assertEq(keyWithLiq.currency0.balanceOfSelf(), prevBalance0 + 1 ether - 1); - assertEq(keyWithLiq.currency1.balanceOfSelf(), prevBalance1 + 1 ether - 1); - assertEq(hasAccruedFees, false); - } - - function testFullRange_removeLiquidity_InitialRemoveFuzz(uint256 amount) public { - manager.initialize(key, SQRT_PRICE_1_1, ZERO_BYTES); - - fullRange.addLiquidity( - FullRange.AddLiquidityParams( - key.currency0, - key.currency1, - 3000, - 1000 ether, - 1000 ether, - 999 ether, - 999 ether, - address(this), - MAX_DEADLINE - ) - ); - - (, address liquidityToken) = fullRange.poolInfo(id); - - UniswapV4ERC20(liquidityToken).approve(address(fullRange), type(uint256).max); - - if (amount > UniswapV4ERC20(liquidityToken).balanceOf(address(this))) { - vm.expectRevert(); - fullRange.removeLiquidity( - FullRange.RemoveLiquidityParams(key.currency0, key.currency1, 3000, amount, MAX_DEADLINE) - ); - } else { - uint256 prevLiquidityTokenBal = UniswapV4ERC20(liquidityToken).balanceOf(address(this)); - fullRange.removeLiquidity( - FullRange.RemoveLiquidityParams(key.currency0, key.currency1, 3000, amount, MAX_DEADLINE) - ); - - uint256 liquidityTokenBal = UniswapV4ERC20(liquidityToken).balanceOf(address(this)); - (bool hasAccruedFees,) = fullRange.poolInfo(id); - - assertEq(prevLiquidityTokenBal - liquidityTokenBal, amount); - assertEq(manager.getLiquidity(id), liquidityTokenBal + LOCKED_LIQUIDITY); - assertEq(hasAccruedFees, false); - } - } - - function testFullRange_removeLiquidity_FailsIfNoPool() public { - vm.expectRevert(FullRange.PoolNotInitialized.selector); - fullRange.removeLiquidity( - FullRange.RemoveLiquidityParams(key.currency0, key.currency1, 0, 10 ether, MAX_DEADLINE) - ); - } - - function testFullRange_removeLiquidity_FailsIfNoLiquidity() public { - manager.initialize(key, SQRT_PRICE_1_1, ZERO_BYTES); - - (, address liquidityToken) = fullRange.poolInfo(id); - UniswapV4ERC20(liquidityToken).approve(address(fullRange), type(uint256).max); - - vm.expectRevert(); // Insufficient balance error from ERC20 contract - fullRange.removeLiquidity( - FullRange.RemoveLiquidityParams(key.currency0, key.currency1, 3000, 10 ether, MAX_DEADLINE) - ); - } - - function testFullRange_removeLiquidity_SucceedsWithPartial() public { - manager.initialize(key, SQRT_PRICE_1_1, ZERO_BYTES); - - uint256 prevBalance0 = key.currency0.balanceOfSelf(); - uint256 prevBalance1 = key.currency1.balanceOfSelf(); - - fullRange.addLiquidity( - FullRange.AddLiquidityParams( - key.currency0, key.currency1, 3000, 10 ether, 10 ether, 9 ether, 9 ether, address(this), MAX_DEADLINE - ) - ); - - (, address liquidityToken) = fullRange.poolInfo(id); - - assertEq(UniswapV4ERC20(liquidityToken).balanceOf(address(this)), 10 ether - LOCKED_LIQUIDITY); - - assertEq(key.currency0.balanceOfSelf(), prevBalance0 - 10 ether); - assertEq(key.currency1.balanceOfSelf(), prevBalance1 - 10 ether); - - UniswapV4ERC20(liquidityToken).approve(address(fullRange), type(uint256).max); - - fullRange.removeLiquidity( - FullRange.RemoveLiquidityParams(key.currency0, key.currency1, 3000, 5 ether, MAX_DEADLINE) - ); - - (bool hasAccruedFees,) = fullRange.poolInfo(id); - uint256 liquidityTokenBal = UniswapV4ERC20(liquidityToken).balanceOf(address(this)); - - assertEq(manager.getLiquidity(id), liquidityTokenBal + LOCKED_LIQUIDITY); - assertEq(liquidityTokenBal, 5 ether - LOCKED_LIQUIDITY); - assertEq(key.currency0.balanceOfSelf(), prevBalance0 - 5 ether - 1); - assertEq(key.currency1.balanceOfSelf(), prevBalance1 - 5 ether - 1); - assertEq(hasAccruedFees, false); - } - - function testFullRange_removeLiquidity_DiffRatios() public { - manager.initialize(key, SQRT_PRICE_1_1, ZERO_BYTES); - - uint256 prevBalance0 = key.currency0.balanceOf(address(this)); - uint256 prevBalance1 = key.currency1.balanceOf(address(this)); - - fullRange.addLiquidity( - FullRange.AddLiquidityParams( - key.currency0, key.currency1, 3000, 10 ether, 10 ether, 9 ether, 9 ether, address(this), MAX_DEADLINE - ) - ); - - assertEq(key.currency0.balanceOf(address(this)), prevBalance0 - 10 ether); - assertEq(key.currency1.balanceOf(address(this)), prevBalance1 - 10 ether); - - (, address liquidityToken) = fullRange.poolInfo(id); - - assertEq(UniswapV4ERC20(liquidityToken).balanceOf(address(this)), 10 ether - LOCKED_LIQUIDITY); - - fullRange.addLiquidity( - FullRange.AddLiquidityParams( - key.currency0, key.currency1, 3000, 5 ether, 2.5 ether, 2 ether, 2 ether, address(this), MAX_DEADLINE - ) - ); - - assertEq(key.currency0.balanceOf(address(this)), prevBalance0 - 12.5 ether); - assertEq(key.currency1.balanceOf(address(this)), prevBalance1 - 12.5 ether); - - assertEq(UniswapV4ERC20(liquidityToken).balanceOf(address(this)), 12.5 ether - LOCKED_LIQUIDITY); - - UniswapV4ERC20(liquidityToken).approve(address(fullRange), type(uint256).max); - - fullRange.removeLiquidity( - FullRange.RemoveLiquidityParams(key.currency0, key.currency1, 3000, 5 ether, MAX_DEADLINE) - ); - - uint256 liquidityTokenBal = UniswapV4ERC20(liquidityToken).balanceOf(address(this)); - - assertEq(manager.getLiquidity(id), liquidityTokenBal + LOCKED_LIQUIDITY); - assertEq(liquidityTokenBal, 7.5 ether - LOCKED_LIQUIDITY); - assertEq(key.currency0.balanceOf(address(this)), prevBalance0 - 7.5 ether - 1); - assertEq(key.currency1.balanceOf(address(this)), prevBalance1 - 7.5 ether - 1); - } - - function testFullRange_removeLiquidity_SwapAndRebalance() public { - (, address liquidityToken) = fullRange.poolInfo(idWithLiq); - - IPoolManager.SwapParams memory params = - IPoolManager.SwapParams({zeroForOne: true, amountSpecified: 1 ether, sqrtPriceLimitX96: SQRT_PRICE_1_2}); - - HookEnabledSwapRouter.TestSettings memory testSettings = - HookEnabledSwapRouter.TestSettings({takeClaims: false, settleUsingBurn: false}); - - router.swap(keyWithLiq, params, testSettings, ZERO_BYTES); - - UniswapV4ERC20(liquidityToken).approve(address(fullRange), type(uint256).max); - - FullRange.RemoveLiquidityParams memory removeLiquidityParams = - FullRange.RemoveLiquidityParams(keyWithLiq.currency0, keyWithLiq.currency1, 3000, 5 ether, MAX_DEADLINE); - - snapStart("FullRangeRemoveLiquidityAndRebalance"); - fullRange.removeLiquidity(removeLiquidityParams); - snapEnd(); - - (bool hasAccruedFees,) = fullRange.poolInfo(idWithLiq); - assertEq(hasAccruedFees, false); - } - - function testFullRange_removeLiquidity_RemoveAllFuzz(uint256 amount) public { - manager.initialize(key, SQRT_PRICE_1_1, ZERO_BYTES); - (, address liquidityToken) = fullRange.poolInfo(id); - - if (amount <= LOCKED_LIQUIDITY) { - vm.expectRevert(FullRange.LiquidityDoesntMeetMinimum.selector); - fullRange.addLiquidity( - FullRange.AddLiquidityParams( - key.currency0, key.currency1, 3000, amount, amount, amount, amount, address(this), MAX_DEADLINE - ) - ); - } else if (amount >= MAX_TICK_LIQUIDITY) { - vm.expectRevert(); - fullRange.addLiquidity( - FullRange.AddLiquidityParams( - key.currency0, key.currency1, 3000, amount, amount, amount, amount, address(this), MAX_DEADLINE - ) - ); - } else { - fullRange.addLiquidity( - FullRange.AddLiquidityParams( - key.currency0, key.currency1, 3000, amount, amount, 0, 0, address(this), MAX_DEADLINE - ) - ); - - // Test contract removes liquidity, succeeds - UniswapV4ERC20(liquidityToken).approve(address(fullRange), type(uint256).max); - - uint256 liquidityTokenBal = UniswapV4ERC20(liquidityToken).balanceOf(address(this)); - - fullRange.removeLiquidity( - FullRange.RemoveLiquidityParams(key.currency0, key.currency1, 3000, liquidityTokenBal, MAX_DEADLINE) - ); - - assertEq(manager.getLiquidity(id), LOCKED_LIQUIDITY); - } - } - - function testFullRange_removeLiquidity_ThreeLPsRemovePrincipalAndFees() public { - // Mint tokens for dummy addresses - token0.mint(address(1), 2 ** 128); - token1.mint(address(1), 2 ** 128); - token0.mint(address(2), 2 ** 128); - token1.mint(address(2), 2 ** 128); - - // Approve the hook - vm.prank(address(1)); - token0.approve(address(fullRange), type(uint256).max); - vm.prank(address(1)); - token1.approve(address(fullRange), type(uint256).max); - - vm.prank(address(2)); - token0.approve(address(fullRange), type(uint256).max); - vm.prank(address(2)); - token1.approve(address(fullRange), type(uint256).max); - - manager.initialize(key, SQRT_PRICE_1_1, ZERO_BYTES); - (, address liquidityToken) = fullRange.poolInfo(id); - - // Test contract adds liquidity - fullRange.addLiquidity( - FullRange.AddLiquidityParams( - key.currency0, - key.currency1, - 3000, - 100 ether, - 100 ether, - 99 ether, - 99 ether, - address(this), - MAX_DEADLINE - ) - ); - - // address(1) adds liquidity - vm.prank(address(1)); - fullRange.addLiquidity( - FullRange.AddLiquidityParams( - key.currency0, - key.currency1, - 3000, - 100 ether, - 100 ether, - 99 ether, - 99 ether, - address(this), - MAX_DEADLINE - ) - ); - - // address(2) adds liquidity - vm.prank(address(2)); - fullRange.addLiquidity( - FullRange.AddLiquidityParams( - key.currency0, - key.currency1, - 3000, - 100 ether, - 100 ether, - 99 ether, - 99 ether, - address(this), - MAX_DEADLINE - ) - ); - - IPoolManager.SwapParams memory params = - IPoolManager.SwapParams({zeroForOne: true, amountSpecified: 100 ether, sqrtPriceLimitX96: SQRT_PRICE_1_4}); - - HookEnabledSwapRouter.TestSettings memory testSettings = - HookEnabledSwapRouter.TestSettings({takeClaims: false, settleUsingBurn: false}); - - router.swap(key, params, testSettings, ZERO_BYTES); - - (bool hasAccruedFees,) = fullRange.poolInfo(id); - assertEq(hasAccruedFees, true); - - // Test contract removes liquidity, succeeds - UniswapV4ERC20(liquidityToken).approve(address(fullRange), type(uint256).max); - fullRange.removeLiquidity( - FullRange.RemoveLiquidityParams( - key.currency0, key.currency1, 3000, 300 ether - LOCKED_LIQUIDITY, MAX_DEADLINE - ) - ); - (hasAccruedFees,) = fullRange.poolInfo(id); - - // PoolManager does not have any liquidity left over - assertTrue(manager.getLiquidity(id) >= LOCKED_LIQUIDITY); - assertTrue(manager.getLiquidity(id) < LOCKED_LIQUIDITY + DUST); - - assertEq(hasAccruedFees, false); - } - - function testFullRange_removeLiquidity_SwapRemoveAllFuzz(uint256 amount) public { - manager.initialize(key, SQRT_PRICE_1_1, ZERO_BYTES); - (, address liquidityToken) = fullRange.poolInfo(id); - - if (amount <= LOCKED_LIQUIDITY) { - vm.expectRevert(FullRange.LiquidityDoesntMeetMinimum.selector); - fullRange.addLiquidity( - FullRange.AddLiquidityParams( - key.currency0, key.currency1, 3000, amount, amount, amount, amount, address(this), MAX_DEADLINE - ) - ); - } else if (amount >= MAX_TICK_LIQUIDITY) { - vm.expectRevert(); - fullRange.addLiquidity( - FullRange.AddLiquidityParams( - key.currency0, key.currency1, 3000, amount, amount, amount, amount, address(this), MAX_DEADLINE - ) - ); - } else { - fullRange.addLiquidity( - FullRange.AddLiquidityParams( - key.currency0, key.currency1, 3000, amount, amount, 0, 0, address(this), MAX_DEADLINE - ) - ); - - IPoolManager.SwapParams memory params = IPoolManager.SwapParams({ - zeroForOne: true, - amountSpecified: (FullMath.mulDiv(amount, 1, 4)).toInt256(), - sqrtPriceLimitX96: SQRT_PRICE_1_4 - }); - - HookEnabledSwapRouter.TestSettings memory testSettings = - HookEnabledSwapRouter.TestSettings({takeClaims: false, settleUsingBurn: false}); - - router.swap(key, params, testSettings, ZERO_BYTES); - - // Test contract removes liquidity, succeeds - UniswapV4ERC20(liquidityToken).approve(address(fullRange), type(uint256).max); - - uint256 liquidityTokenBal = UniswapV4ERC20(liquidityToken).balanceOf(address(this)); - - fullRange.removeLiquidity( - FullRange.RemoveLiquidityParams(key.currency0, key.currency1, 3000, liquidityTokenBal, MAX_DEADLINE) - ); - - assertTrue(manager.getLiquidity(id) <= LOCKED_LIQUIDITY + DUST); - } - } - - function testFullRange_BeforeModifyPositionFailsWithWrongMsgSender() public { - manager.initialize(key, SQRT_PRICE_1_1, ZERO_BYTES); - - vm.expectRevert(FullRange.SenderMustBeHook.selector); - modifyLiquidityRouter.modifyLiquidity( - key, - IPoolManager.ModifyLiquidityParams({tickLower: MIN_TICK, tickUpper: MAX_TICK, liquidityDelta: 100, salt: 0}), - ZERO_BYTES - ); - } - - function createPoolKey(MockERC20 tokenA, MockERC20 tokenB) internal view returns (PoolKey memory) { - if (address(tokenA) > address(tokenB)) (tokenA, tokenB) = (tokenB, tokenA); - return PoolKey(Currency.wrap(address(tokenA)), Currency.wrap(address(tokenB)), 3000, TICK_SPACING, fullRange); - } -} diff --git a/test/GeomeanOracle.t.sol b/test/GeomeanOracle.t.sol deleted file mode 100644 index e6ff1695..00000000 --- a/test/GeomeanOracle.t.sol +++ /dev/null @@ -1,221 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; - -import {Test} from "forge-std/Test.sol"; -import {GetSender} from "./shared/GetSender.sol"; -import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; -import {GeomeanOracle} from "../contracts/hooks/examples/GeomeanOracle.sol"; -import {GeomeanOracleImplementation} from "./shared/implementation/GeomeanOracleImplementation.sol"; -import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; -import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; -import {TestERC20} from "@uniswap/v4-core/src/test/TestERC20.sol"; -import {CurrencyLibrary, Currency} from "@uniswap/v4-core/src/types/Currency.sol"; -import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; -import {PoolModifyLiquidityTest} from "@uniswap/v4-core/src/test/PoolModifyLiquidityTest.sol"; -import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; -import {Oracle} from "../contracts/libraries/Oracle.sol"; -import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; - -contract TestGeomeanOracle is Test, Deployers { - using PoolIdLibrary for PoolKey; - - int24 constant MAX_TICK_SPACING = 32767; - - TestERC20 token0; - TestERC20 token1; - GeomeanOracleImplementation geomeanOracle = GeomeanOracleImplementation( - address( - uint160( - Hooks.BEFORE_INITIALIZE_FLAG | Hooks.AFTER_INITIALIZE_FLAG | Hooks.BEFORE_ADD_LIQUIDITY_FLAG - | Hooks.BEFORE_REMOVE_LIQUIDITY_FLAG | Hooks.BEFORE_SWAP_FLAG - ) - ) - ); - PoolId id; - - function setUp() public { - deployFreshManagerAndRouters(); - (currency0, currency1) = deployMintAndApprove2Currencies(); - - token0 = TestERC20(Currency.unwrap(currency0)); - token1 = TestERC20(Currency.unwrap(currency1)); - - vm.record(); - GeomeanOracleImplementation impl = new GeomeanOracleImplementation(manager, geomeanOracle); - (, bytes32[] memory writes) = vm.accesses(address(impl)); - vm.etch(address(geomeanOracle), address(impl).code); - // for each storage key that was written during the hook implementation, copy the value over - unchecked { - for (uint256 i = 0; i < writes.length; i++) { - bytes32 slot = writes[i]; - vm.store(address(geomeanOracle), slot, vm.load(address(impl), slot)); - } - } - geomeanOracle.setTime(1); - key = PoolKey(currency0, currency1, 0, MAX_TICK_SPACING, geomeanOracle); - id = key.toId(); - - modifyLiquidityRouter = new PoolModifyLiquidityTest(manager); - - token0.approve(address(geomeanOracle), type(uint256).max); - token1.approve(address(geomeanOracle), type(uint256).max); - token0.approve(address(modifyLiquidityRouter), type(uint256).max); - token1.approve(address(modifyLiquidityRouter), type(uint256).max); - } - - function testBeforeInitializeAllowsPoolCreation() public { - manager.initialize(key, SQRT_PRICE_1_1, ZERO_BYTES); - } - - function testBeforeInitializeRevertsIfFee() public { - vm.expectRevert(GeomeanOracle.OnlyOneOraclePoolAllowed.selector); - manager.initialize( - PoolKey(Currency.wrap(address(token0)), Currency.wrap(address(token1)), 1, MAX_TICK_SPACING, geomeanOracle), - SQRT_PRICE_1_1, - ZERO_BYTES - ); - } - - function testBeforeInitializeRevertsIfNotMaxTickSpacing() public { - vm.expectRevert(GeomeanOracle.OnlyOneOraclePoolAllowed.selector); - manager.initialize( - PoolKey(Currency.wrap(address(token0)), Currency.wrap(address(token1)), 0, 60, geomeanOracle), - SQRT_PRICE_1_1, - ZERO_BYTES - ); - } - - function testAfterInitializeState() public { - manager.initialize(key, SQRT_PRICE_2_1, ZERO_BYTES); - GeomeanOracle.ObservationState memory observationState = geomeanOracle.getState(key); - assertEq(observationState.index, 0); - assertEq(observationState.cardinality, 1); - assertEq(observationState.cardinalityNext, 1); - } - - function testAfterInitializeObservation() public { - manager.initialize(key, SQRT_PRICE_2_1, ZERO_BYTES); - Oracle.Observation memory observation = geomeanOracle.getObservation(key, 0); - assertTrue(observation.initialized); - assertEq(observation.blockTimestamp, 1); - assertEq(observation.tickCumulative, 0); - assertEq(observation.secondsPerLiquidityCumulativeX128, 0); - } - - function testAfterInitializeObserve0() public { - manager.initialize(key, SQRT_PRICE_2_1, ZERO_BYTES); - uint32[] memory secondsAgo = new uint32[](1); - secondsAgo[0] = 0; - (int56[] memory tickCumulatives, uint160[] memory secondsPerLiquidityCumulativeX128s) = - geomeanOracle.observe(key, secondsAgo); - assertEq(tickCumulatives.length, 1); - assertEq(secondsPerLiquidityCumulativeX128s.length, 1); - assertEq(tickCumulatives[0], 0); - assertEq(secondsPerLiquidityCumulativeX128s[0], 0); - } - - function testBeforeModifyPositionNoObservations() public { - manager.initialize(key, SQRT_PRICE_2_1, ZERO_BYTES); - modifyLiquidityRouter.modifyLiquidity( - key, - IPoolManager.ModifyLiquidityParams( - TickMath.minUsableTick(MAX_TICK_SPACING), TickMath.maxUsableTick(MAX_TICK_SPACING), 1000, 0 - ), - ZERO_BYTES - ); - - GeomeanOracle.ObservationState memory observationState = geomeanOracle.getState(key); - assertEq(observationState.index, 0); - assertEq(observationState.cardinality, 1); - assertEq(observationState.cardinalityNext, 1); - - Oracle.Observation memory observation = geomeanOracle.getObservation(key, 0); - assertTrue(observation.initialized); - assertEq(observation.blockTimestamp, 1); - assertEq(observation.tickCumulative, 0); - assertEq(observation.secondsPerLiquidityCumulativeX128, 0); - } - - function testBeforeModifyPositionObservation() public { - manager.initialize(key, SQRT_PRICE_2_1, ZERO_BYTES); - geomeanOracle.setTime(3); // advance 2 seconds - modifyLiquidityRouter.modifyLiquidity( - key, - IPoolManager.ModifyLiquidityParams( - TickMath.minUsableTick(MAX_TICK_SPACING), TickMath.maxUsableTick(MAX_TICK_SPACING), 1000, 0 - ), - ZERO_BYTES - ); - - GeomeanOracle.ObservationState memory observationState = geomeanOracle.getState(key); - assertEq(observationState.index, 0); - assertEq(observationState.cardinality, 1); - assertEq(observationState.cardinalityNext, 1); - - Oracle.Observation memory observation = geomeanOracle.getObservation(key, 0); - assertTrue(observation.initialized); - assertEq(observation.blockTimestamp, 3); - assertEq(observation.tickCumulative, 13862); - assertEq(observation.secondsPerLiquidityCumulativeX128, 680564733841876926926749214863536422912); - } - - function testBeforeModifyPositionObservationAndCardinality() public { - manager.initialize(key, SQRT_PRICE_2_1, ZERO_BYTES); - geomeanOracle.setTime(3); // advance 2 seconds - geomeanOracle.increaseCardinalityNext(key, 2); - GeomeanOracle.ObservationState memory observationState = geomeanOracle.getState(key); - assertEq(observationState.index, 0); - assertEq(observationState.cardinality, 1); - assertEq(observationState.cardinalityNext, 2); - - modifyLiquidityRouter.modifyLiquidity( - key, - IPoolManager.ModifyLiquidityParams( - TickMath.minUsableTick(MAX_TICK_SPACING), TickMath.maxUsableTick(MAX_TICK_SPACING), 1000, 0 - ), - ZERO_BYTES - ); - - // cardinality is updated - observationState = geomeanOracle.getState(key); - assertEq(observationState.index, 1); - assertEq(observationState.cardinality, 2); - assertEq(observationState.cardinalityNext, 2); - - // index 0 is untouched - Oracle.Observation memory observation = geomeanOracle.getObservation(key, 0); - assertTrue(observation.initialized); - assertEq(observation.blockTimestamp, 1); - assertEq(observation.tickCumulative, 0); - assertEq(observation.secondsPerLiquidityCumulativeX128, 0); - - // index 1 is written - observation = geomeanOracle.getObservation(key, 1); - assertTrue(observation.initialized); - assertEq(observation.blockTimestamp, 3); - assertEq(observation.tickCumulative, 13862); - assertEq(observation.secondsPerLiquidityCumulativeX128, 680564733841876926926749214863536422912); - } - - function testPermanentLiquidity() public { - manager.initialize(key, SQRT_PRICE_2_1, ZERO_BYTES); - geomeanOracle.setTime(3); // advance 2 seconds - modifyLiquidityRouter.modifyLiquidity( - key, - IPoolManager.ModifyLiquidityParams( - TickMath.minUsableTick(MAX_TICK_SPACING), TickMath.maxUsableTick(MAX_TICK_SPACING), 1000, 0 - ), - ZERO_BYTES - ); - - vm.expectRevert(GeomeanOracle.OraclePoolMustLockLiquidity.selector); - modifyLiquidityRouter.modifyLiquidity( - key, - IPoolManager.ModifyLiquidityParams( - TickMath.minUsableTick(MAX_TICK_SPACING), TickMath.maxUsableTick(MAX_TICK_SPACING), -1000, 0 - ), - ZERO_BYTES - ); - } -} diff --git a/test/LimitOrder.t.sol b/test/LimitOrder.t.sol deleted file mode 100644 index 17f5aecb..00000000 --- a/test/LimitOrder.t.sol +++ /dev/null @@ -1,222 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; - -import {Test} from "forge-std/Test.sol"; -import {GetSender} from "./shared/GetSender.sol"; -import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; -import {LimitOrder, Epoch, EpochLibrary} from "../contracts/hooks/examples/LimitOrder.sol"; -import {LimitOrderImplementation} from "./shared/implementation/LimitOrderImplementation.sol"; -import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; -import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; -import {TestERC20} from "@uniswap/v4-core/src/test/TestERC20.sol"; -import {CurrencyLibrary, Currency} from "@uniswap/v4-core/src/types/Currency.sol"; -import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; -import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; -import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; -import {HookEnabledSwapRouter} from "./utils/HookEnabledSwapRouter.sol"; -import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; - -contract TestLimitOrder is Test, Deployers { - using PoolIdLibrary for PoolKey; - using StateLibrary for IPoolManager; - - uint160 constant SQRT_RATIO_10_1 = 250541448375047931186413801569; - - HookEnabledSwapRouter router; - TestERC20 token0; - TestERC20 token1; - LimitOrder limitOrder = LimitOrder(address(uint160(Hooks.AFTER_INITIALIZE_FLAG | Hooks.AFTER_SWAP_FLAG))); - PoolId id; - - function setUp() public { - deployFreshManagerAndRouters(); - (currency0, currency1) = deployMintAndApprove2Currencies(); - - router = new HookEnabledSwapRouter(manager); - token0 = TestERC20(Currency.unwrap(currency0)); - token1 = TestERC20(Currency.unwrap(currency1)); - - vm.record(); - LimitOrderImplementation impl = new LimitOrderImplementation(manager, limitOrder); - (, bytes32[] memory writes) = vm.accesses(address(impl)); - vm.etch(address(limitOrder), address(impl).code); - // for each storage key that was written during the hook implementation, copy the value over - unchecked { - for (uint256 i = 0; i < writes.length; i++) { - bytes32 slot = writes[i]; - vm.store(address(limitOrder), slot, vm.load(address(impl), slot)); - } - } - - // key = PoolKey(currency0, currency1, 3000, 60, limitOrder); - (key, id) = initPoolAndAddLiquidity(currency0, currency1, limitOrder, 3000, SQRT_PRICE_1_1, ZERO_BYTES); - - token0.approve(address(limitOrder), type(uint256).max); - token1.approve(address(limitOrder), type(uint256).max); - token0.approve(address(router), type(uint256).max); - token1.approve(address(router), type(uint256).max); - } - - function testGetTickLowerLast() public { - assertEq(limitOrder.getTickLowerLast(id), 0); - } - - function testGetTickLowerLastWithDifferentPrice() public { - PoolKey memory differentKey = - PoolKey(Currency.wrap(address(token0)), Currency.wrap(address(token1)), 3000, 61, limitOrder); - manager.initialize(differentKey, SQRT_RATIO_10_1, ZERO_BYTES); - assertEq(limitOrder.getTickLowerLast(differentKey.toId()), 22997); - } - - function testEpochNext() public { - assertTrue(EpochLibrary.equals(limitOrder.epochNext(), Epoch.wrap(1))); - } - - function testZeroLiquidityRevert() public { - vm.expectRevert(LimitOrder.ZeroLiquidity.selector); - limitOrder.place(key, 0, true, 0); - } - - function testZeroForOneRightBoundaryOfCurrentRange() public { - int24 tickLower = 60; - bool zeroForOne = true; - uint128 liquidity = 1000000; - limitOrder.place(key, tickLower, zeroForOne, liquidity); - assertTrue(EpochLibrary.equals(limitOrder.getEpoch(key, tickLower, zeroForOne), Epoch.wrap(1))); - - assertEq(manager.getPosition(id, address(limitOrder), tickLower, tickLower + 60, 0).liquidity, liquidity); - } - - function testZeroForOneLeftBoundaryOfCurrentRange() public { - int24 tickLower = 0; - bool zeroForOne = true; - uint128 liquidity = 1000000; - limitOrder.place(key, tickLower, zeroForOne, liquidity); - assertTrue(EpochLibrary.equals(limitOrder.getEpoch(key, tickLower, zeroForOne), Epoch.wrap(1))); - assertEq(manager.getPosition(id, address(limitOrder), tickLower, tickLower + 60, 0).liquidity, liquidity); - } - - function testZeroForOneCrossedRangeRevert() public { - vm.expectRevert(LimitOrder.CrossedRange.selector); - limitOrder.place(key, -60, true, 1000000); - } - - function testZeroForOneInRangeRevert() public { - // swapping is free, there's no liquidity in the pool, so we only need to specify 1 wei - router.swap( - key, - IPoolManager.SwapParams(false, -1 ether, SQRT_PRICE_1_1 + 1), - HookEnabledSwapRouter.TestSettings(false, false), - ZERO_BYTES - ); - vm.expectRevert(LimitOrder.InRange.selector); - limitOrder.place(key, 0, true, 1000000); - } - - function testNotZeroForOneLeftBoundaryOfCurrentRange() public { - int24 tickLower = -60; - bool zeroForOne = false; - uint128 liquidity = 1000000; - limitOrder.place(key, tickLower, zeroForOne, liquidity); - assertTrue(EpochLibrary.equals(limitOrder.getEpoch(key, tickLower, zeroForOne), Epoch.wrap(1))); - assertEq(manager.getPosition(id, address(limitOrder), tickLower, tickLower + 60, 0).liquidity, liquidity); - } - - function testNotZeroForOneCrossedRangeRevert() public { - vm.expectRevert(LimitOrder.CrossedRange.selector); - limitOrder.place(key, 0, false, 1000000); - } - - function testNotZeroForOneInRangeRevert() public { - // swapping is free, there's no liquidity in the pool, so we only need to specify 1 wei - router.swap( - key, - IPoolManager.SwapParams(true, -1 ether, SQRT_PRICE_1_1 - 1), - HookEnabledSwapRouter.TestSettings(false, false), - ZERO_BYTES - ); - vm.expectRevert(LimitOrder.InRange.selector); - limitOrder.place(key, -60, false, 1000000); - } - - function testMultipleLPs() public { - int24 tickLower = 60; - bool zeroForOne = true; - uint128 liquidity = 1000000; - limitOrder.place(key, tickLower, zeroForOne, liquidity); - address other = 0x1111111111111111111111111111111111111111; - token0.transfer(other, 1e18); - token1.transfer(other, 1e18); - vm.startPrank(other); - token0.approve(address(limitOrder), type(uint256).max); - token1.approve(address(limitOrder), type(uint256).max); - limitOrder.place(key, tickLower, zeroForOne, liquidity); - vm.stopPrank(); - assertTrue(EpochLibrary.equals(limitOrder.getEpoch(key, tickLower, zeroForOne), Epoch.wrap(1))); - assertEq(manager.getPosition(id, address(limitOrder), tickLower, tickLower + 60, 0).liquidity, liquidity * 2); - - ( - bool filled, - Currency currency0, - Currency currency1, - uint256 token0Total, - uint256 token1Total, - uint128 liquidityTotal - ) = limitOrder.epochInfos(Epoch.wrap(1)); - assertFalse(filled); - assertTrue(currency0 == Currency.wrap(address(token0))); - assertTrue(currency1 == Currency.wrap(address(token1))); - assertEq(token0Total, 0); - assertEq(token1Total, 0); - assertEq(liquidityTotal, liquidity * 2); - assertEq(limitOrder.getEpochLiquidity(Epoch.wrap(1), new GetSender().sender()), liquidity); - assertEq(limitOrder.getEpochLiquidity(Epoch.wrap(1), other), liquidity); - } - - event Transfer(address indexed from, address indexed to, uint256 value); - - function testKill() public { - int24 tickLower = 0; - bool zeroForOne = true; - uint128 liquidity = 1000000; - limitOrder.place(key, tickLower, zeroForOne, liquidity); - vm.expectEmit(true, true, true, true, address(token0)); - emit Transfer(address(manager), new GetSender().sender(), 2995); - limitOrder.kill(key, tickLower, zeroForOne, new GetSender().sender()); - } - - function testSwapAcrossRange() public { - int24 tickLower = 0; - bool zeroForOne = true; - uint128 liquidity = 1000000; - limitOrder.place(key, tickLower, zeroForOne, liquidity); - - router.swap( - key, - IPoolManager.SwapParams(false, -1e18, TickMath.getSqrtPriceAtTick(60)), - HookEnabledSwapRouter.TestSettings(false, false), - ZERO_BYTES - ); - - assertEq(limitOrder.getTickLowerLast(id), 60); - (, int24 tick,,) = manager.getSlot0(id); - assertEq(tick, 60); - - (bool filled,,, uint256 token0Total, uint256 token1Total,) = limitOrder.epochInfos(Epoch.wrap(1)); - - assertTrue(filled); - assertEq(token0Total, 0); - assertEq(token1Total, 2996 + 17); // 3013, 2 wei of dust - assertEq(manager.getPosition(id, address(limitOrder), tickLower, tickLower + 60, 0).liquidity, 0); - - vm.expectEmit(true, true, true, true, address(token1)); - emit Transfer(address(manager), new GetSender().sender(), 2996 + 17); - limitOrder.withdraw(Epoch.wrap(1), new GetSender().sender()); - - (,,, token0Total, token1Total,) = limitOrder.epochInfos(Epoch.wrap(1)); - - assertEq(token0Total, 0); - assertEq(token1Total, 0); - } -} diff --git a/test/MiddlewareRemoveFactory.t.sol b/test/MiddlewareRemoveFactory.t.sol new file mode 100644 index 00000000..92d3d791 --- /dev/null +++ b/test/MiddlewareRemoveFactory.t.sol @@ -0,0 +1,450 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import {Test} from "forge-std/Test.sol"; +import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; +import {HooksCounter} from "./middleware/HooksCounter.sol"; +import {MiddlewareRemoveNoDeltas} from "../src/middleware/MiddlewareRemoveNoDeltas.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; +import {TestERC20} from "@uniswap/v4-core/src/test/TestERC20.sol"; +import {CurrencyLibrary, Currency} from "@uniswap/v4-core/src/types/Currency.sol"; +import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {HookEnabledSwapRouter} from "./utils/HookEnabledSwapRouter.sol"; +import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; +import {console} from "../../../lib/v4-core/lib/forge-std/src/console.sol"; +import {HooksRevert} from "./middleware/HooksRevert.sol"; +import {HooksOutOfGas} from "./middleware/HooksOutOfGas.sol"; +import {MiddlewareRemoveFactory} from "./../src/middleware/MiddlewareRemoveFactory.sol"; +import {MiddlewareMiner} from "./utils/MiddlewareMiner.sol"; +import {SafeCallback} from "./../src/base/SafeCallback.sol"; +import {FeeOnRemove} from "./middleware/FeeOnRemove.sol"; +import {FrontrunRemove} from "./middleware/FrontrunRemove.sol"; +import {RemoveGriefs} from "./middleware/RemoveGriefs.sol"; +import {RemoveReturnsMaxDeltas} from "./middleware/RemoveReturnsMaxDeltas.sol"; +import {BaseRemove} from "./../src/middleware/BaseRemove.sol"; +import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; +import {BlankRemoveHooks} from "./middleware/BlankRemoveHooks.sol"; + +contract MiddlewareRemoveFactoryTest is Test, Deployers, GasSnapshot { + HookEnabledSwapRouter router; + TestERC20 token0; + TestERC20 token1; + + MiddlewareRemoveFactory factory; + HooksCounter hookscounter; + address middleware; + + uint160 COUNTER_FLAGS = uint160( + Hooks.BEFORE_INITIALIZE_FLAG | Hooks.AFTER_INITIALIZE_FLAG | Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG + | Hooks.BEFORE_ADD_LIQUIDITY_FLAG | Hooks.AFTER_ADD_LIQUIDITY_FLAG | Hooks.BEFORE_REMOVE_LIQUIDITY_FLAG + | Hooks.AFTER_REMOVE_LIQUIDITY_FLAG | Hooks.BEFORE_DONATE_FLAG | Hooks.AFTER_DONATE_FLAG + ); + + function setUp() public { + deployFreshManagerAndRouters(); + (currency0, currency1) = deployMintAndApprove2Currencies(); + + router = new HookEnabledSwapRouter(manager); + token0 = TestERC20(Currency.unwrap(currency0)); + token1 = TestERC20(Currency.unwrap(currency1)); + + factory = new MiddlewareRemoveFactory(manager); + hookscounter = HooksCounter(address(COUNTER_FLAGS)); + vm.etch(address(hookscounter), address(new HooksCounter(manager)).code); + + token0.approve(address(router), type(uint256).max); + token1.approve(address(router), type(uint256).max); + + (address hookAddress, bytes32 salt) = + MiddlewareMiner.find(address(factory), COUNTER_FLAGS, address(manager), address(hookscounter), 0); + middleware = factory.createMiddleware(address(hookscounter), 0, salt); + assertEq(hookAddress, middleware); + } + + function testFrontrunRemove() public { + uint160 flags = uint160(Hooks.BEFORE_REMOVE_LIQUIDITY_FLAG); + FrontrunRemove frontrunRemove = FrontrunRemove(address(flags)); + vm.etch(address(frontrunRemove), address(new FrontrunRemove(manager)).code); + (, bytes32 salt) = MiddlewareMiner.find(address(factory), flags, address(manager), address(frontrunRemove), 0); + middleware = factory.createMiddleware(address(frontrunRemove), 0, salt); + currency0.transfer(address(frontrunRemove), 1 ether); + currency1.transfer(address(frontrunRemove), 1 ether); + currency0.transfer(address(middleware), 1 ether); + currency1.transfer(address(middleware), 1 ether); + + (key,) = initPoolAndAddLiquidity(currency0, currency1, IHooks(frontrunRemove), 3000, SQRT_PRICE_1_1, ZERO_BYTES); + uint256 initialBalance0 = token0.balanceOf(address(this)); + uint256 initialBalance1 = token1.balanceOf(address(this)); + modifyLiquidityRouter.modifyLiquidity(key, REMOVE_LIQUIDITY_PARAMS, ZERO_BYTES); + uint256 outFrontrun0 = token0.balanceOf(address(this)) - initialBalance0; + uint256 outFrontrun1 = token1.balanceOf(address(this)) - initialBalance1; + + IHooks blankRemoveHooks = IHooks(address(0)); + (key,) = initPoolAndAddLiquidity(currency0, currency1, blankRemoveHooks, 3000, SQRT_PRICE_1_1, ZERO_BYTES); + initialBalance0 = token0.balanceOf(address(this)); + initialBalance1 = token1.balanceOf(address(this)); + modifyLiquidityRouter.modifyLiquidity(key, REMOVE_LIQUIDITY_PARAMS, ZERO_BYTES); + uint256 outNormal0 = token0.balanceOf(address(this)) - initialBalance0; + uint256 outNormal1 = token1.balanceOf(address(this)) - initialBalance1; + + // was frontrun + assertTrue(outFrontrun0 > outNormal0); + assertTrue(outFrontrun1 < outNormal1); + + // (key,) = initPoolAndAddLiquidity(currency0, currency1, IHooks(middleware), 3000, SQRT_PRICE_1_1, ZERO_BYTES); + // initialBalance0 = token0.balanceOf(address(this)); + // initialBalance1 = token1.balanceOf(address(this)); + // modifyLiquidityRouter.modifyLiquidity(key, REMOVE_LIQUIDITY_PARAMS, ZERO_BYTES); + // uint256 out0 = token0.balanceOf(address(this)) - initialBalance0; + // uint256 out1 = token1.balanceOf(address(this)) - initialBalance1; + + // // no frontrun + // // assertEq(outNormal0, out0); + // // assertEq(outNormal1, out1); + + // // was frontrun + // assertTrue(out0 > outNormal0); + // assertTrue(out1 < outNormal1); + } + + function testRevertOnNotSettled() public { + uint160 flags = uint160(Hooks.AFTER_REMOVE_LIQUIDITY_FLAG); + FeeOnRemove feeOnRemove = FeeOnRemove(address(flags)); + vm.etch(address(feeOnRemove), address(new FeeOnRemove(manager)).code); + feeOnRemove.setLiquidityFee(456); + + (key,) = initPoolAndAddLiquidity(currency0, currency1, IHooks(feeOnRemove), 3000, SQRT_PRICE_1_1, ZERO_BYTES); + vm.expectRevert(IPoolManager.CurrencyNotSettled.selector); + modifyLiquidityRouter.modifyLiquidity(key, REMOVE_LIQUIDITY_PARAMS, ZERO_BYTES); + } + + function testFeesOnRemove() public { + uint160 flags = uint160(Hooks.AFTER_REMOVE_LIQUIDITY_FLAG); + FeeOnRemove feeOnRemove = FeeOnRemove(address(flags)); + vm.etch(address(feeOnRemove), address(new FeeOnRemove(manager)).code); + feeOnRemove.setLiquidityFee(456); + + (key,) = initPoolAndAddLiquidity(currency0, currency1, IHooks(feeOnRemove), 3000, SQRT_PRICE_1_1, ZERO_BYTES); + vm.expectRevert(IPoolManager.CurrencyNotSettled.selector); + modifyLiquidityRouter.modifyLiquidity(key, REMOVE_LIQUIDITY_PARAMS, ZERO_BYTES); + } + + function testFeeOnVariousMaxFees() public { + (uint256 outNormal0, uint256 outNormal1) = getNormal(); + (uint256 outWithFees0, uint256 outWithFees1) = getWithFees(543); + uint256 out0; + uint256 out1; + (out0, out1) = testFeeOnRemove(0); + assertEq(out0, outNormal0); + assertEq(out1, outNormal1); + (out0, out1) = testFeeOnRemove(100); + assertEq(out0, outNormal0); + assertEq(out1, outNormal1); + (out0, out1) = testFeeOnRemove(542); + assertEq(out0, outNormal0); + assertEq(out1, outNormal1); + (out0, out1) = testFeeOnRemove(543); + assertEq(out0, outWithFees0); + assertEq(out1, outWithFees1); + } + + function testFeeFuzz(uint128 fee, uint128 maxFeeBips) public { + if (uint256(keccak256(abi.encode(fee, maxFeeBips))) % 20 != 0) return; // throttle number of runs + fee = fee % 10000; + maxFeeBips = maxFeeBips % 10000; + uint160 flags; + if (maxFeeBips == 0) { + flags = uint160(Hooks.AFTER_REMOVE_LIQUIDITY_FLAG); + } else { + flags = uint160(Hooks.AFTER_REMOVE_LIQUIDITY_FLAG | Hooks.AFTER_REMOVE_LIQUIDITY_RETURNS_DELTA_FLAG); + } + FeeOnRemove feeOnRemove = FeeOnRemove(address(flags)); + vm.etch(address(feeOnRemove), address(new FeeOnRemove(manager)).code); + // not necessary: + // feeOnRemove.setLiquidityFee(fee); + (, bytes32 salt) = + MiddlewareMiner.find(address(factory), flags, address(manager), address(feeOnRemove), maxFeeBips); + middleware = factory.createMiddleware(address(feeOnRemove), maxFeeBips, salt); + FeeOnRemove(address(middleware)).setLiquidityFee(fee); + + (key,) = initPoolAndAddLiquidity(currency0, currency1, IHooks(middleware), 3000, SQRT_PRICE_1_1, ZERO_BYTES); + uint256 initialBalance0 = token0.balanceOf(address(this)); + uint256 initialBalance1 = token1.balanceOf(address(this)); + modifyLiquidityRouter.modifyLiquidity(key, REMOVE_LIQUIDITY_PARAMS, ZERO_BYTES); + uint256 out0 = token0.balanceOf(address(this)) - initialBalance0; + uint256 out1 = token1.balanceOf(address(this)) - initialBalance1; + if (fee > maxFeeBips) { + console.log("high"); + (uint256 outNormal0, uint256 outNormal1) = getNormal(); + assertEq(out0, outNormal0); + assertEq(out1, outNormal1); + } else { + console.log("low", fee, maxFeeBips); + (uint256 outWithFees0, uint256 outWithFees1) = getWithFees(fee); + assertEq(out0, outWithFees0); + assertEq(out1, outWithFees1); + } + } + + function getNormal() internal returns (uint256 outNormal0, uint256 outNormal1) { + IHooks blankRemoveHooks = IHooks(address(0)); + (key,) = initPoolAndAddLiquidity(currency0, currency1, blankRemoveHooks, 3000, SQRT_PRICE_1_1, ZERO_BYTES); + uint256 initialBalance0 = token0.balanceOf(address(this)); + uint256 initialBalance1 = token1.balanceOf(address(this)); + modifyLiquidityRouter.modifyLiquidity(key, REMOVE_LIQUIDITY_PARAMS, ZERO_BYTES); + outNormal0 = token0.balanceOf(address(this)) - initialBalance0; + outNormal1 = token1.balanceOf(address(this)) - initialBalance1; + } + + function getWithFees(uint128 fee) internal returns (uint256 outWithFees0, uint256 outWithFees1) { + uint160 flags = uint160(Hooks.AFTER_REMOVE_LIQUIDITY_FLAG | Hooks.AFTER_REMOVE_LIQUIDITY_RETURNS_DELTA_FLAG); + FeeOnRemove feeOnRemove = FeeOnRemove(address(flags)); + vm.etch(address(feeOnRemove), address(new FeeOnRemove(manager)).code); + feeOnRemove.setLiquidityFee(fee); + (key,) = initPoolAndAddLiquidity(currency0, currency1, feeOnRemove, 3000, SQRT_PRICE_1_1, ZERO_BYTES); + uint256 initialBalance0 = token0.balanceOf(address(this)); + uint256 initialBalance1 = token1.balanceOf(address(this)); + modifyLiquidityRouter.modifyLiquidity(key, REMOVE_LIQUIDITY_PARAMS, ZERO_BYTES); + outWithFees0 = token0.balanceOf(address(this)) - initialBalance0; + outWithFees1 = token1.balanceOf(address(this)) - initialBalance1; + } + + function testFeeOnRemove(uint256 maxFeeBips) internal returns (uint256 out0, uint256 out1) { + uint160 flags; + if (maxFeeBips == 0) { + flags = uint160(Hooks.AFTER_REMOVE_LIQUIDITY_FLAG); + } else { + flags = uint160(Hooks.AFTER_REMOVE_LIQUIDITY_FLAG | Hooks.AFTER_REMOVE_LIQUIDITY_RETURNS_DELTA_FLAG); + } + FeeOnRemove feeOnRemove = FeeOnRemove(address(flags)); + vm.etch(address(feeOnRemove), address(new FeeOnRemove(manager)).code); + // not necessary: + // feeOnRemove.setLiquidityFee(543); + (, bytes32 salt) = + MiddlewareMiner.find(address(factory), flags, address(manager), address(feeOnRemove), maxFeeBips); + middleware = factory.createMiddleware(address(feeOnRemove), maxFeeBips, salt); + FeeOnRemove(address(middleware)).setLiquidityFee(543); + + (key,) = initPoolAndAddLiquidity(currency0, currency1, IHooks(middleware), 3000, SQRT_PRICE_1_1, ZERO_BYTES); + uint256 initialBalance0 = token0.balanceOf(address(this)); + uint256 initialBalance1 = token1.balanceOf(address(this)); + modifyLiquidityRouter.modifyLiquidity(key, REMOVE_LIQUIDITY_PARAMS, ZERO_BYTES); + out0 = token0.balanceOf(address(this)) - initialBalance0; + out1 = token1.balanceOf(address(this)) - initialBalance1; + } + + function testVariousFactory() public { + uint160 flags = uint160( + Hooks.BEFORE_INITIALIZE_FLAG | Hooks.AFTER_INITIALIZE_FLAG | Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG + | Hooks.BEFORE_ADD_LIQUIDITY_FLAG | Hooks.AFTER_ADD_LIQUIDITY_FLAG | Hooks.BEFORE_REMOVE_LIQUIDITY_FLAG + | Hooks.AFTER_REMOVE_LIQUIDITY_FLAG | Hooks.BEFORE_DONATE_FLAG | Hooks.AFTER_DONATE_FLAG + ); + testOn(address(hookscounter), flags); + + flags = uint160( + Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG | Hooks.BEFORE_REMOVE_LIQUIDITY_FLAG + | Hooks.AFTER_REMOVE_LIQUIDITY_FLAG + ); + HooksRevert hooksRevert = HooksRevert(address(flags)); + vm.etch(address(hooksRevert), address(new HooksRevert(manager)).code); + testOn(address(hooksRevert), flags); + + HooksOutOfGas hooksOutOfGas = HooksOutOfGas(address(flags)); + vm.etch(address(hooksOutOfGas), address(new HooksOutOfGas(manager)).code); + testOn(address(hooksOutOfGas), flags); + } + + function testGriefs() public { + uint160 flags = uint160(Hooks.BEFORE_REMOVE_LIQUIDITY_FLAG | Hooks.AFTER_REMOVE_LIQUIDITY_FLAG); + RemoveGriefs removeGriefs = RemoveGriefs(address(flags)); + vm.etch(address(removeGriefs), address(new RemoveGriefs(manager)).code); + testOn(address(removeGriefs), flags); + + flags = uint160(Hooks.AFTER_REMOVE_LIQUIDITY_FLAG); + uint160 flagsWithDeltas = flags | Hooks.AFTER_REMOVE_LIQUIDITY_RETURNS_DELTA_FLAG; + (address hookAddress,) = + MiddlewareMiner.find(address(factory), flagsWithDeltas, address(manager), address(flagsWithDeltas), 100); + currency0.transfer(address(hookAddress), currency0.balanceOf(address(this)) / 2); + currency1.transfer(address(hookAddress), currency1.balanceOf(address(this)) / 2); + RemoveReturnsMaxDeltas removeReturnsMaxDeltas = RemoveReturnsMaxDeltas(address(flags)); + vm.etch(address(removeReturnsMaxDeltas), address(new RemoveReturnsMaxDeltas(manager)).code); + testOn(address(removeReturnsMaxDeltas), flags); + } + + // creates a middleware on an implementation + function testOn(address implementation, uint160 flags) internal { + uint256 maxFeeBips = 0; + (, bytes32 salt) = MiddlewareMiner.find(address(factory), flags, address(manager), implementation, maxFeeBips); + address hookAddress = factory.createMiddleware(implementation, maxFeeBips, salt); + (key,) = initPoolAndAddLiquidity(currency0, currency1, IHooks(hookAddress), 3000, SQRT_PRICE_1_1, ZERO_BYTES); + // does not revert + uint256 gasLeft = gasleft(); + modifyLiquidityRouter.modifyLiquidity(key, REMOVE_LIQUIDITY_PARAMS, ZERO_BYTES); + console.log("A", gasLeft - gasleft()); + assertEq(factory.getImplementation(hookAddress), implementation); + assertEq(factory.getMaxFeeBips(hookAddress), maxFeeBips); + + flags = flags | Hooks.AFTER_REMOVE_LIQUIDITY_RETURNS_DELTA_FLAG; + address implementationWithReturnsDelta = address(flags); + vm.etch(implementationWithReturnsDelta, implementation.code); + maxFeeBips = 100; + (, salt) = + MiddlewareMiner.find(address(factory), flags, address(manager), implementationWithReturnsDelta, maxFeeBips); + hookAddress = factory.createMiddleware(implementationWithReturnsDelta, maxFeeBips, salt); + (key,) = initPoolAndAddLiquidity(currency0, currency1, IHooks(hookAddress), 3000, SQRT_PRICE_1_1, ZERO_BYTES); + // does not revert + gasLeft = gasleft(); + modifyLiquidityRouter.modifyLiquidity(key, REMOVE_LIQUIDITY_PARAMS, ZERO_BYTES); + console.log("B", gasLeft - gasleft()); + assertEq(factory.getImplementation(hookAddress), implementationWithReturnsDelta); + assertEq(factory.getMaxFeeBips(hookAddress), maxFeeBips); + } + + function testRevertOnDeltaFlags() public { + uint160 flags = uint160(Hooks.AFTER_REMOVE_LIQUIDITY_RETURNS_DELTA_FLAG); + address removeReturnDeltas = address(1 << 100 | flags); + (address hookAddress, bytes32 salt) = + MiddlewareMiner.find(address(factory), flags, address(manager), address(removeReturnDeltas), 0); + vm.expectRevert(abi.encodePacked(bytes16(BaseRemove.HookPermissionForbidden.selector), hookAddress)); + factory.createMiddleware(address(removeReturnDeltas), 0, salt); + } + + // from BaseMiddlewareFactory.t.sol + function testRevertOnSameDeployment() public { + uint160 flags = uint160( + Hooks.BEFORE_INITIALIZE_FLAG | Hooks.AFTER_INITIALIZE_FLAG | Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG + | Hooks.BEFORE_ADD_LIQUIDITY_FLAG | Hooks.AFTER_ADD_LIQUIDITY_FLAG | Hooks.BEFORE_REMOVE_LIQUIDITY_FLAG + | Hooks.AFTER_REMOVE_LIQUIDITY_FLAG | Hooks.BEFORE_DONATE_FLAG | Hooks.AFTER_DONATE_FLAG + ); + (, bytes32 salt) = MiddlewareMiner.find(address(factory), flags, address(manager), address(hookscounter), 0); + + factory.createMiddleware(address(hookscounter), 0, salt); + // second deployment should revert + vm.expectRevert(ZERO_BYTES); + factory.createMiddleware(address(hookscounter), 0, salt); + } + + function testRevertOnIncorrectCaller() public { + vm.expectRevert(SafeCallback.NotPoolManager.selector); + hookscounter.afterDonate(address(this), key, 0, 0, ZERO_BYTES); + } + + function testCounters() public { + (PoolKey memory key, PoolId id) = + initPoolAndAddLiquidity(currency0, currency1, IHooks(middleware), 3000, SQRT_PRICE_1_1, ZERO_BYTES); + + HooksCounter counterProxy = HooksCounter(middleware); + assertEq(counterProxy.beforeInitializeCount(id), 1); + assertEq(counterProxy.afterInitializeCount(id), 1); + assertEq(counterProxy.beforeSwapCount(id), 0); + assertEq(counterProxy.afterSwapCount(id), 0); + assertEq(counterProxy.beforeAddLiquidityCount(id), 1); + assertEq(counterProxy.afterAddLiquidityCount(id), 1); + assertEq(counterProxy.beforeRemoveLiquidityCount(id), 0); + assertEq(counterProxy.afterRemoveLiquidityCount(id), 0); + assertEq(counterProxy.beforeDonateCount(id), 0); + assertEq(counterProxy.afterDonateCount(id), 0); + + assertEq(counterProxy.lastHookData(), ZERO_BYTES); + swap(key, true, 1, bytes("hi")); + assertEq(counterProxy.lastHookData(), bytes("hi")); + assertEq(counterProxy.beforeSwapCount(id), 1); + assertEq(counterProxy.afterSwapCount(id), 1); + + // hookscounter does not store data itself + assertEq(hookscounter.lastHookData(), bytes("")); + assertEq(hookscounter.beforeSwapCount(id), 0); + assertEq(hookscounter.afterSwapCount(id), 0); + + modifyLiquidityRouter.modifyLiquidity(key, REMOVE_LIQUIDITY_PARAMS, ZERO_BYTES); + assertEq(counterProxy.beforeRemoveLiquidityCount(id), 1); + assertEq(counterProxy.afterRemoveLiquidityCount(id), 1); + } + + function testDataChaining() public { + (PoolKey memory key,) = + initPoolAndAddLiquidity(currency0, currency1, IHooks(middleware), 3000, SQRT_PRICE_1_1, ZERO_BYTES); + HooksCounter counterProxy = HooksCounter(middleware); + modifyLiquidityRouter.modifyLiquidity( + key, REMOVE_LIQUIDITY_PARAMS, hex"23b70c8dec38c3dec67a5596870027b04c4058cb3ac57b4e589bf628ac6669e7FFFF" + ); + assertEq(counterProxy.lastHookData(), hex"FFFF"); + + hookscounter = HooksCounter(address(COUNTER_FLAGS | Hooks.AFTER_REMOVE_LIQUIDITY_RETURNS_DELTA_FLAG)); + vm.etch(address(hookscounter), address(new HooksCounter(manager)).code); + (, bytes32 salt) = MiddlewareMiner.find( + address(factory), + COUNTER_FLAGS | Hooks.AFTER_REMOVE_LIQUIDITY_RETURNS_DELTA_FLAG, + address(manager), + address(hookscounter), + 777 + ); + middleware = factory.createMiddleware(address(hookscounter), 777, salt); + (key,) = initPoolAndAddLiquidity(currency0, currency1, IHooks(middleware), 3000, SQRT_PRICE_1_1, ZERO_BYTES); + counterProxy = HooksCounter(middleware); + modifyLiquidityRouter.modifyLiquidity( + key, REMOVE_LIQUIDITY_PARAMS, hex"23b70c8dec38c3dec67a5596870027b04c4058cb3ac57b4e589bf628ac6669e7AAAA" + ); + assertEq(counterProxy.lastHookData(), hex"AAAA"); + } + + function testMiddlewareRemoveGas() public { + uint160 flags = Hooks.BEFORE_REMOVE_LIQUIDITY_FLAG | Hooks.AFTER_REMOVE_LIQUIDITY_FLAG; + BlankRemoveHooks blankRemoveHooks = BlankRemoveHooks(address(flags)); + vm.etch(address(blankRemoveHooks), address(new BlankRemoveHooks(manager)).code); + (key,) = initPoolAndAddLiquidity( + currency0, currency1, IHooks(address(blankRemoveHooks)), 3000, SQRT_PRICE_1_1, ZERO_BYTES + ); + modifyLiquidityRouter.modifyLiquidity(key, REMOVE_LIQUIDITY_PARAMS, ZERO_BYTES); + snapLastCall("MIDDLEWARE_REMOVE-vanilla"); + uint160 maxFeeBips = 0; + (, bytes32 salt) = + MiddlewareMiner.find(address(factory), flags, address(manager), address(blankRemoveHooks), maxFeeBips); + address hookAddress = factory.createMiddleware(address(blankRemoveHooks), maxFeeBips, salt); + (key,) = initPoolAndAddLiquidity(currency0, currency1, IHooks(hookAddress), 3000, SQRT_PRICE_1_1, ZERO_BYTES); + modifyLiquidityRouter.modifyLiquidity(key, REMOVE_LIQUIDITY_PARAMS, ZERO_BYTES); + snapLastCall("MIDDLEWARE_REMOVE-protected"); + (, salt) = + MiddlewareMiner.find(address(factory), flags, address(manager), address(blankRemoveHooks), maxFeeBips); + hookAddress = factory.createMiddleware(address(blankRemoveHooks), maxFeeBips, salt); + (key,) = initPoolAndAddLiquidity(currency0, currency1, IHooks(hookAddress), 3000, SQRT_PRICE_1_1, ZERO_BYTES); + bytes memory OVERRIDE_BYTES = + abi.encode(MiddlewareRemoveNoDeltas(payable(address(hookAddress))).OVERRIDE_BYTES()); + modifyLiquidityRouter.modifyLiquidity(key, REMOVE_LIQUIDITY_PARAMS, OVERRIDE_BYTES); + snapLastCall("MIDDLEWARE_REMOVE-override"); + + flags = flags | Hooks.AFTER_REMOVE_LIQUIDITY_RETURNS_DELTA_FLAG; + blankRemoveHooks = BlankRemoveHooks(address(flags)); + vm.etch(address(blankRemoveHooks), address(new BlankRemoveHooks(manager)).code); + (key,) = initPoolAndAddLiquidity( + currency0, currency1, IHooks(address(blankRemoveHooks)), 3000, SQRT_PRICE_1_1, ZERO_BYTES + ); + modifyLiquidityRouter.modifyLiquidity(key, REMOVE_LIQUIDITY_PARAMS, ZERO_BYTES); + snapLastCall("MIDDLEWARE_REMOVE-deltas-vanilla"); + maxFeeBips = 1000; + (, salt) = + MiddlewareMiner.find(address(factory), flags, address(manager), address(blankRemoveHooks), maxFeeBips); + hookAddress = factory.createMiddleware(address(blankRemoveHooks), maxFeeBips, salt); + (key,) = initPoolAndAddLiquidity(currency0, currency1, IHooks(hookAddress), 3000, SQRT_PRICE_1_1, ZERO_BYTES); + modifyLiquidityRouter.modifyLiquidity(key, REMOVE_LIQUIDITY_PARAMS, ZERO_BYTES); + snapLastCall("MIDDLEWARE_REMOVE-deltas-protected"); + + flags = flags; + FeeOnRemove feeOnRemove = FeeOnRemove(address(flags | 0x10000000)); + vm.etch(address(feeOnRemove), address(new FeeOnRemove(manager)).code); + (key,) = initPoolAndAddLiquidity( + currency0, currency1, IHooks(address(feeOnRemove)), 3000, SQRT_PRICE_1_1, ZERO_BYTES + ); + modifyLiquidityRouter.modifyLiquidity(key, REMOVE_LIQUIDITY_PARAMS, ZERO_BYTES); + snapLastCall("MIDDLEWARE_REMOVE-fee-vanilla"); + (, salt) = MiddlewareMiner.find(address(factory), flags, address(manager), address(feeOnRemove), maxFeeBips); + hookAddress = factory.createMiddleware(address(feeOnRemove), maxFeeBips, salt); + (key,) = initPoolAndAddLiquidity(currency0, currency1, IHooks(hookAddress), 3000, SQRT_PRICE_1_1, ZERO_BYTES); + modifyLiquidityRouter.modifyLiquidity(key, REMOVE_LIQUIDITY_PARAMS, ZERO_BYTES); + snapLastCall("MIDDLEWARE_REMOVE-fee-protected"); + } +} diff --git a/test/Multicall.t.sol b/test/Multicall.t.sol new file mode 100644 index 00000000..18cdd2a3 --- /dev/null +++ b/test/Multicall.t.sol @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.20; + +import "forge-std/Test.sol"; +import {MockMulticall} from "./mocks/MockMulticall.sol"; + +contract MulticallTest is Test { + MockMulticall multicall; + + function setUp() public { + multicall = new MockMulticall(); + } + + function test_multicall() public { + bytes[] memory calls = new bytes[](2); + calls[0] = abi.encodeWithSelector(MockMulticall(multicall).functionThatReturnsTuple.selector, 10, 20); + calls[1] = abi.encodeWithSelector(MockMulticall(multicall).functionThatReturnsTuple.selector, 1, 2); + + bytes[] memory results = multicall.multicall(calls); + + (uint256 a, uint256 b) = abi.decode(results[0], (uint256, uint256)); + assertEq(a, 10); + assertEq(b, 20); + + (a, b) = abi.decode(results[1], (uint256, uint256)); + assertEq(a, 1); + assertEq(b, 2); + } + + function test_multicall_firstRevert() public { + bytes[] memory calls = new bytes[](2); + calls[0] = + abi.encodeWithSelector(MockMulticall(multicall).functionThatRevertsWithError.selector, "First call failed"); + calls[1] = abi.encodeWithSelector(MockMulticall(multicall).functionThatReturnsTuple.selector, 1, 2); + + vm.expectRevert("First call failed"); + multicall.multicall(calls); + } + + function test_multicall_secondRevert() public { + bytes[] memory calls = new bytes[](2); + calls[0] = abi.encodeWithSelector(MockMulticall(multicall).functionThatReturnsTuple.selector, 1, 2); + calls[1] = + abi.encodeWithSelector(MockMulticall(multicall).functionThatRevertsWithError.selector, "Second call failed"); + + vm.expectRevert("Second call failed"); + multicall.multicall(calls); + } + + function test_multicall_payableStoresMsgValue() public { + assertEq(address(multicall).balance, 0); + bytes[] memory calls = new bytes[](1); + calls[0] = abi.encodeWithSelector(MockMulticall(multicall).payableStoresMsgValue.selector); + multicall.multicall{value: 100}(calls); + assertEq(address(multicall).balance, 100); + assertEq(multicall.msgValue(), 100); + } + + function test_multicall_returnSender() public { + bytes[] memory calls = new bytes[](1); + calls[0] = abi.encodeWithSelector(MockMulticall(multicall).returnSender.selector); + bytes[] memory results = multicall.multicall(calls); + address sender = abi.decode(results[0], (address)); + assertEq(sender, address(this)); + } + + function test_multicall_returnSender_prank() public { + address alice = makeAddr("ALICE"); + + bytes[] memory calls = new bytes[](1); + calls[0] = abi.encodeWithSelector(MockMulticall(multicall).returnSender.selector, alice); + vm.prank(alice); + bytes[] memory results = multicall.multicall(calls); + address sender = abi.decode(results[0], (address)); + assertEq(sender, alice); + } + + function test_multicall_double_send() public { + bytes[] memory calls = new bytes[](2); + calls[0] = abi.encodeWithSelector(MockMulticall(multicall).payableStoresMsgValue.selector); + calls[1] = abi.encodeWithSelector(MockMulticall(multicall).payableStoresMsgValue.selector); + + multicall.multicall{value: 100}(calls); + assertEq(address(multicall).balance, 100); + assertEq(multicall.msgValue(), 100); + } + + function test_multicall_unpayableRevert() public { + // first call is payable, second is not which causes a revert + bytes[] memory calls = new bytes[](2); + calls[0] = abi.encodeWithSelector(MockMulticall(multicall).payableStoresMsgValue.selector); + calls[1] = abi.encodeWithSelector(MockMulticall(multicall).functionThatReturnsTuple.selector, 10, 20); + + vm.expectRevert(); + multicall.multicall{value: 100}(calls); + } + + function test_multicall_bothPayable() public { + // msg.value is provided to both calls + bytes[] memory calls = new bytes[](2); + calls[0] = abi.encodeWithSelector(MockMulticall(multicall).payableStoresMsgValue.selector); + calls[1] = abi.encodeWithSelector(MockMulticall(multicall).payableStoresMsgValueDouble.selector); + + multicall.multicall{value: 100}(calls); + assertEq(address(multicall).balance, 100); + assertEq(multicall.msgValue(), 100); + assertEq(multicall.msgValueDouble(), 200); + } +} diff --git a/test/Oracle.t.sol b/test/Oracle.t.sol deleted file mode 100644 index 04157e16..00000000 --- a/test/Oracle.t.sol +++ /dev/null @@ -1,867 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; - -import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; -import {Test} from "forge-std/Test.sol"; -import {Oracle} from "../contracts/libraries/Oracle.sol"; -import {OracleImplementation} from "./shared/implementation/OracleImplementation.sol"; - -contract TestOracle is Test, GasSnapshot { - OracleImplementation initializedOracle; - OracleImplementation oracle; - - function setUp() public { - oracle = new OracleImplementation(); - initializedOracle = new OracleImplementation(); - initializedOracle.initialize(OracleImplementation.InitializeParams({time: 0, tick: 0, liquidity: 0})); - } - - function testInitialize() public { - snapStart("OracleInitialize"); - oracle.initialize(OracleImplementation.InitializeParams({time: 1, tick: 1, liquidity: 1})); - snapEnd(); - - assertEq(oracle.index(), 0); - assertEq(oracle.cardinality(), 1); - assertEq(oracle.cardinalityNext(), 1); - assertObservation( - oracle, - 0, - Oracle.Observation({ - blockTimestamp: 1, - tickCumulative: 0, - secondsPerLiquidityCumulativeX128: 0, - initialized: true - }) - ); - } - - function testGrow() public { - initializedOracle.grow(5); - assertEq(initializedOracle.index(), 0); - assertEq(initializedOracle.cardinality(), 1); - assertEq(initializedOracle.cardinalityNext(), 5); - - // does not touch first slot - assertObservation( - initializedOracle, - 0, - Oracle.Observation({ - blockTimestamp: 0, - tickCumulative: 0, - secondsPerLiquidityCumulativeX128: 0, - initialized: true - }) - ); - - // adds data to all slots - for (uint64 i = 1; i < 5; i++) { - assertObservation( - initializedOracle, - i, - Oracle.Observation({ - blockTimestamp: 1, - tickCumulative: 0, - secondsPerLiquidityCumulativeX128: 0, - initialized: false - }) - ); - } - - // noop if initializedOracle is already gte size - initializedOracle.grow(3); - assertEq(initializedOracle.index(), 0); - assertEq(initializedOracle.cardinality(), 1); - assertEq(initializedOracle.cardinalityNext(), 5); - } - - function testGrowAfterWrap() public { - initializedOracle.grow(2); - // index is now 1 - initializedOracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 2, liquidity: 1, tick: 1})); - // index is now 0 again - initializedOracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 2, liquidity: 1, tick: 1})); - assertEq(initializedOracle.index(), 0); - initializedOracle.grow(3); - - assertEq(initializedOracle.index(), 0); - assertEq(initializedOracle.cardinality(), 2); - assertEq(initializedOracle.cardinalityNext(), 3); - } - - function testGas1Slot() public { - snapStart("OracleGrow1Slot"); - initializedOracle.grow(2); - snapEnd(); - } - - function testGas10Slots() public { - snapStart("OracleGrow10Slots"); - initializedOracle.grow(11); - snapEnd(); - } - - function testGas1SlotCardinalityGreater() public { - initializedOracle.grow(2); - snapStart("OracleGrow1SlotCardinalityGreater"); - initializedOracle.grow(3); - snapEnd(); - } - - function testGas10SlotCardinalityGreater() public { - initializedOracle.grow(2); - snapStart("OracleGrow10SlotsCardinalityGreater"); - initializedOracle.grow(12); - snapEnd(); - } - - function testWrite() public { - initializedOracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 1, tick: 2, liquidity: 5})); - assertEq(initializedOracle.index(), 0); - assertObservation( - initializedOracle, - 0, - Oracle.Observation({ - blockTimestamp: 1, - tickCumulative: 0, - secondsPerLiquidityCumulativeX128: 340282366920938463463374607431768211456, - initialized: true - }) - ); - - initializedOracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 5, tick: -1, liquidity: 8})); - assertEq(initializedOracle.index(), 0); - assertObservation( - initializedOracle, - 0, - Oracle.Observation({ - blockTimestamp: 6, - tickCumulative: 10, - secondsPerLiquidityCumulativeX128: 680564733841876926926749214863536422912, - initialized: true - }) - ); - - initializedOracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 3, tick: 2, liquidity: 3})); - assertEq(initializedOracle.index(), 0); - assertObservation( - initializedOracle, - 0, - Oracle.Observation({ - blockTimestamp: 9, - tickCumulative: 7, - secondsPerLiquidityCumulativeX128: 808170621437228850725514692650449502208, - initialized: true - }) - ); - } - - function testWriteAddsNothingIfTimeUnchanged() public { - initializedOracle.grow(2); - initializedOracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 1, tick: 3, liquidity: 2})); - assertEq(initializedOracle.index(), 1); - initializedOracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 0, tick: -5, liquidity: 9})); - assertEq(initializedOracle.index(), 1); - } - - function testWriteTimeChanged() public { - initializedOracle.grow(3); - initializedOracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 6, tick: 3, liquidity: 2})); - assertEq(initializedOracle.index(), 1); - initializedOracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 4, tick: -5, liquidity: 9})); - assertEq(initializedOracle.index(), 2); - assertObservation( - initializedOracle, - 1, - Oracle.Observation({ - blockTimestamp: 6, - tickCumulative: 0, - secondsPerLiquidityCumulativeX128: 2041694201525630780780247644590609268736, - initialized: true - }) - ); - } - - function testWriteGrowsCardinalityWritingPast() public { - initializedOracle.grow(2); - initializedOracle.grow(4); - assertEq(initializedOracle.cardinality(), 1); - initializedOracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 3, tick: 5, liquidity: 6})); - assertEq(initializedOracle.cardinality(), 4); - initializedOracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 4, tick: 6, liquidity: 4})); - assertEq(initializedOracle.cardinality(), 4); - assertEq(initializedOracle.index(), 2); - assertObservation( - initializedOracle, - 2, - Oracle.Observation({ - blockTimestamp: 7, - tickCumulative: 20, - secondsPerLiquidityCumulativeX128: 1247702012043441032699040227249816775338, - initialized: true - }) - ); - } - - function testWriteWrapsAround() public { - initializedOracle.grow(3); - initializedOracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 3, tick: 1, liquidity: 2})); - - initializedOracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 4, tick: 2, liquidity: 3})); - - initializedOracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 5, tick: 3, liquidity: 4})); - - assertEq(initializedOracle.index(), 0); - assertObservation( - initializedOracle, - 0, - Oracle.Observation({ - blockTimestamp: 12, - tickCumulative: 14, - secondsPerLiquidityCumulativeX128: 2268549112806256423089164049545121409706, - initialized: true - }) - ); - } - - function testWriteAccumulatesLiquidity() public { - initializedOracle.grow(4); - initializedOracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 3, tick: 3, liquidity: 2})); - - initializedOracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 4, tick: -7, liquidity: 6})); - - initializedOracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 5, tick: -2, liquidity: 4})); - - assertEq(initializedOracle.index(), 3); - - assertObservation( - initializedOracle, - 1, - Oracle.Observation({ - blockTimestamp: 3, - tickCumulative: 0, - secondsPerLiquidityCumulativeX128: 1020847100762815390390123822295304634368, - initialized: true - }) - ); - assertObservation( - initializedOracle, - 2, - Oracle.Observation({ - blockTimestamp: 7, - tickCumulative: 12, - secondsPerLiquidityCumulativeX128: 1701411834604692317316873037158841057280, - initialized: true - }) - ); - assertObservation( - initializedOracle, - 3, - Oracle.Observation({ - blockTimestamp: 12, - tickCumulative: -23, - secondsPerLiquidityCumulativeX128: 1984980473705474370203018543351981233493, - initialized: true - }) - ); - assertObservation( - initializedOracle, - 4, - Oracle.Observation({ - blockTimestamp: 0, - tickCumulative: 0, - secondsPerLiquidityCumulativeX128: 0, - initialized: false - }) - ); - } - - function testObserveFailsBeforeInitialize() public { - uint32[] memory secondsAgos = new uint32[](1); - secondsAgos[0] = 0; - vm.expectRevert(Oracle.OracleCardinalityCannotBeZero.selector); - oracle.observe(secondsAgos); - } - - function testObserveFailsIfOlderDoesNotExist() public { - oracle.initialize(OracleImplementation.InitializeParams({liquidity: 4, tick: 2, time: 5})); - uint32[] memory secondsAgos = new uint32[](1); - secondsAgos[0] = 1; - vm.expectRevert(abi.encodeWithSelector(Oracle.TargetPredatesOldestObservation.selector, 5, 4)); - oracle.observe(secondsAgos); - } - - function testDoesNotFailAcrossOverflowBoundary() public { - oracle.initialize(OracleImplementation.InitializeParams({liquidity: 4, tick: 2, time: 2 ** 32 - 1})); - oracle.advanceTime(2); - (int56 tickCumulative, uint160 secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 1); - assertEq(tickCumulative, 2); - assertEq(secondsPerLiquidityCumulativeX128, 85070591730234615865843651857942052864); - } - - function testInterpolationMaxLiquidity() public { - oracle.initialize(OracleImplementation.InitializeParams({liquidity: type(uint128).max, tick: 0, time: 0})); - oracle.grow(2); - oracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 13, tick: 0, liquidity: 0})); - (, uint160 secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 0); - assertEq(secondsPerLiquidityCumulativeX128, 13); - (, secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 6); - assertEq(secondsPerLiquidityCumulativeX128, 7); - (, secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 12); - assertEq(secondsPerLiquidityCumulativeX128, 1); - (, secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 13); - assertEq(secondsPerLiquidityCumulativeX128, 0); - } - - function testInterpolatesSame0And1Liquidity() public { - oracle.initialize(OracleImplementation.InitializeParams({liquidity: 1, tick: 0, time: 0})); - oracle.grow(2); - oracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 13, tick: 0, liquidity: type(uint128).max})); - (, uint160 secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 0); - assertEq(secondsPerLiquidityCumulativeX128, 13 << 128); - (, secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 6); - assertEq(secondsPerLiquidityCumulativeX128, 7 << 128); - (, secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 12); - assertEq(secondsPerLiquidityCumulativeX128, 1 << 128); - (, secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 13); - assertEq(secondsPerLiquidityCumulativeX128, 0); - } - - function testInterpolatesAcrossChunkBoundaries() public { - oracle.initialize(OracleImplementation.InitializeParams({liquidity: 0, tick: 0, time: 0})); - oracle.grow(2); - oracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 2 ** 32 - 6, tick: 0, liquidity: 0})); - (, uint160 secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 0); - assertEq(secondsPerLiquidityCumulativeX128, (2 ** 32 - 6) << 128); - oracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 13, tick: 0, liquidity: 0})); - (, secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 0); - assertEq(secondsPerLiquidityCumulativeX128, 7 << 128); - - // interpolation checks - (, secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 3); - assertEq(secondsPerLiquidityCumulativeX128, 4 << 128); - (, secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 8); - assertEq(secondsPerLiquidityCumulativeX128, (2 ** 32 - 1) << 128); - } - - function testSingleObservationAtCurrentTime() public { - oracle.initialize(OracleImplementation.InitializeParams({liquidity: 4, tick: 2, time: 5})); - (int56 tickCumulative, uint160 secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 0); - assertEq(tickCumulative, 0); - assertEq(secondsPerLiquidityCumulativeX128, 0); - } - - function testSingleObservationInRecentPast() public { - oracle.initialize(OracleImplementation.InitializeParams({liquidity: 4, tick: 2, time: 5})); - oracle.advanceTime(3); - uint32[] memory secondsAgos = new uint32[](1); - secondsAgos[0] = 4; - vm.expectRevert(abi.encodeWithSelector(Oracle.TargetPredatesOldestObservation.selector, 5, 4)); - oracle.observe(secondsAgos); - } - - function testSingleObservationSecondsAgo() public { - oracle.initialize(OracleImplementation.InitializeParams({liquidity: 4, tick: 2, time: 5})); - oracle.advanceTime(3); - (int56 tickCumulative, uint160 secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 3); - assertEq(tickCumulative, 0); - assertEq(secondsPerLiquidityCumulativeX128, 0); - } - - function testSingleObservationInPastCounterfactualInPast() public { - oracle.initialize(OracleImplementation.InitializeParams({liquidity: 4, tick: 2, time: 5})); - oracle.advanceTime(3); - (int56 tickCumulative, uint160 secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 1); - assertEq(tickCumulative, 4); - assertEq(secondsPerLiquidityCumulativeX128, 170141183460469231731687303715884105728); - } - - function testSingleObservationInPastCounterfactualNow() public { - oracle.initialize(OracleImplementation.InitializeParams({liquidity: 4, tick: 2, time: 5})); - oracle.advanceTime(3); - (int56 tickCumulative, uint160 secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 0); - assertEq(tickCumulative, 6); - assertEq(secondsPerLiquidityCumulativeX128, 255211775190703847597530955573826158592); - } - - function testTwoObservationsChronologicalZeroSecondsAgoExact() public { - oracle.initialize(OracleImplementation.InitializeParams({liquidity: 5, tick: -5, time: 5})); - oracle.grow(2); - oracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 4, tick: 1, liquidity: 2})); - (int56 tickCumulative, uint160 secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 0); - assertEq(tickCumulative, -20); - assertEq(secondsPerLiquidityCumulativeX128, 272225893536750770770699685945414569164); - } - - function testTwoObservationsChronologicalZeroSecondsAgoCounterfactual() public { - oracle.initialize(OracleImplementation.InitializeParams({liquidity: 5, tick: -5, time: 5})); - oracle.grow(2); - oracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 4, tick: 1, liquidity: 2})); - oracle.advanceTime(7); - (int56 tickCumulative, uint160 secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 0); - assertEq(tickCumulative, -13); - assertEq(secondsPerLiquidityCumulativeX128, 1463214177760035392892510811956603309260); - } - - function testTwoObservationsChronologicalSecondsAgoExactlyFirstObservation() public { - oracle.initialize(OracleImplementation.InitializeParams({liquidity: 5, tick: -5, time: 5})); - oracle.grow(2); - oracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 4, tick: 1, liquidity: 2})); - oracle.advanceTime(7); - (int56 tickCumulative, uint160 secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 11); - assertEq(tickCumulative, 0); - assertEq(secondsPerLiquidityCumulativeX128, 0); - } - - function testTwoObservationsChronologicalSecondsAgoBetween() public { - oracle.initialize(OracleImplementation.InitializeParams({liquidity: 5, tick: -5, time: 5})); - oracle.grow(2); - oracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 4, tick: 1, liquidity: 2})); - oracle.advanceTime(7); - (int56 tickCumulative, uint160 secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 9); - assertEq(tickCumulative, -10); - assertEq(secondsPerLiquidityCumulativeX128, 136112946768375385385349842972707284582); - } - - function testTwoObservationsReverseOrderZeroSecondsAgoExact() public { - oracle.initialize(OracleImplementation.InitializeParams({liquidity: 5, tick: -5, time: 5})); - oracle.grow(2); - oracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 4, tick: 1, liquidity: 2})); - oracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 3, tick: -5, liquidity: 4})); - (int56 tickCumulative, uint160 secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 0); - assertEq(tickCumulative, -17); - assertEq(secondsPerLiquidityCumulativeX128, 782649443918158465965761597093066886348); - } - - function testTwoObservationsReverseOrderZeroSecondsAgoCounterfactual() public { - oracle.initialize(OracleImplementation.InitializeParams({liquidity: 5, tick: -5, time: 5})); - oracle.grow(2); - oracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 4, tick: 1, liquidity: 2})); - oracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 3, tick: -5, liquidity: 4})); - oracle.advanceTime(7); - (int56 tickCumulative, uint160 secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 0); - assertEq(tickCumulative, -52); - assertEq(secondsPerLiquidityCumulativeX128, 1378143586029800777026667160098661256396); - } - - function testTwoObservationsReverseOrderSecondsAgoExactlyOnFirstObservation() public { - oracle.initialize(OracleImplementation.InitializeParams({liquidity: 5, tick: -5, time: 5})); - oracle.grow(2); - oracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 4, tick: 1, liquidity: 2})); - oracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 3, tick: -5, liquidity: 4})); - oracle.advanceTime(7); - (int56 tickCumulative, uint160 secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 10); - assertEq(tickCumulative, -20); - assertEq(secondsPerLiquidityCumulativeX128, 272225893536750770770699685945414569164); - } - - function testTwoObservationsReverseOrderSecondsAgoBetween() public { - oracle.initialize(OracleImplementation.InitializeParams({liquidity: 5, tick: -5, time: 5})); - oracle.grow(2); - oracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 4, tick: 1, liquidity: 2})); - oracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 3, tick: -5, liquidity: 4})); - oracle.advanceTime(7); - (int56 tickCumulative, uint160 secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 9); - assertEq(tickCumulative, -19); - assertEq(secondsPerLiquidityCumulativeX128, 442367076997220002502386989661298674892); - } - - function testCanFetchMultipleObservations() public { - oracle.initialize(OracleImplementation.InitializeParams({liquidity: 2 ** 15, tick: 2, time: 5})); - oracle.grow(4); - oracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 13, tick: 6, liquidity: 2 ** 12})); - oracle.advanceTime(5); - uint32[] memory secondsAgos = new uint32[](6); - secondsAgos[0] = 0; - secondsAgos[1] = 3; - secondsAgos[2] = 8; - secondsAgos[3] = 13; - secondsAgos[4] = 15; - secondsAgos[5] = 18; - (int56[] memory tickCumulatives, uint160[] memory secondsPerLiquidityCumulativeX128s) = - oracle.observe(secondsAgos); - assertEq(tickCumulatives.length, 6); - assertEq(tickCumulatives[0], 56); - assertEq(tickCumulatives[1], 38); - assertEq(tickCumulatives[2], 20); - assertEq(tickCumulatives[3], 10); - assertEq(tickCumulatives[4], 6); - assertEq(tickCumulatives[5], 0); - assertEq(secondsPerLiquidityCumulativeX128s.length, 6); - assertEq(secondsPerLiquidityCumulativeX128s[0], 550383467004691728624232610897330176); - assertEq(secondsPerLiquidityCumulativeX128s[1], 301153217795020002454768787094765568); - assertEq(secondsPerLiquidityCumulativeX128s[2], 103845937170696552570609926584401920); - assertEq(secondsPerLiquidityCumulativeX128s[3], 51922968585348276285304963292200960); - assertEq(secondsPerLiquidityCumulativeX128s[4], 31153781151208965771182977975320576); - assertEq(secondsPerLiquidityCumulativeX128s[5], 0); - } - - function testObserveGasSinceMostRecent() public { - oracle.initialize(OracleImplementation.InitializeParams({liquidity: 5, tick: -5, time: 5})); - oracle.advanceTime(2); - uint32[] memory secondsAgos = new uint32[](1); - secondsAgos[0] = 1; - snap("OracleObserveSinceMostRecent", oracle.getGasCostOfObserve(secondsAgos)); - } - - function testObserveGasCurrentTime() public { - oracle.initialize(OracleImplementation.InitializeParams({liquidity: 5, tick: -5, time: 5})); - uint32[] memory secondsAgos = new uint32[](1); - secondsAgos[0] = 0; - snap("OracleObserveCurrentTime", oracle.getGasCostOfObserve(secondsAgos)); - } - - function testObserveGasCurrentTimeCounterfactual() public { - oracle.initialize(OracleImplementation.InitializeParams({liquidity: 5, tick: -5, time: 5})); - initializedOracle.advanceTime(5); - uint32[] memory secondsAgos = new uint32[](1); - secondsAgos[0] = 0; - snap("OracleObserveCurrentTimeCounterfactual", oracle.getGasCostOfObserve(secondsAgos)); - } - - function testManyObservationsSimpleReads(uint32 startingTime) public { - setupOracleWithManyObservations(startingTime); - - assertEq(oracle.index(), 1); - assertEq(oracle.cardinality(), 5); - assertEq(oracle.cardinalityNext(), 5); - } - - function testManyObservationsLatestObservationSameTimeAsLatest(uint32 startingTime) public { - setupOracleWithManyObservations(startingTime); - - (int56 tickCumulative, uint160 secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 0); - assertEq(tickCumulative, -21); - assertEq(secondsPerLiquidityCumulativeX128, 2104079302127802832415199655953100107502); - } - - function testManyObservationsLatestObservation5SecondsAfterLatest(uint32 startingTime) public { - setupOracleWithManyObservations(startingTime); - - // latest observation 5 seconds after latest - oracle.advanceTime(5); - (int56 tickCumulative, uint160 secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 5); - assertEq(tickCumulative, -21); - assertEq(secondsPerLiquidityCumulativeX128, 2104079302127802832415199655953100107502); - } - - function testManyObservationsCurrentObservation5SecondsAfterLatest(uint32 startingTime) public { - setupOracleWithManyObservations(startingTime); - - oracle.advanceTime(5); - (int56 tickCumulative, uint160 secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 0); - assertEq(tickCumulative, 9); - assertEq(secondsPerLiquidityCumulativeX128, 2347138135642758877746181518404363115684); - } - - function testManyObservationsBetweenLatestObservationAtLatest(uint32 startingTime) public { - setupOracleWithManyObservations(startingTime); - - (int56 tickCumulative, uint160 secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 3); - assertEq(tickCumulative, -33); - assertEq(secondsPerLiquidityCumulativeX128, 1593655751746395137220137744805447790318); - } - - function testManyObservationsBetweenLatestObservationAfterLatest(uint32 startingTime) public { - setupOracleWithManyObservations(startingTime); - - oracle.advanceTime(5); - (int56 tickCumulative, uint160 secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 8); - assertEq(tickCumulative, -33); - assertEq(secondsPerLiquidityCumulativeX128, 1593655751746395137220137744805447790318); - } - - function testManyObservationsOlderThanOldestReverts(uint32 startingTime) public { - setupOracleWithManyObservations(startingTime); - - (uint32 oldestTimestamp,,,) = oracle.observations(oracle.index() + 1); - uint32 secondsAgo = 15; - // overflow desired here - uint32 target; - unchecked { - target = oracle.time() - secondsAgo; - } - - uint32[] memory secondsAgos = new uint32[](1); - secondsAgos[0] = secondsAgo; - vm.expectRevert( - abi.encodeWithSelector( - Oracle.TargetPredatesOldestObservation.selector, oldestTimestamp, uint32(int32(target)) - ) - ); - oracle.observe(secondsAgos); - - oracle.advanceTime(5); - - secondsAgos[0] = 20; - vm.expectRevert( - abi.encodeWithSelector( - Oracle.TargetPredatesOldestObservation.selector, oldestTimestamp, uint32(int32(target)) - ) - ); - oracle.observe(secondsAgos); - } - - function testManyObservationsOldest(uint32 startingTime) public { - setupOracleWithManyObservations(startingTime); - (int56 tickCumulative, uint160 secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 14); - assertEq(tickCumulative, -13); - assertEq(secondsPerLiquidityCumulativeX128, 544451787073501541541399371890829138329); - } - - function testManyObservationsOldestAfterTime(uint32 startingTime) public { - setupOracleWithManyObservations(startingTime); - oracle.advanceTime(6); - (int56 tickCumulative, uint160 secondsPerLiquidityCumulativeX128) = observeSingle(oracle, 20); - assertEq(tickCumulative, -13); - assertEq(secondsPerLiquidityCumulativeX128, 544451787073501541541399371890829138329); - } - - function testManyObservationsFetchManyValues(uint32 startingTime) public { - setupOracleWithManyObservations(startingTime); - oracle.advanceTime(6); - uint32[] memory secondsAgos = new uint32[](7); - secondsAgos[0] = 20; - secondsAgos[1] = 17; - secondsAgos[2] = 13; - secondsAgos[3] = 10; - secondsAgos[4] = 5; - secondsAgos[5] = 1; - secondsAgos[6] = 0; - (int56[] memory tickCumulatives, uint160[] memory secondsPerLiquidityCumulativeX128s) = - oracle.observe(secondsAgos); - assertEq(tickCumulatives[0], -13); - assertEq(secondsPerLiquidityCumulativeX128s[0], 544451787073501541541399371890829138329); - assertEq(tickCumulatives[1], -31); - assertEq(secondsPerLiquidityCumulativeX128s[1], 799663562264205389138930327464655296921); - assertEq(tickCumulatives[2], -43); - assertEq(secondsPerLiquidityCumulativeX128s[2], 1045423049484883168306923099498710116305); - assertEq(tickCumulatives[3], -37); - assertEq(secondsPerLiquidityCumulativeX128s[3], 1423514568285925905488450441089563684590); - assertEq(tickCumulatives[4], -15); - assertEq(secondsPerLiquidityCumulativeX128s[4], 2152691068830794041481396028443352709138); - assertEq(tickCumulatives[5], 9); - assertEq(secondsPerLiquidityCumulativeX128s[5], 2347138135642758877746181518404363115684); - assertEq(tickCumulatives[6], 15); - assertEq(secondsPerLiquidityCumulativeX128s[6], 2395749902345750086812377890894615717321); - } - - function testGasAllOfLast20Seconds() public { - setupOracleWithManyObservations(5); - oracle.advanceTime(6); - uint32[] memory secondsAgos = new uint32[](20); - for (uint32 i = 0; i < 20; i++) { - secondsAgos[i] = 20 - i; - } - snap("OracleObserveLast20Seconds", oracle.getGasCostOfObserve(secondsAgos)); - } - - function testGasLatestEqual() public { - setupOracleWithManyObservations(5); - uint32[] memory secondsAgos = new uint32[](1); - secondsAgos[0] = 0; - snap("OracleObserveLatestEqual", oracle.getGasCostOfObserve(secondsAgos)); - } - - function testGasLatestTransform() public { - setupOracleWithManyObservations(5); - oracle.advanceTime(5); - uint32[] memory secondsAgos = new uint32[](1); - secondsAgos[0] = 0; - snap("OracleObserveLatestTransform", oracle.getGasCostOfObserve(secondsAgos)); - } - - function testGasOldest() public { - setupOracleWithManyObservations(5); - uint32[] memory secondsAgos = new uint32[](1); - secondsAgos[0] = 14; - snap("OracleObserveOldest", oracle.getGasCostOfObserve(secondsAgos)); - } - - function testGasBetweenOldestAndOldestPlusOne() public { - setupOracleWithManyObservations(5); - uint32[] memory secondsAgos = new uint32[](1); - secondsAgos[0] = 13; - snap("OracleObserveBetweenOldestAndOldestPlusOne", oracle.getGasCostOfObserve(secondsAgos)); - } - - function testGasMiddle() public { - setupOracleWithManyObservations(5); - uint32[] memory secondsAgos = new uint32[](1); - secondsAgos[0] = 5; - snap("OracleObserveMiddle", oracle.getGasCostOfObserve(secondsAgos)); - } - - function testFullOracle() public { - setupFullOracle(); - - assertEq(oracle.cardinalityNext(), 65535); - assertEq(oracle.cardinality(), 65535); - assertEq(oracle.index(), 165); - - // can observe into the ordered portion with exact seconds ago - (int56 tickCumulative, uint160 secondsPerLiquidityCumulative) = observeSingle(oracle, 100 * 13); - assertEq(tickCumulative, -27970560813); - assertEq(secondsPerLiquidityCumulative, 60465049086512033878831623038233202591033); - - // can observe into the ordered portion with unexact seconds ago - (tickCumulative, secondsPerLiquidityCumulative) = observeSingle(oracle, 100 * 13 + 5); - assertEq(tickCumulative, -27970232823); - assertEq(secondsPerLiquidityCumulative, 60465023149565257990964350912969670793706); - - // can observe at exactly the latest observation - (tickCumulative, secondsPerLiquidityCumulative) = observeSingle(oracle, 0); - assertEq(tickCumulative, -28055903863); - assertEq(secondsPerLiquidityCumulative, 60471787506468701386237800669810720099776); - - // can observe into the unordered portion of array at exact seconds ago - (tickCumulative, secondsPerLiquidityCumulative) = observeSingle(oracle, 200 * 13); - assertEq(tickCumulative, -27885347763); - assertEq(secondsPerLiquidityCumulative, 60458300386499273141628780395875293027404); - - // can observe into the unordered portion of array at seconds ago between observations - (tickCumulative, secondsPerLiquidityCumulative) = observeSingle(oracle, 200 * 13 + 5); - assertEq(tickCumulative, -27885020273); - assertEq(secondsPerLiquidityCumulative, 60458274409952896081377821330361274907140); - - // can observe the oldest observation - (tickCumulative, secondsPerLiquidityCumulative) = observeSingle(oracle, 13 * 65534); - assertEq(tickCumulative, -175890); - assertEq(secondsPerLiquidityCumulative, 33974356747348039873972993881117400879779); - - // can observe at exactly the latest observation after some time passes - oracle.advanceTime(5); - (tickCumulative, secondsPerLiquidityCumulative) = observeSingle(oracle, 5); - assertEq(tickCumulative, -28055903863); - assertEq(secondsPerLiquidityCumulative, 60471787506468701386237800669810720099776); - - // can observe after the latest observation counterfactual - (tickCumulative, secondsPerLiquidityCumulative) = observeSingle(oracle, 3); - assertEq(tickCumulative, -28056035261); - assertEq(secondsPerLiquidityCumulative, 60471797865298117996489508104462919730461); - - // can observe the oldest observation after time passes - (tickCumulative, secondsPerLiquidityCumulative) = observeSingle(oracle, 13 * 65534 + 5); - assertEq(tickCumulative, -175890); - assertEq(secondsPerLiquidityCumulative, 33974356747348039873972993881117400879779); - } - - function testFullOracleGasCostObserveZero() public { - setupFullOracle(); - uint32[] memory secondsAgos = new uint32[](1); - secondsAgos[0] = 0; - snap("FullOracleObserveZero", oracle.getGasCostOfObserve(secondsAgos)); - } - - function testFullOracleGasCostObserve200By13() public { - setupFullOracle(); - uint32[] memory secondsAgos = new uint32[](1); - secondsAgos[0] = 200 * 13; - snap("FullOracleObserve200By13", oracle.getGasCostOfObserve(secondsAgos)); - } - - function testFullOracleGasCostObserve200By13Plus5() public { - setupFullOracle(); - uint32[] memory secondsAgos = new uint32[](1); - secondsAgos[0] = 200 * 13 + 5; - snap("FullOracleObserve200By13Plus5", oracle.getGasCostOfObserve(secondsAgos)); - } - - function testFullOracleGasCostObserve0After5Seconds() public { - setupFullOracle(); - oracle.advanceTime(5); - uint32[] memory secondsAgos = new uint32[](1); - secondsAgos[0] = 0; - snap("FullOracleObserve0After5Seconds", oracle.getGasCostOfObserve(secondsAgos)); - } - - function testFullOracleGasCostObserve5After5Seconds() public { - setupFullOracle(); - oracle.advanceTime(5); - uint32[] memory secondsAgos = new uint32[](1); - secondsAgos[0] = 5; - snap("FullOracleObserve5After5Seconds", oracle.getGasCostOfObserve(secondsAgos)); - } - - function testFullOracleGasCostObserveOldest() public { - setupFullOracle(); - uint32[] memory secondsAgos = new uint32[](1); - secondsAgos[0] = 13 * 65534; - snap("FullOracleObserveOldest", oracle.getGasCostOfObserve(secondsAgos)); - } - - function testFullOracleGasCostObserveOldestAfter5Seconds() public { - setupFullOracle(); - oracle.advanceTime(5); - uint32[] memory secondsAgos = new uint32[](1); - secondsAgos[0] = 13 * 65534; - snap("FullOracleObserveOldestAfter5Seconds", oracle.getGasCostOfObserve(secondsAgos)); - } - - // fixtures and helpers - - function observeSingle(OracleImplementation _initializedOracle, uint32 secondsAgo) - internal - view - returns (int56 tickCumulative, uint160 secondsPerLiquidityCumulative) - { - uint32[] memory secondsAgos = new uint32[](1); - secondsAgos[0] = secondsAgo; - (int56[] memory tickCumulatives, uint160[] memory secondsPerLiquidityCumulatives) = - _initializedOracle.observe(secondsAgos); - return (tickCumulatives[0], secondsPerLiquidityCumulatives[0]); - } - - function assertObservation(OracleImplementation _initializedOracle, uint64 idx, Oracle.Observation memory expected) - internal - { - (uint32 blockTimestamp, int56 tickCumulative, uint160 secondsPerLiquidityCumulativeX128, bool initialized) = - _initializedOracle.observations(idx); - assertEq(blockTimestamp, expected.blockTimestamp); - assertEq(tickCumulative, expected.tickCumulative); - assertEq(secondsPerLiquidityCumulativeX128, expected.secondsPerLiquidityCumulativeX128); - assertEq(initialized, expected.initialized); - } - - function setupOracleWithManyObservations(uint32 startingTime) internal { - oracle.initialize(OracleImplementation.InitializeParams({liquidity: 5, tick: -5, time: startingTime})); - oracle.grow(5); - oracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 3, tick: 1, liquidity: 2})); - oracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 2, tick: -6, liquidity: 4})); - oracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 4, tick: -2, liquidity: 4})); - oracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 1, tick: -2, liquidity: 9})); - oracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 3, tick: 4, liquidity: 2})); - oracle.update(OracleImplementation.UpdateParams({advanceTimeBy: 6, tick: 6, liquidity: 7})); - } - - function setupFullOracle() internal { - uint16 BATCH_SIZE = 300; - oracle.initialize( - OracleImplementation.InitializeParams({ - liquidity: 0, - tick: 0, - // Monday, October 5, 2020 9:00:00 AM GMT-05:00 - time: 1601906400 - }) - ); - - uint16 cardinalityNext = oracle.cardinalityNext(); - while (cardinalityNext < 65535) { - uint16 growTo = cardinalityNext + BATCH_SIZE < 65535 ? 65535 : cardinalityNext + BATCH_SIZE; - oracle.grow(growTo); - cardinalityNext = growTo; - } - - for (int24 i = 0; i < 65535; i += int24(uint24(BATCH_SIZE))) { - OracleImplementation.UpdateParams[] memory batch = new OracleImplementation.UpdateParams[](BATCH_SIZE); - for (int24 j = 0; j < int24(uint24(BATCH_SIZE)); j++) { - batch[uint24(j)] = OracleImplementation.UpdateParams({ - advanceTimeBy: 13, - tick: -i - j, - liquidity: uint128(int128(i) + int128(j)) - }); - } - oracle.batchUpdate(batch); - } - } -} diff --git a/test/Quoter.t.sol b/test/Quoter.t.sol index 0767cadd..b01d9c88 100644 --- a/test/Quoter.t.sol +++ b/test/Quoter.t.sol @@ -3,24 +3,26 @@ pragma solidity ^0.8.20; import {Test} from "forge-std/Test.sol"; -import {PathKey} from "../contracts/libraries/PathKey.sol"; -import {IQuoter} from "../contracts/interfaces/IQuoter.sol"; -import {Quoter} from "../contracts/lens/Quoter.sol"; -import {LiquidityAmounts} from "../contracts/libraries/LiquidityAmounts.sol"; -import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol"; -import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {PathKey} from "../src/libraries/PathKey.sol"; +import {IQuoter} from "../src/interfaces/IQuoter.sol"; +import {Quoter} from "../src/lens/Quoter.sol"; + +// v4-core +import {LiquidityAmounts} from "@uniswap/v4-core/test/utils/LiquidityAmounts.sol"; import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; import {PoolModifyLiquidityTest} from "@uniswap/v4-core/src/test/PoolModifyLiquidityTest.sol"; import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; -import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; +// solmate +import {MockERC20} from "solmate/src/test/utils/mocks/MockERC20.sol"; + contract QuoterTest is Test, Deployers { using SafeCast for *; using PoolIdLibrary for PoolKey; @@ -31,8 +33,8 @@ contract QuoterTest is Test, Deployers { // Max tick for full range with tick spacing of 60 int24 internal constant MAX_TICK = -MIN_TICK; - uint160 internal constant SQRT_RATIO_100_102 = 78447570448055484695608110440; - uint160 internal constant SQRT_RATIO_102_100 = 80016521857016594389520272648; + uint160 internal constant SQRT_PRICE_100_102 = 78447570448055484695608110440; + uint160 internal constant SQRT_PRICE_102_100 = 80016521857016594389520272648; uint256 internal constant CONTROLLER_GAS_LIMIT = 500000; @@ -52,7 +54,7 @@ contract QuoterTest is Test, Deployers { function setUp() public { deployFreshManagerAndRouters(); - quoter = new Quoter(address(manager)); + quoter = new Quoter(IPoolManager(manager)); positionManager = new PoolModifyLiquidityTest(manager); // salts are chosen so that address(token0) < address(token1) && address(token1) < address(token2) @@ -87,7 +89,6 @@ contract QuoterTest is Test, Deployers { IQuoter.QuoteExactSingleParams({ poolKey: key02, zeroForOne: true, - recipient: address(this), exactAmount: uint128(amountIn), sqrtPriceLimitX96: 0, hookData: ZERO_BYTES @@ -109,7 +110,6 @@ contract QuoterTest is Test, Deployers { IQuoter.QuoteExactSingleParams({ poolKey: key02, zeroForOne: false, - recipient: address(this), exactAmount: uint128(amountIn), sqrtPriceLimitX96: 0, hookData: ZERO_BYTES @@ -325,15 +325,14 @@ contract QuoterTest is Test, Deployers { IQuoter.QuoteExactSingleParams({ poolKey: key01, zeroForOne: true, - recipient: address(this), exactAmount: type(uint128).max, - sqrtPriceLimitX96: SQRT_RATIO_100_102, + sqrtPriceLimitX96: SQRT_PRICE_100_102, hookData: ZERO_BYTES }) ); assertEq(deltaAmounts[0], 9981); - assertEq(sqrtPriceX96After, SQRT_RATIO_100_102); + assertEq(sqrtPriceX96After, SQRT_PRICE_100_102); assertEq(initializedTicksLoaded, 0); } @@ -343,15 +342,14 @@ contract QuoterTest is Test, Deployers { IQuoter.QuoteExactSingleParams({ poolKey: key01, zeroForOne: false, - recipient: address(this), exactAmount: type(uint128).max, - sqrtPriceLimitX96: SQRT_RATIO_102_100, + sqrtPriceLimitX96: SQRT_PRICE_102_100, hookData: ZERO_BYTES }) ); assertEq(deltaAmounts[1], 9981); - assertEq(sqrtPriceX96After, SQRT_RATIO_102_100); + assertEq(sqrtPriceX96After, SQRT_PRICE_102_100); assertEq(initializedTicksLoaded, 0); } @@ -639,7 +637,7 @@ contract QuoterTest is Test, Deployers { function getExactInputParams(MockERC20[] memory _tokenPath, uint256 amountIn) internal - view + pure returns (IQuoter.QuoteExactParams memory params) { PathKey[] memory path = new PathKey[](_tokenPath.length - 1); @@ -649,13 +647,12 @@ contract QuoterTest is Test, Deployers { params.exactCurrency = Currency.wrap(address(_tokenPath[0])); params.path = path; - params.recipient = address(this); params.exactAmount = uint128(amountIn); } function getExactOutputParams(MockERC20[] memory _tokenPath, uint256 amountOut) internal - view + pure returns (IQuoter.QuoteExactParams memory params) { PathKey[] memory path = new PathKey[](_tokenPath.length - 1); @@ -665,7 +662,6 @@ contract QuoterTest is Test, Deployers { params.exactCurrency = Currency.wrap(address(_tokenPath[_tokenPath.length - 1])); params.path = path; - params.recipient = address(this); params.exactAmount = uint128(amountOut); } } diff --git a/test/SafeCallback.t.sol b/test/SafeCallback.t.sol new file mode 100644 index 00000000..fd5157ab --- /dev/null +++ b/test/SafeCallback.t.sol @@ -0,0 +1,33 @@ +//SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; +import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; + +import {SafeCallback} from "../src/base/SafeCallback.sol"; +import {MockSafeCallback} from "./mocks/MockSafeCallback.sol"; + +contract SafeCallbackTest is Test, Deployers { + MockSafeCallback safeCallback; + + function setUp() public { + deployFreshManager(); + safeCallback = new MockSafeCallback(manager); + } + + function test_poolManagerAddress() public view { + assertEq(address(safeCallback.poolManager()), address(manager)); + } + + function test_unlock(uint256 num) public { + bytes memory result = safeCallback.unlockManager(num); + assertEq(num, abi.decode(result, (uint256))); + } + + function test_unlockRevert(address caller, bytes calldata data) public { + vm.startPrank(caller); + if (caller != address(manager)) vm.expectRevert(SafeCallback.NotPoolManager.selector); + safeCallback.unlockCallback(data); + vm.stopPrank(); + } +} diff --git a/test/StateViewTest.t.sol b/test/StateViewTest.t.sol new file mode 100644 index 00000000..392d7048 --- /dev/null +++ b/test/StateViewTest.t.sol @@ -0,0 +1,522 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; + +import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; +import {FixedPointMathLib} from "solmate/src/utils/FixedPointMathLib.sol"; +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; +import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; +import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; +import {CurrencyLibrary, Currency} from "@uniswap/v4-core/src/types/Currency.sol"; +import {Pool} from "@uniswap/v4-core/src/libraries/Pool.sol"; +import {TickBitmap} from "@uniswap/v4-core/src/libraries/TickBitmap.sol"; +import {FixedPoint128} from "@uniswap/v4-core/src/libraries/FixedPoint128.sol"; +import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; +import {Fuzzers} from "@uniswap/v4-core/src/test/Fuzzers.sol"; +import {Position} from "@uniswap/v4-core/src/libraries/Position.sol"; + +import {StateView} from "../src/lens/StateView.sol"; + +/// This test was taken from StateLibrary.t.sol in v4-core and adapted to use the StateView contract instead. +contract StateViewTest is Test, Deployers, Fuzzers, GasSnapshot { + using FixedPointMathLib for uint256; + using PoolIdLibrary for PoolKey; + + PoolId poolId; + + StateView state; + + function setUp() public { + deployFreshManagerAndRouters(); + deployMintAndApprove2Currencies(); + + // Create the pool + key = PoolKey(currency0, currency1, 3000, 60, IHooks(address(0x0))); + poolId = key.toId(); + manager.initialize(key, SQRT_PRICE_1_1, ZERO_BYTES); + + state = new StateView(manager); + } + + function test_getSlot0() public { + // create liquidity + modifyLiquidityRouter.modifyLiquidity( + key, IPoolManager.ModifyLiquidityParams(-60, 60, 10_000 ether, 0), ZERO_BYTES + ); + + modifyLiquidityRouter.modifyLiquidity( + key, IPoolManager.ModifyLiquidityParams(-600, 600, 10_000 ether, 0), ZERO_BYTES + ); + + // swap to create fees, crossing a tick + uint256 swapAmount = 100 ether; + swap(key, true, -int256(swapAmount), ZERO_BYTES); + (uint160 sqrtPriceX96, int24 tick, uint24 protocolFee, uint24 swapFee) = state.getSlot0(poolId); + snapLastCall("StateView_extsload_getSlot0"); + assertEq(tick, -139); + + // magic number verified against a native getter + assertEq(sqrtPriceX96, 78680104762184586858280382455); + assertEq(tick, -139); + assertEq(protocolFee, 0); // tested in protocol fee tests + assertEq(swapFee, 3000); + } + + function test_getTickLiquidity() public { + modifyLiquidityRouter.modifyLiquidity(key, IPoolManager.ModifyLiquidityParams(-60, 60, 10 ether, 0), ZERO_BYTES); + + (uint128 liquidityGrossLower, int128 liquidityNetLower) = state.getTickLiquidity(poolId, -60); + snapLastCall("StateView_extsload_getTickLiquidity"); + assertEq(liquidityGrossLower, 10 ether); + assertEq(liquidityNetLower, 10 ether); + + (uint128 liquidityGrossUpper, int128 liquidityNetUpper) = state.getTickLiquidity(poolId, 60); + assertEq(liquidityGrossUpper, 10 ether); + assertEq(liquidityNetUpper, -10 ether); + } + + function test_fuzz_getTickLiquidity(IPoolManager.ModifyLiquidityParams memory params) public { + (IPoolManager.ModifyLiquidityParams memory _params,) = + Fuzzers.createFuzzyLiquidity(modifyLiquidityRouter, key, params, SQRT_PRICE_1_1, ZERO_BYTES); + uint128 liquidityDelta = uint128(uint256(_params.liquidityDelta)); + + (uint128 liquidityGrossLower, int128 liquidityNetLower) = state.getTickLiquidity(poolId, _params.tickLower); + assertEq(liquidityGrossLower, liquidityDelta); + assertEq(liquidityNetLower, int128(_params.liquidityDelta)); + + (uint128 liquidityGrossUpper, int128 liquidityNetUpper) = state.getTickLiquidity(poolId, _params.tickUpper); + assertEq(liquidityGrossUpper, liquidityDelta); + assertEq(liquidityNetUpper, -int128(_params.liquidityDelta)); + + // confirm agreement with getTickInfo() + (uint128 _liquidityGrossLower, int128 _liquidityNetLower,,) = state.getTickInfo(poolId, _params.tickLower); + assertEq(_liquidityGrossLower, liquidityGrossLower); + assertEq(_liquidityNetLower, liquidityNetLower); + + (uint128 _liquidityGrossUpper, int128 _liquidityNetUpper,,) = state.getTickInfo(poolId, _params.tickUpper); + assertEq(_liquidityGrossUpper, liquidityGrossUpper); + assertEq(_liquidityNetUpper, liquidityNetUpper); + } + + function test_fuzz_getTickLiquidity_two_positions( + IPoolManager.ModifyLiquidityParams memory paramsA, + IPoolManager.ModifyLiquidityParams memory paramsB + ) public { + (IPoolManager.ModifyLiquidityParams memory _paramsA,) = Fuzzers.createFuzzyLiquidityWithTightBound( + modifyLiquidityRouter, key, paramsA, SQRT_PRICE_1_1, ZERO_BYTES, 2 + ); + (IPoolManager.ModifyLiquidityParams memory _paramsB,) = Fuzzers.createFuzzyLiquidityWithTightBound( + modifyLiquidityRouter, key, paramsB, SQRT_PRICE_1_1, ZERO_BYTES, 2 + ); + + uint128 liquidityDeltaA = uint128(uint256(_paramsA.liquidityDelta)); + uint128 liquidityDeltaB = uint128(uint256(_paramsB.liquidityDelta)); + + (uint128 liquidityGrossLowerA, int128 liquidityNetLowerA) = state.getTickLiquidity(poolId, _paramsA.tickLower); + (uint128 liquidityGrossLowerB, int128 liquidityNetLowerB) = state.getTickLiquidity(poolId, _paramsB.tickLower); + (uint256 liquidityGrossUpperA, int256 liquidityNetUpperA) = state.getTickLiquidity(poolId, _paramsA.tickUpper); + (uint256 liquidityGrossUpperB, int256 liquidityNetUpperB) = state.getTickLiquidity(poolId, _paramsB.tickUpper); + + // when tick lower is shared between two positions, the gross liquidity is the sum + if (_paramsA.tickLower == _paramsB.tickLower || _paramsA.tickLower == _paramsB.tickUpper) { + assertEq(liquidityGrossLowerA, liquidityDeltaA + liquidityDeltaB); + + // when tick lower is shared with an upper tick, the net liquidity is the difference + (_paramsA.tickLower == _paramsB.tickLower) + ? assertEq(liquidityNetLowerA, int128(liquidityDeltaA + liquidityDeltaB)) + : assertApproxEqAbs(liquidityNetLowerA, int128(liquidityDeltaA) - int128(liquidityDeltaB), 1 wei); + } else { + assertEq(liquidityGrossLowerA, liquidityDeltaA); + assertEq(liquidityNetLowerA, int128(liquidityDeltaA)); + } + + if (_paramsA.tickUpper == _paramsB.tickLower || _paramsA.tickUpper == _paramsB.tickUpper) { + assertEq(liquidityGrossUpperA, liquidityDeltaA + liquidityDeltaB); + (_paramsA.tickUpper == _paramsB.tickUpper) + ? assertEq(liquidityNetUpperA, -int128(liquidityDeltaA + liquidityDeltaB)) + : assertApproxEqAbs(liquidityNetUpperA, int128(liquidityDeltaB) - int128(liquidityDeltaA), 2 wei); + } else { + assertEq(liquidityGrossUpperA, liquidityDeltaA); + assertEq(liquidityNetUpperA, -int128(liquidityDeltaA)); + } + + if (_paramsB.tickLower == _paramsA.tickLower || _paramsB.tickLower == _paramsA.tickUpper) { + assertEq(liquidityGrossLowerB, liquidityDeltaA + liquidityDeltaB); + (_paramsB.tickLower == _paramsA.tickLower) + ? assertEq(liquidityNetLowerB, int128(liquidityDeltaA + liquidityDeltaB)) + : assertApproxEqAbs(liquidityNetLowerB, int128(liquidityDeltaB) - int128(liquidityDeltaA), 1 wei); + } else { + assertEq(liquidityGrossLowerB, liquidityDeltaB); + assertEq(liquidityNetLowerB, int128(liquidityDeltaB)); + } + + if (_paramsB.tickUpper == _paramsA.tickLower || _paramsB.tickUpper == _paramsA.tickUpper) { + assertEq(liquidityGrossUpperB, liquidityDeltaA + liquidityDeltaB); + (_paramsB.tickUpper == _paramsA.tickUpper) + ? assertEq(liquidityNetUpperB, -int128(liquidityDeltaA + liquidityDeltaB)) + : assertApproxEqAbs(liquidityNetUpperB, int128(liquidityDeltaA) - int128(liquidityDeltaB), 2 wei); + } else { + assertEq(liquidityGrossUpperB, liquidityDeltaB); + assertEq(liquidityNetUpperB, -int128(liquidityDeltaB)); + } + } + + function test_getFeeGrowthGlobals0() public { + // create liquidity + uint256 liquidity = 10_000 ether; + modifyLiquidityRouter.modifyLiquidity( + key, IPoolManager.ModifyLiquidityParams(-60, 60, int256(liquidity), 0), ZERO_BYTES + ); + + (uint256 feeGrowthGlobal0, uint256 feeGrowthGlobal1) = state.getFeeGrowthGlobals(poolId); + assertEq(feeGrowthGlobal0, 0); + assertEq(feeGrowthGlobal1, 0); + + // swap to create fees on the input token (currency0) + uint256 swapAmount = 10 ether; + swap(key, true, -int256(swapAmount), ZERO_BYTES); + + (feeGrowthGlobal0, feeGrowthGlobal1) = state.getFeeGrowthGlobals(poolId); + snapLastCall("StateView_extsload_getFeeGrowthGlobals"); + + uint256 feeGrowthGlobalCalc = swapAmount.mulWadDown(0.003e18).mulDivDown(FixedPoint128.Q128, liquidity); + assertEq(feeGrowthGlobal0, feeGrowthGlobalCalc); + assertEq(feeGrowthGlobal1, 0); + } + + function test_getFeeGrowthGlobals1() public { + // create liquidity + uint256 liquidity = 10_000 ether; + modifyLiquidityRouter.modifyLiquidity( + key, IPoolManager.ModifyLiquidityParams(-60, 60, int256(liquidity), 0), ZERO_BYTES + ); + + (uint256 feeGrowthGlobal0, uint256 feeGrowthGlobal1) = state.getFeeGrowthGlobals(poolId); + assertEq(feeGrowthGlobal0, 0); + assertEq(feeGrowthGlobal1, 0); + + // swap to create fees on the input token (currency1) + uint256 swapAmount = 10 ether; + swap(key, false, -int256(swapAmount), ZERO_BYTES); + + (feeGrowthGlobal0, feeGrowthGlobal1) = state.getFeeGrowthGlobals(poolId); + + assertEq(feeGrowthGlobal0, 0); + uint256 feeGrowthGlobalCalc = swapAmount.mulWadDown(0.003e18).mulDivDown(FixedPoint128.Q128, liquidity); + assertEq(feeGrowthGlobal1, feeGrowthGlobalCalc); + } + + function test_getLiquidity() public { + modifyLiquidityRouter.modifyLiquidity(key, IPoolManager.ModifyLiquidityParams(-60, 60, 10 ether, 0), ZERO_BYTES); + modifyLiquidityRouter.modifyLiquidity( + key, IPoolManager.ModifyLiquidityParams(-120, 120, 10 ether, 0), ZERO_BYTES + ); + + uint128 liquidity = state.getLiquidity(poolId); + snapLastCall("StateView_extsload_getLiquidity"); + assertEq(liquidity, 20 ether); + } + + function test_fuzz_getLiquidity(IPoolManager.ModifyLiquidityParams memory params) public { + (IPoolManager.ModifyLiquidityParams memory _params,) = + Fuzzers.createFuzzyLiquidity(modifyLiquidityRouter, key, params, SQRT_PRICE_1_1, ZERO_BYTES); + (, int24 tick,,) = state.getSlot0(poolId); + uint128 liquidity = state.getLiquidity(poolId); + + // out of range liquidity is not added to Pool.State.liquidity + if (tick < _params.tickLower || tick >= _params.tickUpper) { + assertEq(liquidity, 0); + } else { + assertEq(liquidity, uint128(uint256(_params.liquidityDelta))); + } + } + + function test_getTickBitmap() public { + int24 tickLower = -300; + int24 tickUpper = 300; + // create liquidity + modifyLiquidityRouter.modifyLiquidity( + key, IPoolManager.ModifyLiquidityParams(tickLower, tickUpper, 10_000 ether, 0), ZERO_BYTES + ); + + (int16 wordPos, uint8 bitPos) = TickBitmap.position(tickLower / key.tickSpacing); + uint256 tickBitmap = state.getTickBitmap(poolId, wordPos); + snapLastCall("StateView_extsload_getTickBitmap"); + assertNotEq(tickBitmap, 0); + assertEq(tickBitmap, 1 << bitPos); + + (wordPos, bitPos) = TickBitmap.position(tickUpper / key.tickSpacing); + tickBitmap = state.getTickBitmap(poolId, wordPos); + assertNotEq(tickBitmap, 0); + assertEq(tickBitmap, 1 << bitPos); + } + + function test_fuzz_getTickBitmap(IPoolManager.ModifyLiquidityParams memory params) public { + (IPoolManager.ModifyLiquidityParams memory _params,) = + Fuzzers.createFuzzyLiquidity(modifyLiquidityRouter, key, params, SQRT_PRICE_1_1, ZERO_BYTES); + + (int16 wordPos, uint8 bitPos) = TickBitmap.position(_params.tickLower / key.tickSpacing); + (int16 wordPosUpper, uint8 bitPosUpper) = TickBitmap.position(_params.tickUpper / key.tickSpacing); + + uint256 tickBitmap = state.getTickBitmap(poolId, wordPos); + assertNotEq(tickBitmap, 0); + + // in fuzz tests, the tickLower and tickUpper might exist on the same word + if (wordPos == wordPosUpper) { + assertEq(tickBitmap, (1 << bitPos) | (1 << bitPosUpper)); + } else { + assertEq(tickBitmap, 1 << bitPos); + } + } + + function test_getPositionInfo() public { + // create liquidity + modifyLiquidityRouter.modifyLiquidity( + key, IPoolManager.ModifyLiquidityParams(-60, 60, 10_000 ether, 0), ZERO_BYTES + ); + + // swap to create fees, crossing a tick + uint256 swapAmount = 10 ether; + swap(key, true, -int256(swapAmount), ZERO_BYTES); + (, int24 currentTick,,) = state.getSlot0(poolId); + assertNotEq(currentTick, -139); + + // poke the LP so that fees are updated + modifyLiquidityRouter.modifyLiquidity(key, IPoolManager.ModifyLiquidityParams(-60, 60, 0, 0), ZERO_BYTES); + + bytes32 positionId = + Position.calculatePositionKey(address(modifyLiquidityRouter), int24(-60), int24(60), bytes32(0)); + + (uint128 liquidity, uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) = + state.getPositionInfo(poolId, positionId); + snapLastCall("StateView_extsload_getPositionInfo"); + + assertEq(liquidity, 10_000 ether); + + assertNotEq(feeGrowthInside0X128, 0); + assertEq(feeGrowthInside1X128, 0); + } + + function test_fuzz_getPositionInfo( + IPoolManager.ModifyLiquidityParams memory params, + uint256 swapAmount, + bool zeroForOne + ) public { + (IPoolManager.ModifyLiquidityParams memory _params, BalanceDelta delta) = + createFuzzyLiquidity(modifyLiquidityRouter, key, params, SQRT_PRICE_1_1, ZERO_BYTES); + + uint256 delta0 = uint256(int256(-delta.amount0())); + uint256 delta1 = uint256(int256(-delta.amount1())); + // if one of the deltas is zero, ensure to swap in the right direction + if (delta0 == 0) { + zeroForOne = true; + } else if (delta1 == 0) { + zeroForOne = false; + } + swapAmount = bound(swapAmount, 1, uint256(int256(type(int128).max))); + swap(key, zeroForOne, -int256(swapAmount), ZERO_BYTES); + + // poke the LP so that fees are updated + modifyLiquidityRouter.modifyLiquidity( + key, IPoolManager.ModifyLiquidityParams(_params.tickLower, _params.tickUpper, 0, 0), ZERO_BYTES + ); + + bytes32 positionId = Position.calculatePositionKey( + address(modifyLiquidityRouter), _params.tickLower, _params.tickUpper, bytes32(0) + ); + + (uint128 liquidity, uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) = + state.getPositionInfo(poolId, positionId); + + assertEq(liquidity, uint128(uint256(_params.liquidityDelta))); + if (zeroForOne) { + assertNotEq(feeGrowthInside0X128, 0); + assertEq(feeGrowthInside1X128, 0); + } else { + assertEq(feeGrowthInside0X128, 0); + assertNotEq(feeGrowthInside1X128, 0); + } + } + + function test_getTickFeeGrowthOutside() public { + // create liquidity + modifyLiquidityRouter.modifyLiquidity( + key, IPoolManager.ModifyLiquidityParams(-60, 60, 10_000 ether, 0), ZERO_BYTES + ); + + modifyLiquidityRouter.modifyLiquidity( + key, IPoolManager.ModifyLiquidityParams(-600, 600, 10_000 ether, 0), ZERO_BYTES + ); + + // swap to create fees, crossing a tick + uint256 swapAmount = 100 ether; + swap(key, true, -int256(swapAmount), ZERO_BYTES); + (, int24 currentTick,,) = state.getSlot0(poolId); + assertEq(currentTick, -139); + + int24 tick = -60; + (uint256 feeGrowthOutside0X128, uint256 feeGrowthOutside1X128) = state.getTickFeeGrowthOutside(poolId, tick); + snapLastCall("StateView_extsload_getTickFeeGrowthOutside"); + + // magic number verified against a native getter on PoolManager + assertEq(feeGrowthOutside0X128, 3076214778951936192155253373200636); + assertEq(feeGrowthOutside1X128, 0); + + tick = 60; + (feeGrowthOutside0X128, feeGrowthOutside1X128) = state.getTickFeeGrowthOutside(poolId, tick); + assertEq(feeGrowthOutside0X128, 0); + assertEq(feeGrowthOutside1X128, 0); + } + + // also hard to fuzz because of feeGrowthOutside + function test_getTickInfo() public { + // create liquidity + modifyLiquidityRouter.modifyLiquidity( + key, IPoolManager.ModifyLiquidityParams(-60, 60, 10_000 ether, 0), ZERO_BYTES + ); + + modifyLiquidityRouter.modifyLiquidity( + key, IPoolManager.ModifyLiquidityParams(-600, 600, 10_000 ether, 0), ZERO_BYTES + ); + + // swap to create fees, crossing a tick + uint256 swapAmount = 100 ether; + swap(key, true, -int256(swapAmount), ZERO_BYTES); + (, int24 currentTick,,) = state.getSlot0(poolId); + assertEq(currentTick, -139); + + int24 tick = -60; + (uint128 liquidityGross, int128 liquidityNet, uint256 feeGrowthOutside0X128, uint256 feeGrowthOutside1X128) = + state.getTickInfo(poolId, tick); + snapLastCall("StateView_extsload_getTickInfo"); + + (uint128 liquidityGross_, int128 liquidityNet_) = state.getTickLiquidity(poolId, tick); + (uint256 feeGrowthOutside0X128_, uint256 feeGrowthOutside1X128_) = state.getTickFeeGrowthOutside(poolId, tick); + + assertEq(liquidityGross, 10_000 ether); + assertEq(liquidityGross, liquidityGross_); + assertEq(liquidityNet, liquidityNet_); + + assertNotEq(feeGrowthOutside0X128, 0); + assertEq(feeGrowthOutside1X128, 0); + assertEq(feeGrowthOutside0X128, feeGrowthOutside0X128_); + assertEq(feeGrowthOutside1X128, feeGrowthOutside1X128_); + } + + function test_getFeeGrowthInside() public { + // create liquidity + modifyLiquidityRouter.modifyLiquidity( + key, IPoolManager.ModifyLiquidityParams(-60, 60, 10_000 ether, 0), ZERO_BYTES + ); + + modifyLiquidityRouter.modifyLiquidity( + key, IPoolManager.ModifyLiquidityParams(-600, 600, 10_000 ether, 0), ZERO_BYTES + ); + + // swap to create fees, crossing a tick + uint256 swapAmount = 100 ether; + swap(key, true, -int256(swapAmount), ZERO_BYTES); + (, int24 currentTick,,) = state.getSlot0(poolId); + assertEq(currentTick, -139); + + // calculated live + (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) = state.getFeeGrowthInside(poolId, -60, 60); + snapLastCall("StateView_extsload_getFeeGrowthInside"); + + // poke the LP so that fees are updated + modifyLiquidityRouter.modifyLiquidity(key, IPoolManager.ModifyLiquidityParams(-60, 60, 0, 0), ZERO_BYTES); + + bytes32 positionId = + Position.calculatePositionKey(address(modifyLiquidityRouter), int24(-60), int24(60), bytes32(0)); + + (, uint256 feeGrowthInside0X128_, uint256 feeGrowthInside1X128_) = state.getPositionInfo(poolId, positionId); + + assertNotEq(feeGrowthInside0X128, 0); + assertEq(feeGrowthInside0X128, feeGrowthInside0X128_); + assertEq(feeGrowthInside1X128, feeGrowthInside1X128_); + } + + function test_fuzz_getFeeGrowthInside(IPoolManager.ModifyLiquidityParams memory params, bool zeroForOne) public { + modifyLiquidityRouter.modifyLiquidity( + key, + IPoolManager.ModifyLiquidityParams( + TickMath.minUsableTick(key.tickSpacing), TickMath.maxUsableTick(key.tickSpacing), 10_000 ether, 0 + ), + ZERO_BYTES + ); + + (IPoolManager.ModifyLiquidityParams memory _params,) = + createFuzzyLiquidity(modifyLiquidityRouter, key, params, SQRT_PRICE_1_1, ZERO_BYTES); + + swap(key, zeroForOne, -int256(100e18), ZERO_BYTES); + + // calculated live + (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) = + state.getFeeGrowthInside(poolId, _params.tickLower, _params.tickUpper); + + // poke the LP so that fees are updated + modifyLiquidityRouter.modifyLiquidity( + key, IPoolManager.ModifyLiquidityParams(_params.tickLower, _params.tickUpper, 0, 0), ZERO_BYTES + ); + bytes32 positionId = Position.calculatePositionKey( + address(modifyLiquidityRouter), _params.tickLower, _params.tickUpper, bytes32(0) + ); + + (, uint256 feeGrowthInside0X128_, uint256 feeGrowthInside1X128_) = state.getPositionInfo(poolId, positionId); + + assertEq(feeGrowthInside0X128, feeGrowthInside0X128_); + assertEq(feeGrowthInside1X128, feeGrowthInside1X128_); + } + + function test_getPositionLiquidity() public { + // create liquidity + modifyLiquidityRouter.modifyLiquidity( + key, IPoolManager.ModifyLiquidityParams(-60, 60, 10_000 ether, 0), ZERO_BYTES + ); + + bytes32 positionId = + Position.calculatePositionKey(address(modifyLiquidityRouter), int24(-60), int24(60), bytes32(0)); + + uint128 liquidity = state.getPositionLiquidity(poolId, positionId); + snapLastCall("StateView_extsload_getPositionLiquidity"); + + assertEq(liquidity, 10_000 ether); + } + + function test_fuzz_getPositionLiquidity( + IPoolManager.ModifyLiquidityParams memory paramsA, + IPoolManager.ModifyLiquidityParams memory paramsB + ) public { + (IPoolManager.ModifyLiquidityParams memory _paramsA) = + Fuzzers.createFuzzyLiquidityParams(key, paramsA, SQRT_PRICE_1_1); + + (IPoolManager.ModifyLiquidityParams memory _paramsB) = + Fuzzers.createFuzzyLiquidityParams(key, paramsB, SQRT_PRICE_1_1); + + // Assume there are no overlapping positions + vm.assume( + _paramsA.tickLower != _paramsB.tickLower && _paramsA.tickLower != _paramsB.tickUpper + && _paramsB.tickLower != _paramsA.tickUpper && _paramsA.tickUpper != _paramsB.tickUpper + ); + + modifyLiquidityRouter.modifyLiquidity(key, _paramsA, ZERO_BYTES); + modifyLiquidityRouter.modifyLiquidity(key, _paramsB, ZERO_BYTES); + + bytes32 positionIdA = Position.calculatePositionKey( + address(modifyLiquidityRouter), _paramsA.tickLower, _paramsA.tickUpper, bytes32(0) + ); + uint128 liquidityA = state.getPositionLiquidity(poolId, positionIdA); + assertEq(liquidityA, uint128(uint256(_paramsA.liquidityDelta))); + + bytes32 positionIdB = Position.calculatePositionKey( + address(modifyLiquidityRouter), _paramsB.tickLower, _paramsB.tickUpper, bytes32(0) + ); + uint128 liquidityB = state.getPositionLiquidity(poolId, positionIdB); + assertEq(liquidityB, uint128(uint256(_paramsB.liquidityDelta))); + } +} diff --git a/test/TWAMM.t.sol b/test/TWAMM.t.sol deleted file mode 100644 index 0f2f82e0..00000000 --- a/test/TWAMM.t.sol +++ /dev/null @@ -1,432 +0,0 @@ -pragma solidity ^0.8.15; - -import {Test} from "forge-std/Test.sol"; -import {Vm} from "forge-std/Vm.sol"; -import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; -import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol"; -import {IERC20Minimal} from "@uniswap/v4-core/src/interfaces/external/IERC20Minimal.sol"; -import {TWAMMImplementation} from "./shared/implementation/TWAMMImplementation.sol"; -import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; -import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; -import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; -import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; -import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; -import {PoolModifyLiquidityTest} from "@uniswap/v4-core/src/test/PoolModifyLiquidityTest.sol"; -import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol"; -import {PoolDonateTest} from "@uniswap/v4-core/src/test/PoolDonateTest.sol"; -import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; -import {CurrencyLibrary, Currency} from "@uniswap/v4-core/src/types/Currency.sol"; -import {TWAMM} from "../contracts/hooks/examples/TWAMM.sol"; -import {ITWAMM} from "../contracts/interfaces/ITWAMM.sol"; -import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; - -contract TWAMMTest is Test, Deployers, GasSnapshot { - using PoolIdLibrary for PoolKey; - using CurrencyLibrary for Currency; - - event SubmitOrder( - PoolId indexed poolId, - address indexed owner, - uint160 expiration, - bool zeroForOne, - uint256 sellRate, - uint256 earningsFactorLast - ); - - event UpdateOrder( - PoolId indexed poolId, - address indexed owner, - uint160 expiration, - bool zeroForOne, - uint256 sellRate, - uint256 earningsFactorLast - ); - - TWAMM twamm = - TWAMM(address(uint160(Hooks.BEFORE_INITIALIZE_FLAG | Hooks.BEFORE_SWAP_FLAG | Hooks.BEFORE_ADD_LIQUIDITY_FLAG))); - address hookAddress; - MockERC20 token0; - MockERC20 token1; - PoolKey poolKey; - PoolId poolId; - - function setUp() public { - deployFreshManagerAndRouters(); - (currency0, currency1) = deployMintAndApprove2Currencies(); - - token0 = MockERC20(Currency.unwrap(currency0)); - token1 = MockERC20(Currency.unwrap(currency1)); - - TWAMMImplementation impl = new TWAMMImplementation(manager, 10_000, twamm); - (, bytes32[] memory writes) = vm.accesses(address(impl)); - vm.etch(address(twamm), address(impl).code); - // for each storage key that was written during the hook implementation, copy the value over - unchecked { - for (uint256 i = 0; i < writes.length; i++) { - bytes32 slot = writes[i]; - vm.store(address(twamm), slot, vm.load(address(impl), slot)); - } - } - - (poolKey, poolId) = initPool(currency0, currency1, twamm, 3000, SQRT_PRICE_1_1, ZERO_BYTES); - - token0.approve(address(modifyLiquidityRouter), 100 ether); - token1.approve(address(modifyLiquidityRouter), 100 ether); - token0.mint(address(this), 100 ether); - token1.mint(address(this), 100 ether); - modifyLiquidityRouter.modifyLiquidity( - poolKey, IPoolManager.ModifyLiquidityParams(-60, 60, 10 ether, 0), ZERO_BYTES - ); - modifyLiquidityRouter.modifyLiquidity( - poolKey, IPoolManager.ModifyLiquidityParams(-120, 120, 10 ether, 0), ZERO_BYTES - ); - modifyLiquidityRouter.modifyLiquidity( - poolKey, - IPoolManager.ModifyLiquidityParams(TickMath.minUsableTick(60), TickMath.maxUsableTick(60), 10 ether, 0), - ZERO_BYTES - ); - } - - function testTWAMM_beforeInitialize_SetsLastVirtualOrderTimestamp() public { - (PoolKey memory initKey, PoolId initId) = newPoolKeyWithTWAMM(twamm); - assertEq(twamm.lastVirtualOrderTimestamp(initId), 0); - vm.warp(10000); - - manager.initialize(initKey, SQRT_PRICE_1_1, ZERO_BYTES); - assertEq(twamm.lastVirtualOrderTimestamp(initId), 10000); - } - - function testTWAMM_submitOrder_StoresOrderWithCorrectPoolAndOrderPoolInfo() public { - uint160 expiration = 30000; - uint160 submitTimestamp = 10000; - uint160 duration = expiration - submitTimestamp; - - ITWAMM.OrderKey memory orderKey = ITWAMM.OrderKey(address(this), expiration, true); - - ITWAMM.Order memory nullOrder = twamm.getOrder(poolKey, orderKey); - assertEq(nullOrder.sellRate, 0); - assertEq(nullOrder.earningsFactorLast, 0); - - vm.warp(10000); - token0.approve(address(twamm), 100 ether); - snapStart("TWAMMSubmitOrder"); - twamm.submitOrder(poolKey, orderKey, 1 ether); - snapEnd(); - - ITWAMM.Order memory submittedOrder = twamm.getOrder(poolKey, orderKey); - (uint256 sellRateCurrent0For1, uint256 earningsFactorCurrent0For1) = twamm.getOrderPool(poolKey, true); - (uint256 sellRateCurrent1For0, uint256 earningsFactorCurrent1For0) = twamm.getOrderPool(poolKey, false); - - assertEq(submittedOrder.sellRate, 1 ether / duration); - assertEq(submittedOrder.earningsFactorLast, 0); - assertEq(sellRateCurrent0For1, 1 ether / duration); - assertEq(sellRateCurrent1For0, 0); - assertEq(earningsFactorCurrent0For1, 0); - assertEq(earningsFactorCurrent1For0, 0); - } - - function TWAMMSingleSell0For1SellRateAndEarningsFactorGetsUpdatedProperly() public { - // TODO: fails with a bug for single pool sell, swap amount 3 wei above balance. - - ITWAMM.OrderKey memory orderKey1 = ITWAMM.OrderKey(address(this), 30000, true); - ITWAMM.OrderKey memory orderKey2 = ITWAMM.OrderKey(address(this), 40000, true); - - token0.approve(address(twamm), 100e18); - token1.approve(address(twamm), 100e18); - vm.warp(10000); - twamm.submitOrder(poolKey, orderKey1, 1e18); - vm.warp(30000); - twamm.submitOrder(poolKey, orderKey2, 1e18); - vm.warp(40000); - - ITWAMM.Order memory submittedOrder = twamm.getOrder(poolKey, orderKey2); - (, uint256 earningsFactorCurrent) = twamm.getOrderPool(poolKey, true); - assertEq(submittedOrder.sellRate, 1 ether / 10000); - assertEq(submittedOrder.earningsFactorLast, earningsFactorCurrent); - } - - function testTWAMM_submitOrder_StoresSellRatesEarningsFactorsProperly() public { - uint160 expiration1 = 30000; - uint160 expiration2 = 40000; - uint256 submitTimestamp1 = 10000; - uint256 submitTimestamp2 = 30000; - uint256 earningsFactor0For1; - uint256 earningsFactor1For0; - uint256 sellRate0For1; - uint256 sellRate1For0; - - ITWAMM.OrderKey memory orderKey1 = ITWAMM.OrderKey(address(this), expiration1, true); - ITWAMM.OrderKey memory orderKey2 = ITWAMM.OrderKey(address(this), expiration2, true); - ITWAMM.OrderKey memory orderKey3 = ITWAMM.OrderKey(address(this), expiration2, false); - - token0.approve(address(twamm), 100e18); - token1.approve(address(twamm), 100e18); - - // Submit 2 TWAMM orders and test all information gets updated - vm.warp(submitTimestamp1); - twamm.submitOrder(poolKey, orderKey1, 1e18); - twamm.submitOrder(poolKey, orderKey3, 3e18); - - (sellRate0For1, earningsFactor0For1) = twamm.getOrderPool(poolKey, true); - (sellRate1For0, earningsFactor1For0) = twamm.getOrderPool(poolKey, false); - assertEq(sellRate0For1, 1e18 / (expiration1 - submitTimestamp1)); - assertEq(sellRate1For0, 3e18 / (expiration2 - submitTimestamp1)); - assertEq(earningsFactor0For1, 0); - assertEq(earningsFactor1For0, 0); - - // Warp time and submit 1 TWAMM order. Test that pool information is updated properly as one order expires and - // another order is added to the pool - vm.warp(submitTimestamp2); - twamm.submitOrder(poolKey, orderKey2, 2e18); - - (sellRate0For1, earningsFactor0For1) = twamm.getOrderPool(poolKey, true); - (sellRate1For0, earningsFactor1For0) = twamm.getOrderPool(poolKey, false); - - assertEq(sellRate0For1, 2e18 / (expiration2 - submitTimestamp2)); - assertEq(sellRate1For0, 3e18 / (expiration2 - submitTimestamp1)); - assertEq(earningsFactor0For1, 1712020976636017581269515821040000); - assertEq(earningsFactor1For0, 1470157410324350030712806974476955); - } - - function testTWAMM_submitOrder_EmitsEvent() public { - ITWAMM.OrderKey memory orderKey1 = ITWAMM.OrderKey(address(this), 30000, true); - - token0.approve(address(twamm), 100e18); - vm.warp(10000); - - vm.expectEmit(false, false, false, true); - emit SubmitOrder(poolId, address(this), 30000, true, 1 ether / 20000, 0); - twamm.submitOrder(poolKey, orderKey1, 1e18); - } - - function testTWAMM_updateOrder_EmitsEvent() public { - ITWAMM.OrderKey memory orderKey1; - ITWAMM.OrderKey memory orderKey2; - uint256 orderAmount; - (orderKey1, orderKey2, orderAmount) = submitOrdersBothDirections(); - // decrease order amount by 10% - int256 amountDelta = -1; - - // set timestamp to halfway through the order - vm.warp(20000); - - vm.expectEmit(true, true, true, true); - emit UpdateOrder(poolId, address(this), 30000, true, 0, 10000 << 96); - twamm.updateOrder(poolKey, orderKey1, amountDelta); - } - - function testTWAMM_updateOrder_ZeroForOne_DecreasesSellrateUpdatesSellTokensOwed() public { - ITWAMM.OrderKey memory orderKey1; - ITWAMM.OrderKey memory orderKey2; - uint256 orderAmount; - (orderKey1, orderKey2, orderAmount) = submitOrdersBothDirections(); - // decrease order amount by 10% - int256 amountDelta = -int256(orderAmount) / 10; - - // set timestamp to halfway through the order - vm.warp(20000); - - (uint256 originalSellRate,) = twamm.getOrderPool(poolKey, true); - twamm.updateOrder(poolKey, orderKey1, amountDelta); - (uint256 updatedSellRate,) = twamm.getOrderPool(poolKey, true); - - uint256 token0Owed = twamm.tokensOwed(poolKey.currency0, orderKey1.owner); - uint256 token1Owed = twamm.tokensOwed(poolKey.currency1, orderKey1.owner); - - // takes 10% off the remaining half (so 80% of original sellrate) - assertEq(updatedSellRate, (originalSellRate * 80) / 100); - assertEq(token0Owed, uint256(-amountDelta)); - assertEq(token1Owed, orderAmount / 2); - } - - function testTWAMM_updateOrder_OneForZero_DecreasesSellrateUpdatesSellTokensOwed() public { - ITWAMM.OrderKey memory orderKey1; - ITWAMM.OrderKey memory orderKey2; - uint256 orderAmount; - (orderKey1, orderKey2, orderAmount) = submitOrdersBothDirections(); - - // decrease order amount by 10% - int256 amountDelta = -int256(orderAmount) / 10; - - // set timestamp to halfway through the order - vm.warp(20000); - - (uint256 originalSellRate,) = twamm.getOrderPool(poolKey, false); - twamm.updateOrder(poolKey, orderKey2, amountDelta); - (uint256 updatedSellRate,) = twamm.getOrderPool(poolKey, false); - - uint256 token0Owed = twamm.tokensOwed(poolKey.currency0, orderKey1.owner); - uint256 token1Owed = twamm.tokensOwed(poolKey.currency1, orderKey1.owner); - - // takes 10% off the remaining half (so 80% of original sellrate) - assertEq(updatedSellRate, (originalSellRate * 80) / 100); - assertEq(token0Owed, orderAmount / 2); - assertEq(token1Owed, uint256(-amountDelta)); - } - - function testTWAMM_updatedOrder_ZeroForOne_ClosesOrderIfEliminatingPosition() public { - ITWAMM.OrderKey memory orderKey1; - ITWAMM.OrderKey memory orderKey2; - uint256 orderAmount; - (orderKey1, orderKey2, orderAmount) = submitOrdersBothDirections(); - - // set timestamp to halfway through the order - vm.warp(20000); - - twamm.updateOrder(poolKey, orderKey1, -1); - ITWAMM.Order memory deletedOrder = twamm.getOrder(poolKey, orderKey1); - uint256 token0Owed = twamm.tokensOwed(poolKey.currency0, orderKey1.owner); - uint256 token1Owed = twamm.tokensOwed(poolKey.currency1, orderKey1.owner); - - assertEq(deletedOrder.sellRate, 0); - assertEq(deletedOrder.earningsFactorLast, 0); - assertEq(token0Owed, orderAmount / 2); - assertEq(token1Owed, orderAmount / 2); - } - - function testTWAMM_updatedOrder_OneForZero_ClosesOrderIfEliminatingPosition() public { - ITWAMM.OrderKey memory orderKey1; - ITWAMM.OrderKey memory orderKey2; - uint256 orderAmount; - (orderKey1, orderKey2, orderAmount) = submitOrdersBothDirections(); - - // set timestamp to halfway through the order - vm.warp(20000); - - twamm.updateOrder(poolKey, orderKey2, -1); - ITWAMM.Order memory deletedOrder = twamm.getOrder(poolKey, orderKey2); - uint256 token0Owed = twamm.tokensOwed(poolKey.currency0, orderKey2.owner); - uint256 token1Owed = twamm.tokensOwed(poolKey.currency1, orderKey2.owner); - - assertEq(deletedOrder.sellRate, 0); - assertEq(deletedOrder.earningsFactorLast, 0); - assertEq(token0Owed, orderAmount / 2); - assertEq(token1Owed, orderAmount / 2); - } - - function testTWAMM_updatedOrder_ZeroForOne_IncreaseOrderAmount() public { - int256 amountDelta = 1 ether; - ITWAMM.OrderKey memory orderKey1; - ITWAMM.OrderKey memory orderKey2; - uint256 orderAmount; - (orderKey1, orderKey2, orderAmount) = submitOrdersBothDirections(); - - // set timestamp to halfway through the order - vm.warp(20000); - - uint256 balance0TWAMMBefore = token0.balanceOf(address(twamm)); - token0.approve(address(twamm), uint256(amountDelta)); - twamm.updateOrder(poolKey, orderKey1, amountDelta); - uint256 balance0TWAMMAfter = token0.balanceOf(address(twamm)); - - ITWAMM.Order memory updatedOrder = twamm.getOrder(poolKey, orderKey1); - uint256 token0Owed = twamm.tokensOwed(poolKey.currency0, orderKey1.owner); - uint256 token1Owed = twamm.tokensOwed(poolKey.currency1, orderKey1.owner); - - assertEq(balance0TWAMMAfter - balance0TWAMMBefore, uint256(amountDelta)); - assertEq(updatedOrder.sellRate, 150000000000000); - assertEq(token0Owed, 0); - assertEq(token1Owed, orderAmount / 2); - } - - function testTWAMM_updatedOrder_OneForZero_IncreaseOrderAmount() public { - int256 amountDelta = 1 ether; - ITWAMM.OrderKey memory orderKey1; - ITWAMM.OrderKey memory orderKey2; - uint256 orderAmount; - (orderKey1, orderKey2, orderAmount) = submitOrdersBothDirections(); - - // set timestamp to halfway through the order - vm.warp(20000); - - uint256 balance0TWAMMBefore = token1.balanceOf(address(twamm)); - token1.approve(address(twamm), uint256(amountDelta)); - twamm.updateOrder(poolKey, orderKey2, amountDelta); - uint256 balance0TWAMMAfter = token1.balanceOf(address(twamm)); - - ITWAMM.Order memory updatedOrder = twamm.getOrder(poolKey, orderKey2); - uint256 token0Owed = twamm.tokensOwed(poolKey.currency0, orderKey2.owner); - uint256 token1Owed = twamm.tokensOwed(poolKey.currency1, orderKey2.owner); - - assertEq(balance0TWAMMAfter - balance0TWAMMBefore, uint256(amountDelta)); - assertEq(updatedOrder.sellRate, 150000000000000); - assertEq(token0Owed, orderAmount / 2); - assertEq(token1Owed, 0); - } - - function testTWAMMEndToEndSimSymmetricalOrderPools() public { - uint256 orderAmount = 1e18; - ITWAMM.OrderKey memory orderKey1 = ITWAMM.OrderKey(address(this), 30000, true); - ITWAMM.OrderKey memory orderKey2 = ITWAMM.OrderKey(address(this), 30000, false); - - token0.approve(address(twamm), 100e18); - token1.approve(address(twamm), 100e18); - modifyLiquidityRouter.modifyLiquidity( - poolKey, IPoolManager.ModifyLiquidityParams(-2400, 2400, 10 ether, 0), ZERO_BYTES - ); - - vm.warp(10000); - twamm.submitOrder(poolKey, orderKey1, orderAmount); - twamm.submitOrder(poolKey, orderKey2, orderAmount); - vm.warp(20000); - twamm.executeTWAMMOrders(poolKey); - twamm.updateOrder(poolKey, orderKey1, 0); - twamm.updateOrder(poolKey, orderKey2, 0); - - uint256 earningsToken0 = twamm.tokensOwed(poolKey.currency0, address(this)); - uint256 earningsToken1 = twamm.tokensOwed(poolKey.currency1, address(this)); - - assertEq(earningsToken0, orderAmount / 2); - assertEq(earningsToken1, orderAmount / 2); - - uint256 balance0BeforeTWAMM = MockERC20(Currency.unwrap(poolKey.currency0)).balanceOf(address(twamm)); - uint256 balance1BeforeTWAMM = MockERC20(Currency.unwrap(poolKey.currency1)).balanceOf(address(twamm)); - uint256 balance0BeforeThis = poolKey.currency0.balanceOfSelf(); - uint256 balance1BeforeThis = poolKey.currency1.balanceOfSelf(); - - vm.warp(30000); - twamm.executeTWAMMOrders(poolKey); - twamm.updateOrder(poolKey, orderKey1, 0); - twamm.updateOrder(poolKey, orderKey2, 0); - twamm.claimTokens(poolKey.currency0, address(this), 0); - twamm.claimTokens(poolKey.currency1, address(this), 0); - - assertEq(twamm.tokensOwed(poolKey.currency0, address(this)), 0); - assertEq(twamm.tokensOwed(poolKey.currency1, address(this)), 0); - - uint256 balance0AfterTWAMM = MockERC20(Currency.unwrap(poolKey.currency0)).balanceOf(address(twamm)); - uint256 balance1AfterTWAMM = MockERC20(Currency.unwrap(poolKey.currency1)).balanceOf(address(twamm)); - uint256 balance0AfterThis = poolKey.currency0.balanceOfSelf(); - uint256 balance1AfterThis = poolKey.currency1.balanceOfSelf(); - - assertEq(balance1AfterTWAMM, 0); - assertEq(balance0AfterTWAMM, 0); - assertEq(balance0BeforeTWAMM - balance0AfterTWAMM, orderAmount); - assertEq(balance0AfterThis - balance0BeforeThis, orderAmount); - assertEq(balance1BeforeTWAMM - balance1AfterTWAMM, orderAmount); - assertEq(balance1AfterThis - balance1BeforeThis, orderAmount); - } - - function newPoolKeyWithTWAMM(IHooks hooks) public returns (PoolKey memory, PoolId) { - (Currency _token0, Currency _token1) = deployMintAndApprove2Currencies(); - PoolKey memory key = PoolKey(_token0, _token1, 0, 60, hooks); - return (key, key.toId()); - } - - function submitOrdersBothDirections() - internal - returns (ITWAMM.OrderKey memory key1, ITWAMM.OrderKey memory key2, uint256 amount) - { - key1 = ITWAMM.OrderKey(address(this), 30000, true); - key2 = ITWAMM.OrderKey(address(this), 30000, false); - amount = 1 ether; - - token0.approve(address(twamm), amount); - token1.approve(address(twamm), amount); - - vm.warp(10000); - twamm.submitOrder(poolKey, key1, amount); - twamm.submitOrder(poolKey, key2, amount); - } -} diff --git a/test/libraries/CalldataDecoder.t.sol b/test/libraries/CalldataDecoder.t.sol new file mode 100644 index 00000000..25f8593d --- /dev/null +++ b/test/libraries/CalldataDecoder.t.sol @@ -0,0 +1,187 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; +import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; + +import {MockCalldataDecoder} from "../mocks/MockCalldataDecoder.sol"; +import {PositionConfig} from "../../src/libraries/PositionConfig.sol"; +import {IV4Router} from "../../src/interfaces/IV4Router.sol"; +import {PathKey} from "../../src/libraries/PathKey.sol"; + +contract CalldataDecoderTest is Test { + MockCalldataDecoder decoder; + + function setUp() public { + decoder = new MockCalldataDecoder(); + } + + function test_fuzz_decodeModifyLiquidityParams( + uint256 _tokenId, + PositionConfig calldata _config, + uint256 _liquidity, + uint128 _amount0, + uint128 _amount1, + bytes calldata _hookData + ) public view { + bytes memory params = abi.encode(_tokenId, _config, _liquidity, _amount0, _amount1, _hookData); + ( + uint256 tokenId, + PositionConfig memory config, + uint256 liquidity, + uint128 amount0, + uint128 amount1, + bytes memory hookData + ) = decoder.decodeModifyLiquidityParams(params); + + assertEq(tokenId, _tokenId); + assertEq(liquidity, _liquidity); + assertEq(amount0, _amount0); + assertEq(amount1, _amount1); + assertEq(hookData, _hookData); + _assertEq(_config, config); + } + + function test_fuzz_decodeBurnParams( + uint256 _tokenId, + PositionConfig calldata _config, + uint128 _amount0Min, + uint128 _amount1Min, + bytes calldata _hookData + ) public view { + bytes memory params = abi.encode(_tokenId, _config, _amount0Min, _amount1Min, _hookData); + (uint256 tokenId, PositionConfig memory config, uint128 amount0Min, uint128 amount1Min, bytes memory hookData) = + decoder.decodeBurnParams(params); + + assertEq(tokenId, _tokenId); + assertEq(hookData, _hookData); + _assertEq(_config, config); + assertEq(amount0Min, _amount0Min); + assertEq(amount1Min, _amount1Min); + } + + function test_fuzz_decodeMintParams( + PositionConfig calldata _config, + uint256 _liquidity, + uint128 _amount0Max, + uint128 _amount1Max, + address _owner, + bytes calldata _hookData + ) public view { + bytes memory params = abi.encode(_config, _liquidity, _amount0Max, _amount1Max, _owner, _hookData); + ( + PositionConfig memory config, + uint256 liquidity, + uint128 amount0Max, + uint128 amount1Max, + address owner, + bytes memory hookData + ) = decoder.decodeMintParams(params); + + assertEq(liquidity, _liquidity); + assertEq(amount0Max, _amount0Max); + assertEq(amount1Max, _amount1Max); + assertEq(owner, _owner); + assertEq(hookData, _hookData); + _assertEq(_config, config); + } + + function test_fuzz_decodeSwapExactInParams(IV4Router.ExactInputParams calldata _swapParams) public view { + bytes memory params = abi.encode(_swapParams); + IV4Router.ExactInputParams memory swapParams = decoder.decodeSwapExactInParams(params); + + assertEq(Currency.unwrap(swapParams.currencyIn), Currency.unwrap(_swapParams.currencyIn)); + assertEq(swapParams.amountIn, _swapParams.amountIn); + assertEq(swapParams.amountOutMinimum, _swapParams.amountOutMinimum); + _assertEq(swapParams.path, _swapParams.path); + } + + function test_fuzz_decodeSwapExactInSingleParams(IV4Router.ExactInputSingleParams calldata _swapParams) + public + view + { + bytes memory params = abi.encode(_swapParams); + IV4Router.ExactInputSingleParams memory swapParams = decoder.decodeSwapExactInSingleParams(params); + + 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); + } + + function test_fuzz_decodeSwapExactOutParams(IV4Router.ExactOutputParams calldata _swapParams) public view { + bytes memory params = abi.encode(_swapParams); + IV4Router.ExactOutputParams memory swapParams = decoder.decodeSwapExactOutParams(params); + + assertEq(Currency.unwrap(swapParams.currencyOut), Currency.unwrap(_swapParams.currencyOut)); + assertEq(swapParams.amountOut, _swapParams.amountOut); + assertEq(swapParams.amountInMaximum, _swapParams.amountInMaximum); + _assertEq(swapParams.path, _swapParams.path); + } + + function test_fuzz_decodeSwapExactOutSingleParams(IV4Router.ExactOutputSingleParams calldata _swapParams) + public + view + { + bytes memory params = abi.encode(_swapParams); + IV4Router.ExactOutputSingleParams memory swapParams = decoder.decodeSwapExactOutSingleParams(params); + + 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); + } + + function test_fuzz_decodeCurrencyAndAddress(Currency _currency, address __address) public view { + bytes memory params = abi.encode(_currency, __address); + (Currency currency, address _address) = decoder.decodeCurrencyAndAddress(params); + + assertEq(Currency.unwrap(currency), Currency.unwrap(_currency)); + assertEq(_address, __address); + } + + function test_fuzz_decodeCurrency(Currency _currency) public view { + bytes memory params = abi.encode(_currency); + (Currency currency) = decoder.decodeCurrency(params); + + assertEq(Currency.unwrap(currency), Currency.unwrap(_currency)); + } + + function test_fuzz_decodeCurrencyAndUint256(Currency _currency, uint256 _amount) public view { + bytes memory params = abi.encode(_currency, _amount); + (Currency currency, uint256 amount) = decoder.decodeCurrencyAndUint256(params); + + assertEq(Currency.unwrap(currency), Currency.unwrap(_currency)); + assertEq(amount, _amount); + } + + function _assertEq(PathKey[] memory path1, PathKey[] memory path2) internal pure { + assertEq(path1.length, path2.length); + for (uint256 i = 0; i < path1.length; i++) { + assertEq(Currency.unwrap(path1[i].intermediateCurrency), Currency.unwrap(path2[i].intermediateCurrency)); + assertEq(path1[i].fee, path2[i].fee); + assertEq(path1[i].tickSpacing, path2[i].tickSpacing); + assertEq(address(path1[i].hooks), address(path2[i].hooks)); + assertEq(path1[i].hookData, path2[i].hookData); + } + } + + function _assertEq(PositionConfig memory config1, PositionConfig memory config2) internal pure { + _assertEq(config1.poolKey, config2.poolKey); + assertEq(config1.tickLower, config2.tickLower); + assertEq(config1.tickUpper, config2.tickUpper); + } + + function _assertEq(PoolKey memory key1, PoolKey memory key2) internal pure { + assertEq(Currency.unwrap(key1.currency0), Currency.unwrap(key2.currency0)); + assertEq(Currency.unwrap(key1.currency1), Currency.unwrap(key2.currency1)); + assertEq(key1.fee, key2.fee); + assertEq(key1.tickSpacing, key2.tickSpacing); + assertEq(address(key1.hooks), address(key2.hooks)); + } +} diff --git a/test/libraries/CurrencyDeltas.t.sol b/test/libraries/CurrencyDeltas.t.sol new file mode 100644 index 00000000..53dad9e4 --- /dev/null +++ b/test/libraries/CurrencyDeltas.t.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; +import {IERC20} from "forge-std/interfaces/IERC20.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; +import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; + +import {MockCurrencyDeltaReader} from "../mocks/MockCurrencyDeltaReader.sol"; + +contract CurrencyDeltasTest is Test, Deployers { + using CurrencyLibrary for Currency; + + MockCurrencyDeltaReader reader; + + function setUp() public { + deployFreshManagerAndRouters(); + deployMintAndApprove2Currencies(); + + reader = new MockCurrencyDeltaReader(manager); + + IERC20 token0 = IERC20(Currency.unwrap(currency0)); + IERC20 token1 = IERC20(Currency.unwrap(currency1)); + + token0.approve(address(reader), type(uint256).max); + token1.approve(address(reader), type(uint256).max); + + // send tokens to PoolManager so tests can .take() + token0.transfer(address(manager), 1_000_000e18); + token1.transfer(address(manager), 1_000_000e18); + + // convert some ERC20s into ERC6909 + claimsRouter.deposit(currency0, address(this), 1_000_000e18); + claimsRouter.deposit(currency1, address(this), 1_000_000e18); + manager.approve(address(reader), currency0.toId(), type(uint256).max); + manager.approve(address(reader), currency1.toId(), type(uint256).max); + } + + function test_fuzz_currencyDeltas(uint8 depth, uint256 seed, uint128 amount0, uint128 amount1) public { + int128 delta0Expected = 0; + int128 delta1Expected = 0; + + bytes[] memory calls = new bytes[](depth); + for (uint256 i = 0; i < depth; i++) { + amount0 = uint128(bound(amount0, 1, 100e18)); + amount1 = uint128(bound(amount1, 1, 100e18)); + uint256 _seed = seed % (i + 1); + if (_seed % 8 == 0) { + calls[i] = abi.encodeWithSelector(MockCurrencyDeltaReader.settle.selector, currency0, amount0); + delta0Expected += int128(amount0); + } else if (_seed % 8 == 1) { + calls[i] = abi.encodeWithSelector(MockCurrencyDeltaReader.settle.selector, currency1, amount1); + delta1Expected += int128(amount1); + } else if (_seed % 8 == 2) { + calls[i] = abi.encodeWithSelector(MockCurrencyDeltaReader.burn.selector, currency0, amount0); + delta0Expected += int128(amount0); + } else if (_seed % 8 == 3) { + calls[i] = abi.encodeWithSelector(MockCurrencyDeltaReader.burn.selector, currency1, amount1); + delta1Expected += int128(amount1); + } else if (_seed % 8 == 4) { + calls[i] = abi.encodeWithSelector(MockCurrencyDeltaReader.take.selector, currency0, amount0); + delta0Expected -= int128(amount0); + } else if (_seed % 8 == 5) { + calls[i] = abi.encodeWithSelector(MockCurrencyDeltaReader.take.selector, currency1, amount1); + delta1Expected -= int128(amount1); + } else if (_seed % 8 == 6) { + calls[i] = abi.encodeWithSelector(MockCurrencyDeltaReader.mint.selector, currency0, amount0); + delta0Expected -= int128(amount0); + } else if (_seed % 8 == 7) { + calls[i] = abi.encodeWithSelector(MockCurrencyDeltaReader.mint.selector, currency1, amount1); + delta1Expected -= int128(amount1); + } + } + + BalanceDelta delta = reader.execute(calls, currency0, currency1); + assertEq(delta.amount0(), delta0Expected); + assertEq(delta.amount1(), delta1Expected); + } +} diff --git a/test/libraries/PositionConfig.t.sol b/test/libraries/PositionConfig.t.sol new file mode 100644 index 00000000..1eeedc18 --- /dev/null +++ b/test/libraries/PositionConfig.t.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; +import {PositionConfig, PositionConfigLibrary} from "../../src/libraries/PositionConfig.sol"; + +contract PositionConfigTest is Test { + using PositionConfigLibrary for PositionConfig; + + function test_fuzz_toId(PositionConfig calldata config) public pure { + bytes32 expectedId = keccak256( + abi.encodePacked( + config.poolKey.currency0, + config.poolKey.currency1, + config.poolKey.fee, + config.poolKey.tickSpacing, + config.poolKey.hooks, + config.tickLower, + config.tickUpper + ) + ); + assertEq(expectedId, config.toId()); + } +} diff --git a/test/middleware/BaseMiddlewareFactoryImplementation.sol b/test/middleware/BaseMiddlewareFactoryImplementation.sol index 1eca231d..6137e7e9 100644 --- a/test/middleware/BaseMiddlewareFactoryImplementation.sol +++ b/test/middleware/BaseMiddlewareFactoryImplementation.sol @@ -2,13 +2,30 @@ pragma solidity ^0.8.24; import {BaseMiddlewareImplementation} from "./BaseMiddlewareImplemenation.sol"; -import {BaseMiddlewareFactory} from "./../../contracts/middleware/BaseMiddlewareFactory.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -contract BaseMiddlewareFactoryImplementation is BaseMiddlewareFactory { - constructor(IPoolManager _manager) BaseMiddlewareFactory(_manager) {} +contract BaseMiddlewareFactoryImplementation { + event MiddlewareCreated(address implementation, address middleware); - function _deployMiddleware(address implementation, bytes32 salt) internal override returns (address middleware) { - middleware = address(new BaseMiddlewareImplementation{salt: salt}(manager, implementation)); + mapping(address => address) private _implementations; + + IPoolManager public immutable poolManager; + + constructor(IPoolManager _poolManager) { + poolManager = _poolManager; + } + + function getImplementation(address middleware) external view returns (address implementation) { + return _implementations[middleware]; + } + + function createMiddleware(address implementation, bytes32 salt) external returns (address middleware) { + middleware = _deployMiddleware(implementation, salt); + _implementations[middleware] = implementation; + emit MiddlewareCreated(implementation, middleware); + } + + function _deployMiddleware(address implementation, bytes32 salt) internal returns (address middleware) { + middleware = address(new BaseMiddlewareImplementation{salt: salt}(poolManager, implementation)); } } diff --git a/test/middleware/BaseMiddlewareImplemenation.sol b/test/middleware/BaseMiddlewareImplemenation.sol index 865a2e39..8f0a0b08 100644 --- a/test/middleware/BaseMiddlewareImplemenation.sol +++ b/test/middleware/BaseMiddlewareImplemenation.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.24; -import {BaseMiddleware} from "./../../contracts/middleware/BaseMiddleware.sol"; +import {BaseMiddleware} from "./../../src/middleware/BaseMiddleware.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; contract BaseMiddlewareImplementation is BaseMiddleware { diff --git a/test/middleware/BlankRemoveHooks.sol b/test/middleware/BlankRemoveHooks.sol new file mode 100644 index 00000000..8d43ecc5 --- /dev/null +++ b/test/middleware/BlankRemoveHooks.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import {BaseHook} from "./../../src/base/hooks/BaseHook.sol"; +import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {BalanceDelta, BalanceDeltaLibrary} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; + +contract BlankRemoveHooks is BaseHook { + constructor(IPoolManager _poolManager) BaseHook(_poolManager) {} + + // for testing + function validateHookAddress(BaseHook _this) internal pure override {} + + function getHookPermissions() public pure override returns (Hooks.Permissions memory) { + return Hooks.Permissions({ + beforeInitialize: false, + afterInitialize: false, + beforeAddLiquidity: false, + afterAddLiquidity: false, + beforeRemoveLiquidity: true, + afterRemoveLiquidity: true, + beforeSwap: false, + afterSwap: false, + beforeDonate: false, + afterDonate: false, + beforeSwapReturnDelta: false, + afterSwapReturnDelta: false, + afterAddLiquidityReturnDelta: false, + afterRemoveLiquidityReturnDelta: false + }); + } + + function beforeRemoveLiquidity( + address, + PoolKey calldata, + IPoolManager.ModifyLiquidityParams calldata, + bytes calldata + ) external pure override returns (bytes4) { + return BaseHook.beforeRemoveLiquidity.selector; + } + + function afterRemoveLiquidity( + address, + PoolKey calldata, + IPoolManager.ModifyLiquidityParams calldata, + BalanceDelta, + bytes calldata + ) external pure override returns (bytes4, BalanceDelta) { + return (BaseHook.afterRemoveLiquidity.selector, BalanceDeltaLibrary.ZERO_DELTA); + } +} diff --git a/test/middleware/FeeOnRemove.sol b/test/middleware/FeeOnRemove.sol new file mode 100644 index 00000000..8f1caeef --- /dev/null +++ b/test/middleware/FeeOnRemove.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import {BaseHook} from "./../../src/base/hooks/BaseHook.sol"; +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; +import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; + +contract FeeOnRemove is BaseHook { + constructor(IPoolManager _poolManager) BaseHook(_poolManager) {} + + error FeeTooHigh(); + + uint128 public liquidityFee = 543; // 5.43% + uint128 public constant TOTAL_BIPS = 10000; + + // middleware implementations do not need to be mined + function validateHookAddress(BaseHook _this) internal pure override {} + + function getHookPermissions() public pure override returns (Hooks.Permissions memory) { + return Hooks.Permissions({ + beforeInitialize: false, + afterInitialize: false, + beforeAddLiquidity: false, + afterAddLiquidity: false, + beforeRemoveLiquidity: true, + afterRemoveLiquidity: true, + beforeSwap: false, + afterSwap: false, + beforeDonate: false, + afterDonate: false, + beforeSwapReturnDelta: false, + afterSwapReturnDelta: false, + afterAddLiquidityReturnDelta: false, + afterRemoveLiquidityReturnDelta: true + }); + } + + function setLiquidityFee(uint128 _liquidityFee) external { + if (_liquidityFee > TOTAL_BIPS) revert FeeTooHigh(); + liquidityFee = _liquidityFee; + } + + function beforeRemoveLiquidity( + address, + PoolKey calldata, + IPoolManager.ModifyLiquidityParams calldata, + bytes calldata + ) external pure override returns (bytes4) { + return BaseHook.beforeRemoveLiquidity.selector; + } + + function afterRemoveLiquidity( + address, + PoolKey calldata key, + IPoolManager.ModifyLiquidityParams calldata, + BalanceDelta delta, + bytes calldata + ) external override onlyByPoolManager returns (bytes4, BalanceDelta) { + uint128 feeAmount0 = uint128(delta.amount0()) * liquidityFee / TOTAL_BIPS; + uint128 feeAmount1 = uint128(delta.amount1()) * liquidityFee / TOTAL_BIPS; + + poolManager.mint(address(this), key.currency0.toId(), feeAmount0); + poolManager.mint(address(this), key.currency1.toId(), feeAmount1); + + return (BaseHook.afterRemoveLiquidity.selector, toBalanceDelta(int128(feeAmount0), int128(feeAmount1))); + } +} diff --git a/test/middleware/FrontrunRemove.sol b/test/middleware/FrontrunRemove.sol new file mode 100644 index 00000000..5c134ef2 --- /dev/null +++ b/test/middleware/FrontrunRemove.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import {BaseHook} from "./../../src/base/hooks/BaseHook.sol"; +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; +import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; + +contract FrontrunRemove is BaseHook { + constructor(IPoolManager _poolManager) BaseHook(_poolManager) {} + + bytes internal constant ZERO_BYTES = bytes(""); + uint160 public constant MIN_PRICE_LIMIT = TickMath.MIN_SQRT_PRICE + 1; + + // middleware implementations do not need to be mined + function validateHookAddress(BaseHook _this) internal pure override {} + + function getHookPermissions() public pure override returns (Hooks.Permissions memory) { + return Hooks.Permissions({ + beforeInitialize: false, + afterInitialize: false, + beforeAddLiquidity: false, + afterAddLiquidity: false, + beforeRemoveLiquidity: true, + afterRemoveLiquidity: false, + beforeSwap: false, + afterSwap: false, + beforeDonate: false, + afterDonate: false, + beforeSwapReturnDelta: false, + afterSwapReturnDelta: false, + afterAddLiquidityReturnDelta: false, + afterRemoveLiquidityReturnDelta: false + }); + } + + function beforeRemoveLiquidity( + address, + PoolKey calldata key, + IPoolManager.ModifyLiquidityParams calldata, + bytes calldata + ) external override returns (bytes4) { + BalanceDelta swapDelta = poolManager.swap(key, IPoolManager.SwapParams(true, 1000, MIN_PRICE_LIMIT), ZERO_BYTES); + poolManager.sync(key.currency0); + key.currency0.transfer(address(poolManager), uint128(-swapDelta.amount0())); + poolManager.settle(); + poolManager.take(key.currency1, address(this), uint128(swapDelta.amount1())); + return IHooks.beforeRemoveLiquidity.selector; + } +} diff --git a/test/middleware/HooksCounter.sol b/test/middleware/HooksCounter.sol index 84f0b107..d0a8dc8e 100644 --- a/test/middleware/HooksCounter.sol +++ b/test/middleware/HooksCounter.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.24; -import {BaseHook} from "./../../contracts/BaseHook.sol"; +import {BaseHook} from "./../../src/base/hooks/BaseHook.sol"; import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; @@ -55,7 +55,7 @@ contract HooksCounter is BaseHook { function beforeInitialize(address, PoolKey calldata key, uint160, bytes calldata hookData) external override - onlyByManager + onlyByPoolManager returns (bytes4) { beforeInitializeCount[key.toId()]++; @@ -66,7 +66,7 @@ contract HooksCounter is BaseHook { function afterInitialize(address, PoolKey calldata key, uint160, int24, bytes calldata hookData) external override - onlyByManager + onlyByPoolManager returns (bytes4) { afterInitializeCount[key.toId()]++; @@ -79,7 +79,7 @@ contract HooksCounter is BaseHook { PoolKey calldata key, IPoolManager.ModifyLiquidityParams calldata, bytes calldata hookData - ) external override onlyByManager returns (bytes4) { + ) external override onlyByPoolManager returns (bytes4) { beforeAddLiquidityCount[key.toId()]++; lastHookData = hookData; return BaseHook.beforeAddLiquidity.selector; @@ -91,7 +91,7 @@ contract HooksCounter is BaseHook { IPoolManager.ModifyLiquidityParams calldata, BalanceDelta, bytes calldata hookData - ) external override onlyByManager returns (bytes4, BalanceDelta) { + ) external override onlyByPoolManager returns (bytes4, BalanceDelta) { afterAddLiquidityCount[key.toId()]++; lastHookData = hookData; return (BaseHook.afterAddLiquidity.selector, BalanceDeltaLibrary.ZERO_DELTA); @@ -102,7 +102,7 @@ contract HooksCounter is BaseHook { PoolKey calldata key, IPoolManager.ModifyLiquidityParams calldata, bytes calldata hookData - ) external override onlyByManager returns (bytes4) { + ) external override onlyByPoolManager returns (bytes4) { beforeRemoveLiquidityCount[key.toId()]++; lastHookData = hookData; return BaseHook.beforeRemoveLiquidity.selector; @@ -114,7 +114,7 @@ contract HooksCounter is BaseHook { IPoolManager.ModifyLiquidityParams calldata, BalanceDelta, bytes calldata hookData - ) external override onlyByManager returns (bytes4, BalanceDelta) { + ) external override onlyByPoolManager returns (bytes4, BalanceDelta) { afterRemoveLiquidityCount[key.toId()]++; lastHookData = hookData; return (BaseHook.afterRemoveLiquidity.selector, BalanceDeltaLibrary.ZERO_DELTA); @@ -123,7 +123,7 @@ contract HooksCounter is BaseHook { function beforeSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata, bytes calldata hookData) external override - onlyByManager + onlyByPoolManager returns (bytes4, BeforeSwapDelta, uint24) { beforeSwapCount[key.toId()]++; @@ -137,7 +137,7 @@ contract HooksCounter is BaseHook { IPoolManager.SwapParams calldata, BalanceDelta, bytes calldata hookData - ) external override onlyByManager returns (bytes4, int128) { + ) external override onlyByPoolManager returns (bytes4, int128) { afterSwapCount[key.toId()]++; lastHookData = hookData; return (BaseHook.afterSwap.selector, 0); @@ -146,7 +146,7 @@ contract HooksCounter is BaseHook { function beforeDonate(address, PoolKey calldata key, uint256, uint256, bytes calldata hookData) external override - onlyByManager + onlyByPoolManager returns (bytes4) { beforeDonateCount[key.toId()]++; @@ -157,7 +157,7 @@ contract HooksCounter is BaseHook { function afterDonate(address, PoolKey calldata key, uint256, uint256, bytes calldata hookData) external override - onlyByManager + onlyByPoolManager returns (bytes4) { afterDonateCount[key.toId()]++; diff --git a/test/middleware/HooksOutOfGas.sol b/test/middleware/HooksOutOfGas.sol new file mode 100644 index 00000000..fe8b603d --- /dev/null +++ b/test/middleware/HooksOutOfGas.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import {BaseHook} from "./../../src/base/hooks/BaseHook.sol"; +import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {BalanceDelta, BalanceDeltaLibrary} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol"; + +contract HooksOutOfGas is BaseHook { + uint256 counter; + + constructor(IPoolManager _poolManager) BaseHook(_poolManager) {} + + // for testing + function validateHookAddress(BaseHook _this) internal pure override {} + + function getHookPermissions() public pure override returns (Hooks.Permissions memory) { + return Hooks.Permissions({ + beforeInitialize: false, + afterInitialize: false, + beforeAddLiquidity: false, + afterAddLiquidity: false, + beforeRemoveLiquidity: true, + afterRemoveLiquidity: true, + beforeSwap: true, + afterSwap: true, + beforeDonate: false, + afterDonate: false, + beforeSwapReturnDelta: false, + afterSwapReturnDelta: false, + afterAddLiquidityReturnDelta: false, + afterRemoveLiquidityReturnDelta: false + }); + } + + function beforeSwap(address, PoolKey calldata, IPoolManager.SwapParams calldata, bytes calldata) + external + override + returns (bytes4, BeforeSwapDelta, uint24) + { + consumeAllGas(); + return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0); + } + + function afterSwap(address, PoolKey calldata, IPoolManager.SwapParams calldata, BalanceDelta, bytes calldata) + external + override + returns (bytes4, int128) + { + consumeAllGas(); + return (BaseHook.afterSwap.selector, 0); + } + + function beforeRemoveLiquidity( + address, + PoolKey calldata, + IPoolManager.ModifyLiquidityParams calldata, + bytes calldata + ) external override returns (bytes4) { + consumeAllGas(); + return BaseHook.beforeRemoveLiquidity.selector; + } + + function afterRemoveLiquidity( + address, + PoolKey calldata, + IPoolManager.ModifyLiquidityParams calldata, + BalanceDelta, + bytes calldata + ) external override returns (bytes4, BalanceDelta) { + consumeAllGas(); + return (BaseHook.afterRemoveLiquidity.selector, BalanceDeltaLibrary.ZERO_DELTA); + } + + function consumeAllGas() internal { + while (true) { + counter++; + } + } +} diff --git a/test/middleware/HooksRevert.sol b/test/middleware/HooksRevert.sol new file mode 100644 index 00000000..362b9aa6 --- /dev/null +++ b/test/middleware/HooksRevert.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import {BaseHook} from "./../../src/base/hooks/BaseHook.sol"; +import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol"; + +contract HooksRevert is BaseHook { + error AlwaysRevert(); + + constructor(IPoolManager _poolManager) BaseHook(_poolManager) {} + + // for testing + function validateHookAddress(BaseHook _this) internal pure override {} + + function getHookPermissions() public pure override returns (Hooks.Permissions memory) { + return Hooks.Permissions({ + beforeInitialize: false, + afterInitialize: false, + beforeAddLiquidity: false, + afterAddLiquidity: false, + beforeRemoveLiquidity: true, + afterRemoveLiquidity: true, + beforeSwap: true, + afterSwap: true, + beforeDonate: false, + afterDonate: false, + beforeSwapReturnDelta: false, + afterSwapReturnDelta: false, + afterAddLiquidityReturnDelta: false, + afterRemoveLiquidityReturnDelta: false + }); + } + + function beforeSwap(address, PoolKey calldata, IPoolManager.SwapParams calldata, bytes calldata) + external + pure + override + returns (bytes4, BeforeSwapDelta, uint24) + { + revert AlwaysRevert(); + } + + function afterSwap(address, PoolKey calldata, IPoolManager.SwapParams calldata, BalanceDelta, bytes calldata) + external + pure + override + returns (bytes4, int128) + { + revert AlwaysRevert(); + } + + function beforeRemoveLiquidity( + address, + PoolKey calldata, + IPoolManager.ModifyLiquidityParams calldata, + bytes calldata + ) external pure override returns (bytes4) { + revert AlwaysRevert(); + } + + function afterRemoveLiquidity( + address sender, + PoolKey calldata, + IPoolManager.ModifyLiquidityParams calldata, + BalanceDelta, + bytes calldata + ) external pure override returns (bytes4, BalanceDelta) { + require(sender == address(0), "nobody can remove"); + return (BaseHook.beforeRemoveLiquidity.selector, toBalanceDelta(0, 0)); + } +} diff --git a/test/middleware/RemoveGriefs.sol b/test/middleware/RemoveGriefs.sol new file mode 100644 index 00000000..ec671d7a --- /dev/null +++ b/test/middleware/RemoveGriefs.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import {BaseHook} from "./../../src/base/hooks/BaseHook.sol"; +import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {BalanceDelta, BalanceDeltaLibrary} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; + +// @notice This contract is used to test griefing via returning large amounts of data +contract RemoveGriefs is BaseHook { + constructor(IPoolManager _poolManager) BaseHook(_poolManager) {} + + // Middleware implementations do not need to be mined + function validateHookAddress(BaseHook _this) internal pure override {} + + function getHookPermissions() public pure override returns (Hooks.Permissions memory) { + return Hooks.Permissions({ + beforeInitialize: false, + afterInitialize: false, + beforeAddLiquidity: false, + afterAddLiquidity: false, + beforeRemoveLiquidity: true, + afterRemoveLiquidity: true, + beforeSwap: false, + afterSwap: false, + beforeDonate: false, + afterDonate: false, + beforeSwapReturnDelta: false, + afterSwapReturnDelta: false, + afterAddLiquidityReturnDelta: false, + afterRemoveLiquidityReturnDelta: false + }); + } + + function beforeRemoveLiquidity( + address, + PoolKey calldata, + IPoolManager.ModifyLiquidityParams calldata, + bytes calldata + ) external view override returns (bytes4) { + returnLotsOfData(); + } + + function afterRemoveLiquidity( + address, + PoolKey calldata, + IPoolManager.ModifyLiquidityParams calldata, + BalanceDelta, + bytes calldata + ) external view override returns (bytes4, BalanceDelta) { + returnLotsOfData(); + } + + function returnLotsOfData() internal view { + bytes memory largeData = new bytes(320000); // preallocate memory for efficiency + bytes32 tempData; + uint256 i = 0; + while (true) { + unchecked { + ++i; + } + tempData = bytes32(i); + assembly { + mstore(add(largeData, add(32, mul(i, 32))), tempData) + } + if (gasleft() < 100_000) break; + } + assembly { + let len := mul(i, 32) + mstore(largeData, len) + return(add(largeData, 32), len) + } + } +} diff --git a/test/middleware/RemoveOutOfGas.sol b/test/middleware/RemoveOutOfGas.sol new file mode 100644 index 00000000..deaddc92 --- /dev/null +++ b/test/middleware/RemoveOutOfGas.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {BalanceDelta, BalanceDeltaLibrary} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {BaseHook} from "./../../src/base/hooks/BaseHook.sol"; + +contract RemoveOutOfGas is BaseHook { + uint256 counter; + + constructor(IPoolManager _poolManager) BaseHook(_poolManager) {} + + // middleware implementations do not need to be mined + function validateHookAddress(BaseHook _this) internal pure override {} + + function getHookPermissions() public pure override returns (Hooks.Permissions memory) { + return Hooks.Permissions({ + beforeInitialize: false, + afterInitialize: false, + beforeAddLiquidity: false, + afterAddLiquidity: false, + beforeRemoveLiquidity: true, + afterRemoveLiquidity: true, + beforeSwap: false, + afterSwap: false, + beforeDonate: false, + afterDonate: false, + beforeSwapReturnDelta: false, + afterSwapReturnDelta: false, + afterAddLiquidityReturnDelta: false, + afterRemoveLiquidityReturnDelta: false + }); + } + + function beforeRemoveLiquidity( + address, + PoolKey calldata, + IPoolManager.ModifyLiquidityParams calldata, + bytes calldata + ) external override returns (bytes4) { + consumeAllGas(); + return BaseHook.beforeRemoveLiquidity.selector; + } + + function afterRemoveLiquidity( + address, + PoolKey calldata, + IPoolManager.ModifyLiquidityParams calldata, + BalanceDelta, + bytes calldata + ) external override returns (bytes4, BalanceDelta) { + consumeAllGas(); + return (BaseHook.afterRemoveLiquidity.selector, BalanceDeltaLibrary.ZERO_DELTA); + } + + function consumeAllGas() internal { + while (true) { + counter++; + } + } +} diff --git a/test/middleware/RemoveReturnsMaxDeltas.sol b/test/middleware/RemoveReturnsMaxDeltas.sol new file mode 100644 index 00000000..2c2633fb --- /dev/null +++ b/test/middleware/RemoveReturnsMaxDeltas.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import {BaseHook} from "./../../src/base/hooks/BaseHook.sol"; +import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {BalanceDelta, BalanceDeltaLibrary, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; + +// @notice This contract is used to test griefing via returning the maximum amount of data +contract RemoveReturnsMaxDeltas is BaseHook { + constructor(IPoolManager _poolManager) BaseHook(_poolManager) {} + + // Middleware implementations do not need to be mined + function validateHookAddress(BaseHook _this) internal pure override {} + + function getHookPermissions() public pure override returns (Hooks.Permissions memory) { + return Hooks.Permissions({ + beforeInitialize: false, + afterInitialize: false, + beforeAddLiquidity: false, + afterAddLiquidity: false, + beforeRemoveLiquidity: false, + afterRemoveLiquidity: true, + beforeSwap: false, + afterSwap: false, + beforeDonate: false, + afterDonate: false, + beforeSwapReturnDelta: false, + afterSwapReturnDelta: false, + afterAddLiquidityReturnDelta: false, + afterRemoveLiquidityReturnDelta: false + }); + } + + function afterRemoveLiquidity( + address, + PoolKey calldata key, + IPoolManager.ModifyLiquidityParams calldata, + BalanceDelta, + bytes calldata + ) external override returns (bytes4, BalanceDelta) { + int128 max = type(int128).max; + key.currency0.transfer(address(poolManager), uint128(max)); + key.currency1.transfer(address(poolManager), uint128(max)); + poolManager.sync(key.currency0); + poolManager.settle(); + poolManager.sync(key.currency1); + poolManager.settle(); + return (BaseHook.afterRemoveLiquidity.selector, toBalanceDelta(-max, -max)); + } +} diff --git a/test/middleware/RemoveReverts.sol b/test/middleware/RemoveReverts.sol new file mode 100644 index 00000000..6ad7010d --- /dev/null +++ b/test/middleware/RemoveReverts.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import {BaseHook} from "./../../src/base/hooks/BaseHook.sol"; +import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; + +contract RemoveReverts is BaseHook { + error AlwaysRevert(); + + constructor(IPoolManager _poolManager) BaseHook(_poolManager) {} + + // middleware implementations do not need to be mined + function validateHookAddress(BaseHook _this) internal pure override {} + + function getHookPermissions() public pure override returns (Hooks.Permissions memory) { + return Hooks.Permissions({ + beforeInitialize: false, + afterInitialize: false, + beforeAddLiquidity: false, + afterAddLiquidity: false, + beforeRemoveLiquidity: true, + afterRemoveLiquidity: true, + beforeSwap: false, + afterSwap: false, + beforeDonate: false, + afterDonate: false, + beforeSwapReturnDelta: false, + afterSwapReturnDelta: false, + afterAddLiquidityReturnDelta: false, + afterRemoveLiquidityReturnDelta: false + }); + } + + function beforeRemoveLiquidity( + address, + PoolKey calldata, + IPoolManager.ModifyLiquidityParams calldata, + bytes calldata + ) external pure override returns (bytes4) { + revert AlwaysRevert(); + } + + function afterRemoveLiquidity( + address sender, + PoolKey calldata, + IPoolManager.ModifyLiquidityParams calldata, + BalanceDelta, + bytes calldata + ) external pure override returns (bytes4, BalanceDelta) { + require(sender == address(0), "nobody can remove"); + return (BaseHook.beforeRemoveLiquidity.selector, toBalanceDelta(0, 0)); + } +} diff --git a/test/mocks/MockBaseActionsRouter.sol b/test/mocks/MockBaseActionsRouter.sol new file mode 100644 index 00000000..7e8e186d --- /dev/null +++ b/test/mocks/MockBaseActionsRouter.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.24; + +import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {BaseActionsRouter} from "../../src/base/BaseActionsRouter.sol"; +import {Actions} from "../../src/libraries/Actions.sol"; +import {ReentrancyLock} from "../../src/base/ReentrancyLock.sol"; + +contract MockBaseActionsRouter is BaseActionsRouter, ReentrancyLock { + uint256 public swapCount; + uint256 public increaseLiqCount; + uint256 public decreaseLiqCount; + uint256 public donateCount; + uint256 public clearCount; + uint256 public settleCount; + uint256 public takeCount; + uint256 public mintCount; + uint256 public burnCount; + + constructor(IPoolManager _poolManager) BaseActionsRouter(_poolManager) {} + + function executeActions(bytes calldata params) external isNotLocked { + _executeActions(params); + } + + function _handleAction(uint256 action, bytes calldata params) internal override { + if (action < Actions.SETTLE) { + if (action == Actions.SWAP_EXACT_IN) _swap(params); + else if (action == Actions.INCREASE_LIQUIDITY) _increaseLiquidity(params); + else if (action == Actions.DECREASE_LIQUIDITY) _decreaseLiquidity(params); + else if (action == Actions.DONATE) _donate(params); + else revert UnsupportedAction(action); + } else { + if (action == Actions.SETTLE) _settle(params); + else if (action == Actions.TAKE) _take(params); + else if (action == Actions.CLEAR) _clear(params); + else if (action == Actions.MINT_6909) _mint6909(params); + else if (action == Actions.BURN_6909) _burn6909(params); + else revert UnsupportedAction(action); + } + } + + function _msgSender() internal view override returns (address) { + return _getLocker(); + } + + function _settle(bytes calldata /* params **/ ) internal { + settleCount++; + } + + function _take(bytes calldata /* params **/ ) internal { + takeCount++; + } + + function _swap(bytes calldata /* params **/ ) internal { + swapCount++; + } + + function _increaseLiquidity(bytes calldata /* params **/ ) internal { + increaseLiqCount++; + } + + function _decreaseLiquidity(bytes calldata /* params **/ ) internal { + decreaseLiqCount++; + } + + function _donate(bytes calldata /* params **/ ) internal { + donateCount++; + } + + function _mint6909(bytes calldata /* params **/ ) internal { + mintCount++; + } + + function _burn6909(bytes calldata /* params **/ ) internal { + burnCount++; + } + + function _clear(bytes calldata /* params **/ ) internal { + clearCount++; + } +} diff --git a/test/mocks/MockCalldataDecoder.sol b/test/mocks/MockCalldataDecoder.sol new file mode 100644 index 00000000..2839e6a5 --- /dev/null +++ b/test/mocks/MockCalldataDecoder.sol @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {PositionConfig} from "../../src/libraries/PositionConfig.sol"; +import {CalldataDecoder} from "../../src/libraries/CalldataDecoder.sol"; +import {IV4Router} from "../../src/interfaces/IV4Router.sol"; +import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; + +// we need to use a mock contract to make the calls happen in calldata not memory +contract MockCalldataDecoder { + using CalldataDecoder for bytes; + + function decodeModifyLiquidityParams(bytes calldata params) + external + pure + returns ( + uint256 tokenId, + PositionConfig calldata config, + uint256 liquidity, + uint128 amount0, + uint128 amount1, + bytes calldata hookData + ) + { + return params.decodeModifyLiquidityParams(); + } + + function decodeBurnParams(bytes calldata params) + external + pure + returns ( + uint256 tokenId, + PositionConfig calldata config, + uint128 amount0Min, + uint128 amount1Min, + bytes calldata hookData + ) + { + return params.decodeBurnParams(); + } + + function decodeSwapExactInParams(bytes calldata params) + external + pure + returns (IV4Router.ExactInputParams calldata swapParams) + { + return params.decodeSwapExactInParams(); + } + + function decodeSwapExactInSingleParams(bytes calldata params) + external + pure + returns (IV4Router.ExactInputSingleParams calldata swapParams) + { + return params.decodeSwapExactInSingleParams(); + } + + function decodeSwapExactOutParams(bytes calldata params) + external + pure + returns (IV4Router.ExactOutputParams calldata swapParams) + { + return params.decodeSwapExactOutParams(); + } + + function decodeSwapExactOutSingleParams(bytes calldata params) + external + pure + returns (IV4Router.ExactOutputSingleParams calldata swapParams) + { + return params.decodeSwapExactOutSingleParams(); + } + + function decodeMintParams(bytes calldata params) + external + pure + returns ( + PositionConfig calldata config, + uint256 liquidity, + uint128 amount0Max, + uint128 amount1Max, + address owner, + bytes calldata hookData + ) + { + return params.decodeMintParams(); + } + + function decodeCurrencyAndAddress(bytes calldata params) + external + pure + returns (Currency currency, address _address) + { + return params.decodeCurrencyAndAddress(); + } + + function decodeCurrency(bytes calldata params) external pure returns (Currency currency) { + return params.decodeCurrency(); + } + + function decodeCurrencyAndUint256(bytes calldata params) external pure returns (Currency currency, uint256 _uint) { + return params.decodeCurrencyAndUint256(); + } +} diff --git a/test/mocks/MockCurrencyDeltaReader.sol b/test/mocks/MockCurrencyDeltaReader.sol new file mode 100644 index 00000000..94aa3db7 --- /dev/null +++ b/test/mocks/MockCurrencyDeltaReader.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; +import {TransientStateLibrary} from "@uniswap/v4-core/src/libraries/TransientStateLibrary.sol"; +import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; +import {CurrencySettler} from "@uniswap/v4-core/test/utils/CurrencySettler.sol"; + +import {CurrencyDeltas} from "../../src/libraries/CurrencyDeltas.sol"; + +/// @dev A minimal helper strictly for testing +contract MockCurrencyDeltaReader { + using TransientStateLibrary for IPoolManager; + using CurrencyDeltas for IPoolManager; + using CurrencySettler for Currency; + + IPoolManager public poolManager; + + address sender; + + constructor(IPoolManager _poolManager) { + poolManager = _poolManager; + } + + /// @param calls an array of abi.encodeWithSelector + function execute(bytes[] calldata calls, Currency currency0, Currency currency1) external returns (BalanceDelta) { + sender = msg.sender; + return abi.decode(poolManager.unlock(abi.encode(calls, currency0, currency1)), (BalanceDelta)); + } + + function unlockCallback(bytes calldata data) external returns (bytes memory) { + (bytes[] memory calls, Currency currency0, Currency currency1) = abi.decode(data, (bytes[], Currency, Currency)); + for (uint256 i; i < calls.length; i++) { + (bool success,) = address(this).call(calls[i]); + if (!success) revert("CurrencyDeltaReader"); + } + + BalanceDelta delta = poolManager.currencyDeltas(address(this), currency0, currency1); + int256 delta0 = poolManager.currencyDelta(address(this), currency0); + int256 delta1 = poolManager.currencyDelta(address(this), currency1); + + // confirm agreement between currencyDeltas and single-read currencyDelta + require(delta.amount0() == int128(delta0), "CurrencyDeltaReader: delta0"); + require(delta.amount1() == int128(delta1), "CurrencyDeltaReader: delta1"); + + // close deltas + if (delta.amount0() < 0) currency0.settle(poolManager, sender, uint256(-int256(delta.amount0())), false); + if (delta.amount1() < 0) currency1.settle(poolManager, sender, uint256(-int256(delta.amount1())), false); + if (delta.amount0() > 0) currency0.take(poolManager, sender, uint256(int256(delta.amount0())), false); + if (delta.amount1() > 0) currency1.take(poolManager, sender, uint256(int256(delta.amount1())), false); + return abi.encode(delta); + } + + function settle(Currency currency, uint256 amount) external { + currency.settle(poolManager, sender, amount, false); + } + + function burn(Currency currency, uint256 amount) external { + currency.settle(poolManager, sender, amount, true); + } + + function take(Currency currency, uint256 amount) external { + currency.take(poolManager, sender, amount, false); + } + + function mint(Currency currency, uint256 amount) external { + currency.take(poolManager, sender, amount, true); + } +} diff --git a/test/mocks/MockDeltaResolver.sol b/test/mocks/MockDeltaResolver.sol new file mode 100644 index 00000000..4a3941e4 --- /dev/null +++ b/test/mocks/MockDeltaResolver.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.24; + +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {IUnlockCallback} from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol"; +import {DeltaResolver} from "../../src/base/DeltaResolver.sol"; +import {ImmutableState} from "../../src/base/ImmutableState.sol"; +import {ERC20} from "solmate/src/tokens/ERC20.sol"; +import {Test} from "forge-std/Test.sol"; + +contract MockDeltaResolver is Test, DeltaResolver, IUnlockCallback { + using CurrencyLibrary for Currency; + + uint256 public payCallCount; + + constructor(IPoolManager _poolManager) ImmutableState(_poolManager) {} + + function executeTest(Currency currency, uint256 amount) external { + poolManager.unlock(abi.encode(currency, msg.sender, amount)); + } + + function unlockCallback(bytes calldata data) external returns (bytes memory) { + (Currency currency, address caller, uint256 amount) = abi.decode(data, (Currency, address, uint256)); + address recipient = (currency.isNative()) ? address(this) : caller; + + uint256 balanceBefore = currency.balanceOf(recipient); + _take(currency, recipient, amount); + uint256 balanceAfter = currency.balanceOf(recipient); + + assertEq(balanceBefore + amount, balanceAfter); + + balanceBefore = balanceAfter; + _settle(currency, recipient, amount); + balanceAfter = currency.balanceOf(recipient); + + assertEq(balanceBefore - amount, balanceAfter); + + return ""; + } + + function _pay(Currency token, address payer, uint256 amount) internal override { + ERC20(Currency.unwrap(token)).transferFrom(payer, address(poolManager), amount); + payCallCount++; + } + + // needs to receive native tokens from the `take` call + receive() external payable {} +} diff --git a/test/mocks/MockMulticall.sol b/test/mocks/MockMulticall.sol new file mode 100644 index 00000000..4b96d915 --- /dev/null +++ b/test/mocks/MockMulticall.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.20; + +import "../../src/base/Multicall.sol"; + +contract MockMulticall is Multicall { + struct Tuple { + uint256 a; + uint256 b; + } + + uint256 public msgValue; + uint256 public msgValueDouble; + + function functionThatRevertsWithError(string memory error) external pure { + revert(error); + } + + function functionThatReturnsTuple(uint256 a, uint256 b) external pure returns (Tuple memory tuple) { + tuple = Tuple({a: a, b: b}); + } + + function payableStoresMsgValue() external payable { + msgValue = msg.value; + } + + function payableStoresMsgValueDouble() external payable { + msgValueDouble = 2 * msg.value; + } + + function returnSender() external view returns (address) { + return msg.sender; + } +} diff --git a/test/mocks/MockSafeCallback.sol b/test/mocks/MockSafeCallback.sol new file mode 100644 index 00000000..232fbe3c --- /dev/null +++ b/test/mocks/MockSafeCallback.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.20; + +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; + +import "../../src/base/SafeCallback.sol"; + +contract MockSafeCallback is SafeCallback { + constructor(IPoolManager _poolManager) SafeCallback(_poolManager) {} + + function unlockManager(uint256 num) external returns (bytes memory) { + return poolManager.unlock(abi.encode(num)); + } + + function _unlockCallback(bytes calldata data) internal pure override returns (bytes memory) { + return data; + } +} diff --git a/test/mocks/MockV4Router.sol b/test/mocks/MockV4Router.sol new file mode 100644 index 00000000..e821edef --- /dev/null +++ b/test/mocks/MockV4Router.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import {V4Router} from "../../src/V4Router.sol"; +import {ReentrancyLock} from "../../src/base/ReentrancyLock.sol"; +import {SafeTransferLib} from "solmate/src/utils/SafeTransferLib.sol"; +import {ERC20} from "solmate/src/tokens/ERC20.sol"; + +contract MockV4Router is V4Router, ReentrancyLock { + using SafeTransferLib for *; + using CurrencyLibrary for Currency; + + constructor(IPoolManager _poolManager) V4Router(_poolManager) {} + + function executeActions(bytes calldata params) external payable isNotLocked { + _executeActions(params); + } + + function executeActionsAndSweepExcessETH(bytes calldata params) external payable isNotLocked { + _executeActions(params); + + uint256 balance = address(this).balance; + if (balance > 0) { + msg.sender.safeTransferETH(balance); + } + } + + function _pay(Currency token, address payer, uint256 amount) internal override { + if (payer == address(this)) { + token.transfer(address(poolManager), amount); + } else { + ERC20(Currency.unwrap(token)).safeTransferFrom(payer, address(poolManager), amount); + } + } + + function _msgSender() internal view override returns (address) { + return _getLocker(); + } +} diff --git a/test/mocks/ReentrantToken.sol b/test/mocks/ReentrantToken.sol new file mode 100644 index 00000000..63cc71ee --- /dev/null +++ b/test/mocks/ReentrantToken.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.20; + +import {MockERC20} from "solmate/src/test/utils/mocks/MockERC20.sol"; +import {IPositionManager} from "../../src/interfaces/IPositionManager.sol"; + +contract ReentrantToken is MockERC20 { + IPositionManager immutable posm; + + constructor(IPositionManager _posm) MockERC20("Reentrant Token", "RT", 18) { + posm = _posm; + } + + function transferFrom(address, /*from*/ address, /*to*/ uint256 /*amount*/ ) public override returns (bool) { + // we dont need data because itll revert before it does anything + posm.modifyLiquidities("", type(uint256).max); + return true; + } +} diff --git a/test/position-managers/Execute.t.sol b/test/position-managers/Execute.t.sol new file mode 100644 index 00000000..d75f3ac7 --- /dev/null +++ b/test/position-managers/Execute.t.sol @@ -0,0 +1,241 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "forge-std/Test.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"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {FixedPointMathLib} from "solmate/src/utils/FixedPointMathLib.sol"; +import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; +import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; +import {LiquidityAmounts} from "@uniswap/v4-core/test/utils/LiquidityAmounts.sol"; +import {Position} from "@uniswap/v4-core/src/libraries/Position.sol"; + +import {IERC20} from "forge-std/interfaces/IERC20.sol"; + +import {IPositionManager} from "../../src/interfaces/IPositionManager.sol"; +import {PositionManager} from "../../src/PositionManager.sol"; +import {PositionConfig} from "../../src/libraries/PositionConfig.sol"; +import {Actions} from "../../src/libraries/Actions.sol"; + +import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; +import {Planner, Plan} from "../shared/Planner.sol"; +import {PosmTestSetup} from "../shared/PosmTestSetup.sol"; + +contract ExecuteTest is Test, PosmTestSetup, LiquidityFuzzers { + using FixedPointMathLib for uint256; + using CurrencyLibrary for Currency; + using PoolIdLibrary for PoolKey; + using Planner for Plan; + using StateLibrary for IPoolManager; + + PoolId poolId; + address alice = makeAddr("ALICE"); + address bob = makeAddr("BOB"); + + PositionConfig config; + + function setUp() public { + deployFreshManagerAndRouters(); + deployMintAndApprove2Currencies(); + + // 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); + + // Requires currency0 and currency1 to be set in base Deployers contract. + deployAndApprovePosm(manager); + + // Give tokens to Alice and Bob. + seedBalance(alice); + seedBalance(bob); + + // Approve posm for Alice and bob. + approvePosmFor(alice); + approvePosmFor(bob); + + // define a reusable pool position + config = PositionConfig({poolKey: key, tickLower: -300, tickUpper: 300}); + } + + function test_fuzz_execute_increaseLiquidity_once(uint256 initialLiquidity, uint256 liquidityToAdd) public { + initialLiquidity = bound(initialLiquidity, 1e18, 1000e18); + liquidityToAdd = bound(liquidityToAdd, 1e18, 1000e18); + uint256 tokenId = lpm.nextTokenId(); + mint(config, initialLiquidity, address(this), ZERO_BYTES); + + increaseLiquidity(tokenId, config, liquidityToAdd, ZERO_BYTES); + + bytes32 positionId = + Position.calculatePositionKey(address(lpm), config.tickLower, config.tickUpper, bytes32(tokenId)); + (uint256 liquidity,,) = manager.getPositionInfo(config.poolKey.toId(), positionId); + + assertEq(liquidity, initialLiquidity + liquidityToAdd); + } + + function test_fuzz_execute_increaseLiquidity_twice( + uint256 initialLiquidity, + uint256 liquidityToAdd, + uint256 liquidityToAdd2 + ) public { + initialLiquidity = bound(initialLiquidity, 1e18, 1000e18); + liquidityToAdd = bound(liquidityToAdd, 1e18, 1000e18); + liquidityToAdd2 = bound(liquidityToAdd2, 1e18, 1000e18); + uint256 tokenId = lpm.nextTokenId(); + mint(config, initialLiquidity, address(this), ZERO_BYTES); + + Plan memory planner = Planner.init(); + + planner.add( + Actions.INCREASE_LIQUIDITY, + abi.encode(tokenId, config, liquidityToAdd, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, ZERO_BYTES) + ); + planner.add( + Actions.INCREASE_LIQUIDITY, + abi.encode(tokenId, config, liquidityToAdd2, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, ZERO_BYTES) + ); + + bytes memory calls = planner.finalizeModifyLiquidity(config.poolKey); + lpm.modifyLiquidities(calls, _deadline); + + bytes32 positionId = + Position.calculatePositionKey(address(lpm), config.tickLower, config.tickUpper, bytes32(tokenId)); + (uint256 liquidity,,) = manager.getPositionInfo(config.poolKey.toId(), positionId); + + assertEq(liquidity, initialLiquidity + liquidityToAdd + liquidityToAdd2); + } + + // this case doesnt make sense in real world usage, so it doesnt have a cool name. but its a good test case + function test_fuzz_execute_mintAndIncrease(uint256 initialLiquidity, uint256 liquidityToAdd) public { + initialLiquidity = bound(initialLiquidity, 1e18, 1000e18); + liquidityToAdd = bound(liquidityToAdd, 1e18, 1000e18); + + uint256 tokenId = lpm.nextTokenId(); // assume that the .mint() produces tokenId=1, to be used in increaseLiquidity + + Plan memory planner = Planner.init(); + + planner.add( + Actions.MINT_POSITION, + abi.encode( + config, initialLiquidity, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, address(this), ZERO_BYTES + ) + ); + planner.add( + Actions.INCREASE_LIQUIDITY, + abi.encode(tokenId, config, liquidityToAdd, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, ZERO_BYTES) + ); + + bytes memory calls = planner.finalizeModifyLiquidity(config.poolKey); + lpm.modifyLiquidities(calls, _deadline); + + bytes32 positionId = + Position.calculatePositionKey(address(lpm), config.tickLower, config.tickUpper, bytes32(tokenId)); + (uint256 liquidity,,) = manager.getPositionInfo(config.poolKey.toId(), positionId); + + assertEq(liquidity, initialLiquidity + liquidityToAdd); + } + + // rebalance: burn and mint + function test_execute_rebalance_perfect() public { + uint256 initialLiquidity = 100e18; + + // mint a position on range [-300, 300] + uint256 tokenId = lpm.nextTokenId(); + mint(config, initialLiquidity, address(this), ZERO_BYTES); + BalanceDelta delta = getLastDelta(); + + // we'll burn and mint a new position on [-60, 60]; calculate the liquidity units for the new range + PositionConfig memory newConfig = PositionConfig({poolKey: config.poolKey, tickLower: -60, tickUpper: 60}); + uint128 newLiquidity = LiquidityAmounts.getLiquidityForAmounts( + SQRT_PRICE_1_1, + TickMath.getSqrtPriceAtTick(newConfig.tickLower), + TickMath.getSqrtPriceAtTick(newConfig.tickUpper), + uint128(-delta.amount0()), + uint128(-delta.amount1()) + ); + + uint256 balance0Before = currency0.balanceOfSelf(); + uint256 balance1Before = currency1.balanceOfSelf(); + + hook.clearDeltas(); // clear the delta so that we can check the net delta for BURN & MINT + + Plan memory planner = Planner.init(); + planner.add( + Actions.BURN_POSITION, + abi.encode( + tokenId, config, uint128(-delta.amount0()) - 1 wei, uint128(-delta.amount1()) - 1 wei, ZERO_BYTES + ) + ); + planner.add( + Actions.MINT_POSITION, + abi.encode(newConfig, newLiquidity, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, address(this), ZERO_BYTES) + ); + bytes memory calls = planner.finalizeModifyLiquidity(config.poolKey); + + lpm.modifyLiquidities(calls, _deadline); + { + BalanceDelta netDelta = getNetDelta(); + + uint256 balance0After = currency0.balanceOfSelf(); + uint256 balance1After = currency1.balanceOfSelf(); + + // TODO: use clear so user does not pay 1 wei + assertEq(netDelta.amount0(), -1 wei); + assertEq(netDelta.amount1(), -1 wei); + assertApproxEqAbs(balance0Before - balance0After, 0, 1 wei); + assertApproxEqAbs(balance1Before - balance1After, 0, 1 wei); + } + + // old position was burned + vm.expectRevert(); + lpm.ownerOf(tokenId); + + { + // old position has no liquidity + bytes32 positionId = + Position.calculatePositionKey(address(lpm), config.tickLower, config.tickUpper, bytes32(tokenId)); + uint128 liquidity = manager.getPositionLiquidity(config.poolKey.toId(), positionId); + assertEq(liquidity, 0); + + // new token was minted + uint256 newTokenId = lpm.nextTokenId() - 1; + assertEq(lpm.ownerOf(newTokenId), address(this)); + + // new token has expected liquidity + positionId = Position.calculatePositionKey( + address(lpm), newConfig.tickLower, newConfig.tickUpper, bytes32(newTokenId) + ); + liquidity = manager.getPositionLiquidity(config.poolKey.toId(), positionId); + assertEq(liquidity, newLiquidity); + } + } + + // coalesce: burn and increase + function test_execute_coalesce() public {} + // split: decrease and mint + function test_execute_split() public {} + // shift: decrease and increase + function test_execute_shift() public {} + // shard: collect and mint + function test_execute_shard() public {} + // feed: collect and increase + function test_execute_feed() public {} + + // transplant: burn and mint on different keys + function test_execute_transplant() public {} + // cross-coalesce: burn and increase on different keys + function test_execute_crossCoalesce() public {} + // cross-split: decrease and mint on different keys + function test_execute_crossSplit() public {} + // cross-shift: decrease and increase on different keys + function test_execute_crossShift() public {} + // cross-shard: collect and mint on different keys + function test_execute_crossShard() public {} + // cross-feed: collect and increase on different keys + function test_execute_crossFeed() public {} +} diff --git a/test/position-managers/FeeCollection.t.sol b/test/position-managers/FeeCollection.t.sol new file mode 100644 index 00000000..54ad44ad --- /dev/null +++ b/test/position-managers/FeeCollection.t.sol @@ -0,0 +1,355 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "forge-std/Test.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"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {FixedPointMathLib} from "solmate/src/utils/FixedPointMathLib.sol"; + +import {IERC20} from "forge-std/interfaces/IERC20.sol"; + +import {PositionManager} from "../../src/PositionManager.sol"; +import {PositionConfig} from "../../src/libraries/PositionConfig.sol"; + +import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; +import {PosmTestSetup} from "../shared/PosmTestSetup.sol"; +import {FeeMath} from "../shared/FeeMath.sol"; +import {IPositionManager} from "../../src/interfaces/IPositionManager.sol"; + +contract FeeCollectionTest is Test, PosmTestSetup, LiquidityFuzzers { + using FixedPointMathLib for uint256; + using CurrencyLibrary for Currency; + using FeeMath for IPositionManager; + + PoolId poolId; + address alice = makeAddr("ALICE"); + address bob = makeAddr("BOB"); + + // expresses the fee as a wad (i.e. 3000 = 0.003e18) + uint256 FEE_WAD; + + function setUp() public { + deployFreshManagerAndRouters(); + deployMintAndApprove2Currencies(); + + // 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); + FEE_WAD = uint256(key.fee).mulDivDown(FixedPointMathLib.WAD, 1_000_000); + + // Requires currency0 and currency1 to be set in base Deployers contract. + deployAndApprovePosm(manager); + + // Give tokens to Alice and Bob. + seedBalance(alice); + seedBalance(bob); + + // Approve posm for Alice and bob. + approvePosmFor(alice); + approvePosmFor(bob); + } + + // asserts that donations agree with feesOwed helper function + function test_fuzz_getFeesOwed_donate(uint256 feeRevenue0, uint256 feeRevenue1) public { + feeRevenue0 = bound(feeRevenue0, 0, 100_000_000 ether); + feeRevenue1 = bound(feeRevenue1, 0, 100_000_000 ether); + + PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); + uint256 tokenId = lpm.nextTokenId(); + mint(config, 10e18, address(this), ZERO_BYTES); + + // donate to generate fee revenue + donateRouter.donate(key, feeRevenue0, feeRevenue1, ZERO_BYTES); + + BalanceDelta expectedFees = IPositionManager(address(lpm)).getFeesOwed(manager, config, tokenId); + assertApproxEqAbs(uint128(expectedFees.amount0()), feeRevenue0, 1 wei); // imprecision 😅 + assertApproxEqAbs(uint128(expectedFees.amount1()), feeRevenue1, 1 wei); + } + + function test_fuzz_collect_erc20(IPoolManager.ModifyLiquidityParams memory params) public { + params.liquidityDelta = bound(params.liquidityDelta, 10e18, 10_000e18); + uint256 tokenId; + (tokenId, params) = addFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); + vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity + + PositionConfig memory config = + PositionConfig({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); + + // swap to create fees + uint256 swapAmount = 0.01e18; + swap(key, false, -int256(swapAmount), ZERO_BYTES); + + BalanceDelta expectedFees = IPositionManager(address(lpm)).getFeesOwed(manager, config, tokenId); + + // collect fees + uint256 balance0Before = currency0.balanceOfSelf(); + uint256 balance1Before = currency1.balanceOfSelf(); + + collect(tokenId, config, ZERO_BYTES); + BalanceDelta delta = getLastDelta(); + + assertApproxEqAbs(uint256(int256(delta.amount1())), swapAmount.mulWadDown(FEE_WAD), 1 wei); + assertEq(uint256(int256(delta.amount1())), uint256(int256(expectedFees.amount1()))); + assertEq(uint256(int256(delta.amount0())), uint256(int256(expectedFees.amount0()))); + + assertEq(uint256(int256(delta.amount0())), currency0.balanceOfSelf() - balance0Before); + assertEq(uint256(int256(delta.amount1())), currency1.balanceOfSelf() - balance1Before); + } + + function test_fuzz_collect_sameRange_erc20( + IPoolManager.ModifyLiquidityParams memory params, + uint256 liquidityDeltaBob + ) public { + params.liquidityDelta = bound(params.liquidityDelta, 10e18, 10_000e18); + params = createFuzzyLiquidityParams(key, params, SQRT_PRICE_1_1); + vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity + + liquidityDeltaBob = bound(liquidityDeltaBob, 100e18, 100_000e18); + + PositionConfig memory config = + PositionConfig({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); + vm.startPrank(alice); + uint256 tokenIdAlice = lpm.nextTokenId(); + mint(config, uint256(params.liquidityDelta), alice, ZERO_BYTES); + vm.stopPrank(); + + vm.startPrank(bob); + uint256 tokenIdBob = lpm.nextTokenId(); + mint(config, liquidityDeltaBob, bob, ZERO_BYTES); + vm.stopPrank(); + + // confirm the positions are same range + // (, int24 tickLowerAlice, int24 tickUpperAlice) = lpm.tokenRange(tokenIdAlice); + // (, int24 tickLowerBob, int24 tickUpperBob) = lpm.tokenRange(tokenIdBob); + // assertEq(tickLowerAlice, tickLowerBob); + // assertEq(tickUpperAlice, tickUpperBob); + + // swap to create fees + uint256 swapAmount = 0.01e18; + swap(key, false, -int256(swapAmount), ZERO_BYTES); + + // alice collects only her fees + uint256 balance0AliceBefore = currency0.balanceOf(alice); + uint256 balance1AliceBefore = currency1.balanceOf(alice); + vm.startPrank(alice); + collect(tokenIdAlice, config, ZERO_BYTES); + vm.stopPrank(); + BalanceDelta delta = getLastDelta(); + uint256 balance0AliceAfter = currency0.balanceOf(alice); + uint256 balance1AliceAfter = currency1.balanceOf(alice); + + assertEq(balance0AliceBefore, balance0AliceAfter); + assertEq(uint256(uint128(delta.amount1())), balance1AliceAfter - balance1AliceBefore); + assertTrue(delta.amount1() != 0); + + // bob collects only his fees + uint256 balance0BobBefore = currency0.balanceOf(bob); + uint256 balance1BobBefore = currency1.balanceOf(bob); + vm.startPrank(bob); + collect(tokenIdBob, config, ZERO_BYTES); + vm.stopPrank(); + delta = getLastDelta(); + uint256 balance0BobAfter = currency0.balanceOf(bob); + uint256 balance1BobAfter = currency1.balanceOf(bob); + + assertEq(balance0BobBefore, balance0BobAfter); + assertEq(uint256(uint128(delta.amount1())), balance1BobAfter - balance1BobBefore); + assertTrue(delta.amount1() != 0); + + // position manager should never hold fees + assertEq(manager.balanceOf(address(lpm), currency0.toId()), 0); + assertEq(manager.balanceOf(address(lpm), currency1.toId()), 0); + } + + function test_collect_donate() public { + PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); + uint256 tokenId = lpm.nextTokenId(); + mint(config, 10e18, address(this), ZERO_BYTES); + + // donate to generate fee revenue + uint256 feeRevenue = 1e18; + donateRouter.donate(key, feeRevenue, feeRevenue, ZERO_BYTES); + + BalanceDelta expectedFees = IPositionManager(address(lpm)).getFeesOwed(manager, config, tokenId); + + // collect fees + uint256 balance0Before = currency0.balanceOfSelf(); + uint256 balance1Before = currency1.balanceOfSelf(); + collect(tokenId, config, ZERO_BYTES); + BalanceDelta delta = getLastDelta(); + + assertApproxEqAbs(uint256(int256(delta.amount0())), feeRevenue, 1 wei); + assertApproxEqAbs(uint256(int256(delta.amount1())), feeRevenue, 1 wei); + assertEq(delta.amount0(), expectedFees.amount0()); + assertEq(delta.amount1(), expectedFees.amount1()); + + assertEq(balance0Before + uint256(uint128(delta.amount0())), currency0.balanceOfSelf()); + assertEq(balance1Before + uint256(uint128(delta.amount1())), currency1.balanceOfSelf()); + } + + function test_collect_donate_sameRange() public { + // alice and bob create liquidity on the same range [-120, 120] + PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); + + // alice provisions 3x the amount of liquidity as bob + uint256 liquidityAlice = 3000e18; + uint256 liquidityBob = 1000e18; + + vm.startPrank(alice); + uint256 tokenIdAlice = lpm.nextTokenId(); + mint(config, liquidityAlice, alice, ZERO_BYTES); + vm.stopPrank(); + + vm.startPrank(bob); + uint256 tokenIdBob = lpm.nextTokenId(); + mint(config, liquidityBob, bob, ZERO_BYTES); + vm.stopPrank(); + + // donate to generate fee revenue + uint256 feeRevenue0 = 1e18; + uint256 feeRevenue1 = 0.1e18; + donateRouter.donate(key, feeRevenue0, feeRevenue1, ZERO_BYTES); + + { + // alice collects her share + BalanceDelta expectedFeesAlice = IPositionManager(address(lpm)).getFeesOwed(manager, config, tokenIdAlice); + assertApproxEqAbs( + uint128(expectedFeesAlice.amount0()), + feeRevenue0.mulDivDown(liquidityAlice, liquidityAlice + liquidityBob), + 1 wei + ); + assertApproxEqAbs( + uint128(expectedFeesAlice.amount1()), + feeRevenue1.mulDivDown(liquidityAlice, liquidityAlice + liquidityBob), + 1 wei + ); + + uint256 balance0BeforeAlice = currency0.balanceOf(alice); + uint256 balance1BeforeAlice = currency1.balanceOf(alice); + vm.startPrank(alice); + collect(tokenIdAlice, config, ZERO_BYTES); + BalanceDelta deltaAlice = getLastDelta(); + vm.stopPrank(); + + assertEq(deltaAlice.amount0(), expectedFeesAlice.amount0()); + assertEq(deltaAlice.amount1(), expectedFeesAlice.amount1()); + assertEq(currency0.balanceOf(alice), balance0BeforeAlice + uint256(uint128(expectedFeesAlice.amount0()))); + assertEq(currency1.balanceOf(alice), balance1BeforeAlice + uint256(uint128(expectedFeesAlice.amount1()))); + } + + { + // bob collects his share + BalanceDelta expectedFeesBob = IPositionManager(address(lpm)).getFeesOwed(manager, config, tokenIdBob); + assertApproxEqAbs( + uint128(expectedFeesBob.amount0()), + feeRevenue0.mulDivDown(liquidityBob, liquidityAlice + liquidityBob), + 1 wei + ); + assertApproxEqAbs( + uint128(expectedFeesBob.amount1()), + feeRevenue1.mulDivDown(liquidityBob, liquidityAlice + liquidityBob), + 1 wei + ); + + uint256 balance0BeforeBob = currency0.balanceOf(bob); + uint256 balance1BeforeBob = currency1.balanceOf(bob); + vm.startPrank(bob); + collect(tokenIdBob, config, ZERO_BYTES); + BalanceDelta deltaBob = getLastDelta(); + vm.stopPrank(); + + assertEq(deltaBob.amount0(), expectedFeesBob.amount0()); + assertEq(deltaBob.amount1(), expectedFeesBob.amount1()); + assertEq(currency0.balanceOf(bob), balance0BeforeBob + uint256(uint128(expectedFeesBob.amount0()))); + assertEq(currency1.balanceOf(bob), balance1BeforeBob + uint256(uint128(expectedFeesBob.amount1()))); + } + } + + /// @dev Alice and Bob create liquidity on the same config, and decrease their liquidity + // Even though their positions are the same config, they are unique positions in pool manager. + function test_decreaseLiquidity_sameRange_exact() public { + // alice and bob create liquidity on the same range [-120, 120] + PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); + + // alice provisions 3x the amount of liquidity as bob + uint256 liquidityAlice = 3000e18; + uint256 liquidityBob = 1000e18; + + uint256 tokenIdAlice = lpm.nextTokenId(); + vm.startPrank(alice); + mint(config, liquidityAlice, alice, ZERO_BYTES); + vm.stopPrank(); + BalanceDelta lpDeltaAlice = getLastDelta(); + + uint256 tokenIdBob = lpm.nextTokenId(); + vm.startPrank(bob); + mint(config, liquidityBob, bob, ZERO_BYTES); + vm.stopPrank(); + BalanceDelta lpDeltaBob = getLastDelta(); + + // swap to create fees + uint256 swapAmount = 0.001e18; + swap(key, true, -int256(swapAmount), ZERO_BYTES); // zeroForOne is true, so zero is the input + swap(key, false, -int256(swapAmount), ZERO_BYTES); // move the price back, // zeroForOne is false, so one is the input + + uint256 tolerance = 0.000000001 ether; + + { + uint256 aliceBalance0Before = IERC20(Currency.unwrap(currency0)).balanceOf(address(alice)); + uint256 aliceBalance1Before = IERC20(Currency.unwrap(currency1)).balanceOf(address(alice)); + // alice decreases liquidity + vm.startPrank(alice); + decreaseLiquidity(tokenIdAlice, config, liquidityAlice, ZERO_BYTES); + vm.stopPrank(); + + // alice has accrued her principle liquidity + any fees in token0 + assertApproxEqAbs( + IERC20(Currency.unwrap(currency0)).balanceOf(address(alice)) - aliceBalance0Before, + uint256(int256(-lpDeltaAlice.amount0())) + + swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityAlice, liquidityAlice + liquidityBob), + tolerance + ); + // alice has accrued her principle liquidity + any fees in token1 + assertApproxEqAbs( + IERC20(Currency.unwrap(currency1)).balanceOf(address(alice)) - aliceBalance1Before, + uint256(int256(-lpDeltaAlice.amount1())) + + swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityAlice, liquidityAlice + liquidityBob), + tolerance + ); + } + + { + uint256 bobBalance0Before = IERC20(Currency.unwrap(currency0)).balanceOf(address(bob)); + uint256 bobBalance1Before = IERC20(Currency.unwrap(currency1)).balanceOf(address(bob)); + // bob decreases half of his liquidity + vm.startPrank(bob); + decreaseLiquidity(tokenIdBob, config, liquidityBob / 2, ZERO_BYTES); + vm.stopPrank(); + + // bob has accrued half his principle liquidity + any fees in token0 + assertApproxEqAbs( + IERC20(Currency.unwrap(currency0)).balanceOf(address(bob)) - bobBalance0Before, + uint256(int256(-lpDeltaBob.amount0()) / 2) + + swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityBob, liquidityAlice + liquidityBob), + tolerance + ); + // bob has accrued half his principle liquidity + any fees in token0 + assertApproxEqAbs( + IERC20(Currency.unwrap(currency1)).balanceOf(address(bob)) - bobBalance1Before, + uint256(int256(-lpDeltaBob.amount1()) / 2) + + swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityBob, liquidityAlice + liquidityBob), + tolerance + ); + } + } + + // TODO: ERC6909 Support. + function test_collect_6909() public {} + function test_collect_sameRange_6909() public {} +} diff --git a/test/position-managers/IncreaseLiquidity.t.sol b/test/position-managers/IncreaseLiquidity.t.sol new file mode 100644 index 00000000..49e057c6 --- /dev/null +++ b/test/position-managers/IncreaseLiquidity.t.sol @@ -0,0 +1,714 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "forge-std/Test.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"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {LiquidityAmounts} from "@uniswap/v4-core/test/utils/LiquidityAmounts.sol"; +import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; +import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; +import {FixedPointMathLib} from "solmate/src/utils/FixedPointMathLib.sol"; +import {Fuzzers} from "@uniswap/v4-core/src/test/Fuzzers.sol"; +import {Position} from "@uniswap/v4-core/src/libraries/Position.sol"; +import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; + +import {IERC20} from "forge-std/interfaces/IERC20.sol"; + +import {PositionManager} from "../../src/PositionManager.sol"; +import {PositionConfig} from "../../src/libraries/PositionConfig.sol"; +import {SlippageCheckLibrary} from "../../src/libraries/SlippageCheck.sol"; +import {IPositionManager} from "../../src/interfaces/IPositionManager.sol"; +import {Actions} from "../../src/libraries/Actions.sol"; +import {Planner, Plan} from "../shared/Planner.sol"; +import {FeeMath} from "../shared/FeeMath.sol"; +import {PosmTestSetup} from "../shared/PosmTestSetup.sol"; + +contract IncreaseLiquidityTest is Test, PosmTestSetup, Fuzzers { + using FixedPointMathLib for uint256; + using CurrencyLibrary for Currency; + using PoolIdLibrary for PoolKey; + using Planner for Plan; + using FeeMath for IPositionManager; + using StateLibrary for IPoolManager; + + PoolId poolId; + address alice = makeAddr("ALICE"); + address bob = makeAddr("BOB"); + + // expresses the fee as a wad (i.e. 3000 = 0.003e18 = 0.30%) + uint256 FEE_WAD; + + PositionConfig config; + + // Error tolerance. + uint256 tolerance = 0.00000000001 ether; + + function setUp() public { + deployFreshManagerAndRouters(); + deployMintAndApprove2Currencies(); + + // 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); + FEE_WAD = uint256(key.fee).mulDivDown(FixedPointMathLib.WAD, 1_000_000); + + // Requires currency0 and currency1 to be set in base Deployers contract. + deployAndApprovePosm(manager); + + // Give tokens to Alice and Bob. + seedBalance(alice); + seedBalance(bob); + + // Approve posm for Alice and bob. + approvePosmFor(alice); + approvePosmFor(bob); + + // define a reusable range + config = PositionConfig({poolKey: key, tickLower: -300, tickUpper: 300}); + } + + /// @notice Increase liquidity with exact fees, taking dust + function test_increaseLiquidity_withExactFees_take() public { + // Alice and Bob provide liquidity on the range + // Alice uses her exact fees to increase liquidity (compounding) + + uint256 liquidityAlice = 3_000e18; + uint256 liquidityBob = 1_000e18; + + // alice provides liquidity + vm.startPrank(alice); + uint256 tokenIdAlice = lpm.nextTokenId(); + mint(config, liquidityAlice, alice, ZERO_BYTES); + vm.stopPrank(); + + // bob provides liquidity + vm.startPrank(bob); + mint(config, liquidityBob, bob, ZERO_BYTES); + vm.stopPrank(); + + // swap to create fees + uint256 swapAmount = 0.001e18; + swap(key, true, -int256(swapAmount), ZERO_BYTES); + swap(key, false, -int256(swapAmount), ZERO_BYTES); // move the price back + + // alice uses her exact fees to increase liquidity + // Slight error in this calculation vs. actual fees.. TODO: Fix this. + BalanceDelta feesOwedAlice = IPositionManager(lpm).getFeesOwed(manager, config, tokenIdAlice); + // Note: You can alternatively calculate Alice's fees owed from the swap amount, fee on the pool, and total liquidity in that range. + // swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityAlice, liquidityAlice + liquidityBob); + + (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, config.poolKey.toId()); + uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( + sqrtPriceX96, + TickMath.getSqrtPriceAtTick(config.tickLower), + TickMath.getSqrtPriceAtTick(config.tickUpper), + uint256(int256(feesOwedAlice.amount0())), + uint256(int256(feesOwedAlice.amount1())) + ); + + uint256 balance0BeforeAlice = currency0.balanceOf(alice); + uint256 balance1BeforeAlice = currency1.balanceOf(alice); + + Plan memory planner = Planner.init(); + planner.add( + Actions.INCREASE_LIQUIDITY, abi.encode(tokenIdAlice, config, liquidityDelta, 0 wei, 0 wei, ZERO_BYTES) + ); + bytes memory calls = planner.finalizeModifyLiquidity(config.poolKey); + vm.startPrank(alice); + lpm.modifyLiquidities(calls, _deadline); + vm.stopPrank(); + + // alice barely spent any tokens + assertApproxEqAbs(balance0BeforeAlice, currency0.balanceOf(alice), tolerance); + assertApproxEqAbs(balance1BeforeAlice, currency1.balanceOf(alice), tolerance); + } + + /// @dev Increase liquidity with exact fees, clearing dust + function test_increaseLiquidity_withExactFees_clear() public { + // Alice and Bob provide liquidity on the range + // Alice uses her exact fees to increase liquidity (compounding) + + uint256 liquidityAlice = 3_000e18; + uint256 liquidityBob = 1_000e18; + + // alice provides liquidity + vm.startPrank(alice); + uint256 tokenIdAlice = lpm.nextTokenId(); + mint(config, liquidityAlice, alice, ZERO_BYTES); + vm.stopPrank(); + + // bob provides liquidity + vm.startPrank(bob); + mint(config, liquidityBob, bob, ZERO_BYTES); + vm.stopPrank(); + + // swap to create fees + uint256 swapAmount = 0.001e18; + swap(key, true, -int256(swapAmount), ZERO_BYTES); + swap(key, false, -int256(swapAmount), ZERO_BYTES); // move the price back + + // alice uses her exact fees to increase liquidity + // Slight error in this calculation vs. actual fees.. TODO: Fix this. + BalanceDelta feesOwedAlice = IPositionManager(lpm).getFeesOwed(manager, config, tokenIdAlice); + // Note: You can alternatively calculate Alice's fees owed from the swap amount, fee on the pool, and total liquidity in that range. + // swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityAlice, liquidityAlice + liquidityBob); + + (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, config.poolKey.toId()); + uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( + sqrtPriceX96, + TickMath.getSqrtPriceAtTick(config.tickLower), + TickMath.getSqrtPriceAtTick(config.tickUpper), + uint256(int256(feesOwedAlice.amount0())), + uint256(int256(feesOwedAlice.amount1())) + ); + + uint256 balance0BeforeAlice = currency0.balanceOf(alice); + uint256 balance1BeforeAlice = currency1.balanceOf(alice); + + Plan memory planner = Planner.init(); + planner.add( + Actions.INCREASE_LIQUIDITY, + abi.encode(tokenIdAlice, config, liquidityDelta, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, ZERO_BYTES) + ); + planner.add(Actions.CLEAR, abi.encode(config.poolKey.currency0, 18 wei)); // alice is willing to forfeit 18 wei + planner.add(Actions.CLEAR, abi.encode(config.poolKey.currency1, 18 wei)); + bytes memory calls = planner.encode(); + + vm.prank(alice); + lpm.modifyLiquidities(calls, _deadline); + + // alice did not spend or receive tokens + // (alice forfeited a small amount of tokens to the pool with CLEAR) + assertEq(currency0.balanceOf(alice), balance0BeforeAlice); + assertEq(currency1.balanceOf(alice), balance1BeforeAlice); + } + + // uses donate to simulate fee revenue, taking dust + function test_increaseLiquidity_withExactFees_take_donate() public { + // Alice and Bob provide liquidity on the range + // Alice uses her exact fees to increase liquidity (compounding) + + uint256 liquidityAlice = 3_000e18; + uint256 liquidityBob = 1_000e18; + + // alice provides liquidity + vm.startPrank(alice); + uint256 tokenIdAlice = lpm.nextTokenId(); + mint(config, liquidityAlice, alice, ZERO_BYTES); + vm.stopPrank(); + + // bob provides liquidity + vm.startPrank(bob); + mint(config, liquidityBob, bob, ZERO_BYTES); + vm.stopPrank(); + + // donate to create fees + uint256 amountDonate = 0.2e18; + donateRouter.donate(key, 0.2e18, 0.2e18, ZERO_BYTES); + + // subtract 1 cause we'd rather take than pay + uint256 feesAmount = amountDonate.mulDivDown(liquidityAlice, liquidityAlice + liquidityBob) - 1; + + (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, config.poolKey.toId()); + uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( + sqrtPriceX96, + TickMath.getSqrtPriceAtTick(config.tickLower), + TickMath.getSqrtPriceAtTick(config.tickUpper), + feesAmount, + feesAmount + ); + + uint256 balance0BeforeAlice = currency0.balanceOf(alice); + uint256 balance1BeforeAlice = currency1.balanceOf(alice); + + vm.startPrank(alice); + increaseLiquidity(tokenIdAlice, config, liquidityDelta, ZERO_BYTES); + vm.stopPrank(); + + // alice barely spent any tokens + assertApproxEqAbs(balance0BeforeAlice, currency0.balanceOf(alice), 1 wei); + assertApproxEqAbs(balance1BeforeAlice, currency1.balanceOf(alice), 1 wei); + } + + // uses donate to simulate fee revenue, clearing dust + function test_increaseLiquidity_withExactFees_clear_donate() public { + // Alice and Bob provide liquidity on the range + // Alice uses her exact fees to increase liquidity (compounding) + + uint256 liquidityAlice = 3_000e18; + uint256 liquidityBob = 1_000e18; + + // alice provides liquidity + vm.startPrank(alice); + uint256 tokenIdAlice = lpm.nextTokenId(); + mint(config, liquidityAlice, alice, ZERO_BYTES); + vm.stopPrank(); + + // bob provides liquidity + vm.startPrank(bob); + mint(config, liquidityBob, bob, ZERO_BYTES); + vm.stopPrank(); + + // donate to create fees + uint256 amountDonate = 0.2e18; + donateRouter.donate(key, 0.2e18, 0.2e18, ZERO_BYTES); + + // subtract 1 cause we'd rather take than pay + uint256 feesAmount = amountDonate.mulDivDown(liquidityAlice, liquidityAlice + liquidityBob) - 1; + + (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, config.poolKey.toId()); + uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( + sqrtPriceX96, + TickMath.getSqrtPriceAtTick(config.tickLower), + TickMath.getSqrtPriceAtTick(config.tickUpper), + feesAmount, + feesAmount + ); + + uint256 balance0BeforeAlice = currency0.balanceOf(alice); + uint256 balance1BeforeAlice = currency1.balanceOf(alice); + + Plan memory planner = Planner.init(); + planner.add( + Actions.INCREASE_LIQUIDITY, + abi.encode(tokenIdAlice, config, liquidityDelta, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, ZERO_BYTES) + ); + planner.add(Actions.CLEAR, abi.encode(config.poolKey.currency0, 1 wei)); // alice is willing to forfeit 1 wei + planner.add(Actions.CLEAR, abi.encode(config.poolKey.currency1, 1 wei)); + bytes memory calls = planner.encode(); + + vm.prank(alice); + lpm.modifyLiquidities(calls, _deadline); + + // alice did not spend or receive tokens + // (alice forfeited a small amount of tokens to the pool with CLEAR) + assertEq(currency0.balanceOf(alice), balance0BeforeAlice); + assertEq(currency1.balanceOf(alice), balance1BeforeAlice); + } + + function test_increaseLiquidity_withUnapprovedCaller() public { + // Alice provides liquidity + // Bob increases Alice's liquidity without being approved + uint256 liquidityAlice = 3_000e18; + + // alice provides liquidity + vm.startPrank(alice); + uint256 tokenIdAlice = lpm.nextTokenId(); + mint(config, liquidityAlice, alice, ZERO_BYTES); + vm.stopPrank(); + + bytes32 positionId = + Position.calculatePositionKey(address(lpm), config.tickLower, config.tickUpper, bytes32(tokenIdAlice)); + uint128 oldLiquidity = StateLibrary.getPositionLiquidity(manager, config.poolKey.toId(), positionId); + + // bob can increase liquidity for alice even though he is not the owner / not approved + vm.startPrank(bob); + increaseLiquidity(tokenIdAlice, config, 100e18, ZERO_BYTES); + vm.stopPrank(); + + uint128 newLiquidity = StateLibrary.getPositionLiquidity(manager, config.poolKey.toId(), positionId); + + // assert liqudity increased by the correct amount + assertEq(newLiquidity, oldLiquidity + uint128(100e18)); + } + + function test_increaseLiquidity_sameRange_withExcessFees() public { + // Alice and Bob provide liquidity on the same range + // Alice uses half her fees to increase liquidity. The other half are collected to her wallet. + // Bob collects all fees. + uint256 liquidityAlice = 3_000e18; + uint256 liquidityBob = 1_000e18; + uint256 totalLiquidity = liquidityAlice + liquidityBob; + + // alice provides liquidity + vm.startPrank(alice); + uint256 tokenIdAlice = lpm.nextTokenId(); + mint(config, liquidityAlice, alice, ZERO_BYTES); + vm.stopPrank(); + + // bob provides liquidity + vm.prank(bob); + uint256 tokenIdBob = lpm.nextTokenId(); + mint(config, liquidityBob, bob, ZERO_BYTES); + vm.stopPrank(); + + // swap to create fees + uint256 swapAmount = 0.001e18; + swap(key, true, -int256(swapAmount), ZERO_BYTES); + swap(key, false, -int256(swapAmount), ZERO_BYTES); // move the price back + + { + // alice will use half of her fees to increase liquidity + BalanceDelta aliceFeesOwed = IPositionManager(lpm).getFeesOwed(manager, config, tokenIdAlice); + + (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, config.poolKey.toId()); + uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( + sqrtPriceX96, + TickMath.getSqrtPriceAtTick(config.tickLower), + TickMath.getSqrtPriceAtTick(config.tickUpper), + uint256(int256(aliceFeesOwed.amount0() / 2)), + uint256(int256(aliceFeesOwed.amount1() / 2)) + ); + uint256 balance0BeforeAlice = currency0.balanceOf(alice); + uint256 balance1BeforeAlice = currency1.balanceOf(alice); + vm.startPrank(alice); + increaseLiquidity(tokenIdAlice, config, liquidityDelta, ZERO_BYTES); + vm.stopPrank(); + + assertApproxEqAbs( + currency0.balanceOf(alice) - balance0BeforeAlice, + swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityAlice, totalLiquidity) / 2, + tolerance + ); + assertApproxEqAbs( + currency1.balanceOf(alice) - balance1BeforeAlice, + swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityAlice, totalLiquidity) / 2, + tolerance + ); + + assertApproxEqAbs( + currency0.balanceOf(alice) - balance0BeforeAlice, uint128(aliceFeesOwed.amount0()) / 2, tolerance + ); + + assertApproxEqAbs( + currency1.balanceOf(alice) - balance1BeforeAlice, uint128(aliceFeesOwed.amount1()) / 2, tolerance + ); + } + + { + // bob collects his fees + uint256 balance0BeforeBob = currency0.balanceOf(bob); + uint256 balance1BeforeBob = currency1.balanceOf(bob); + vm.startPrank(bob); + collect(tokenIdBob, config, ZERO_BYTES); + vm.stopPrank(); + + assertApproxEqAbs( + currency0.balanceOf(bob) - balance0BeforeBob, + swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityBob, totalLiquidity), + tolerance + ); + assertApproxEqAbs( + currency1.balanceOf(bob) - balance1BeforeBob, + swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityBob, totalLiquidity), + tolerance + ); + + uint256 balance0AfterBob = currency0.balanceOf(bob); + uint256 balance1AfterBob = currency1.balanceOf(bob); + assertApproxEqAbs( + balance0AfterBob - balance0BeforeBob, + swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityBob, totalLiquidity), + 1 wei + ); + assertApproxEqAbs( + balance1AfterBob - balance1BeforeBob, + swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityBob, totalLiquidity), + 1 wei + ); + } + } + + function test_increaseLiquidity_withInsufficientFees() public { + // Alice and Bob provide liquidity on the range + // Alice uses her fees to increase liquidity. Additional funds are used by alice to increase liquidity + uint256 liquidityAlice = 3_000e18; + uint256 liquidityBob = 1_000e18; + uint256 totalLiquidity = liquidityAlice + liquidityBob; + + // alice provides liquidity + vm.startPrank(alice); + uint256 tokenIdAlice = lpm.nextTokenId(); + mint(config, liquidityAlice, alice, ZERO_BYTES); + vm.stopPrank(); + + // bob provides liquidity + vm.startPrank(bob); + uint256 tokenIdBob = lpm.nextTokenId(); + mint(config, liquidityBob, bob, ZERO_BYTES); + vm.stopPrank(); + + // swap to create fees + uint256 swapAmount = 0.001e18; + swap(key, true, -int256(swapAmount), ZERO_BYTES); + swap(key, false, -int256(swapAmount), ZERO_BYTES); // move the price back + + // alice will use all of her fees + additional capital to increase liquidity + BalanceDelta feesOwed = IPositionManager(lpm).getFeesOwed(manager, config, tokenIdAlice); + + { + (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, config.poolKey.toId()); + uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( + sqrtPriceX96, + TickMath.getSqrtPriceAtTick(config.tickLower), + TickMath.getSqrtPriceAtTick(config.tickUpper), + uint256(int256(feesOwed.amount0())) * 2, + uint256(int256(feesOwed.amount1())) * 2 + ); + + uint256 balance0BeforeAlice = currency0.balanceOf(alice); + uint256 balance1BeforeAlice = currency1.balanceOf(alice); + vm.startPrank(alice); + increaseLiquidity(tokenIdAlice, config, liquidityDelta, ZERO_BYTES); + vm.stopPrank(); + uint256 balance0AfterAlice = currency0.balanceOf(alice); + uint256 balance1AfterAlice = currency1.balanceOf(alice); + + // Alice owed feesOwed amount in 0 and 1 because she places feesOwed * 2 back into the pool. + assertApproxEqAbs(balance0BeforeAlice - balance0AfterAlice, uint256(int256(feesOwed.amount0())), tolerance); + assertApproxEqAbs(balance1BeforeAlice - balance1AfterAlice, uint256(int256(feesOwed.amount1())), tolerance); + } + + { + // bob collects his fees + uint256 balance0BeforeBob = currency0.balanceOf(bob); + uint256 balance1BeforeBob = currency1.balanceOf(bob); + vm.startPrank(bob); + collect(tokenIdBob, config, ZERO_BYTES); + vm.stopPrank(); + uint256 balance0AfterBob = currency0.balanceOf(bob); + uint256 balance1AfterBob = currency1.balanceOf(bob); + assertApproxEqAbs( + balance0AfterBob - balance0BeforeBob, + swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityBob, totalLiquidity), + tolerance + ); + assertApproxEqAbs( + balance1AfterBob - balance1BeforeBob, + swapAmount.mulWadDown(FEE_WAD).mulDivDown(liquidityBob, totalLiquidity), + tolerance + ); + } + } + + function test_increaseLiquidity_slippage_revertAmount0() public { + // increasing liquidity with strict slippage parameters (amount0) will revert + uint256 tokenId = lpm.nextTokenId(); + mint(config, 100e18, address(this), ZERO_BYTES); + + // revert since amount0Max is too low + bytes memory calls = getIncreaseEncoded(tokenId, config, 100e18, 1 wei, type(uint128).max, ZERO_BYTES); + vm.expectRevert(SlippageCheckLibrary.MaximumAmountExceeded.selector); + lpm.modifyLiquidities(calls, _deadline); + } + + function test_increaseLiquidity_slippage_revertAmount1() public { + // increasing liquidity with strict slippage parameters (amount1) will revert + uint256 tokenId = lpm.nextTokenId(); + mint(config, 100e18, address(this), ZERO_BYTES); + + // revert since amount1Max is too low + bytes memory calls = getIncreaseEncoded(tokenId, config, 100e18, type(uint128).max, 1 wei, ZERO_BYTES); + vm.expectRevert(SlippageCheckLibrary.MaximumAmountExceeded.selector); + lpm.modifyLiquidities(calls, _deadline); + } + + function test_increaseLiquidity_slippage_exactDoesNotRevert() public { + // increasing liquidity with perfect slippage parameters does not revert + uint256 tokenId = lpm.nextTokenId(); + mint(config, 100e18, address(this), ZERO_BYTES); + + uint128 newLiquidity = 10e18; + (uint256 amount0, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity( + SQRT_PRICE_1_1, + TickMath.getSqrtPriceAtTick(config.tickLower), + TickMath.getSqrtPriceAtTick(config.tickUpper), + newLiquidity + ); + assertEq(amount0, amount1); // symmetric liquidity addition + uint128 slippage = uint128(amount0) + 1; + + bytes memory calls = getIncreaseEncoded(tokenId, config, newLiquidity, slippage, slippage, ZERO_BYTES); + lpm.modifyLiquidities(calls, _deadline); + BalanceDelta delta = getLastDelta(); + + // confirm that delta == slippage tolerance + assertEq(-delta.amount0(), int128(slippage)); + assertEq(-delta.amount1(), int128(slippage)); + } + + /// price movement from swaps will cause slippage reverts + function test_increaseLiquidity_slippage_revert_swap() public { + // increasing liquidity with perfect slippage parameters does not revert + uint256 tokenId = lpm.nextTokenId(); + mint(config, 100e18, address(this), ZERO_BYTES); + + uint128 newLiquidity = 10e18; + (uint256 amount0, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity( + SQRT_PRICE_1_1, + TickMath.getSqrtPriceAtTick(config.tickLower), + TickMath.getSqrtPriceAtTick(config.tickUpper), + newLiquidity + ); + assertEq(amount0, amount1); // symmetric liquidity addition + uint128 slippage = uint128(amount0) + 1; + + // swap to create slippage + swap(key, true, -10e18, ZERO_BYTES); + + bytes memory calls = getIncreaseEncoded(tokenId, config, newLiquidity, slippage, slippage, ZERO_BYTES); + vm.expectRevert(SlippageCheckLibrary.MaximumAmountExceeded.selector); + lpm.modifyLiquidities(calls, _deadline); + } + + function test_mint_settleWithBalance() public { + uint256 liquidityAlice = 3_000e18; + + Plan memory planner = Planner.init(); + planner.add( + Actions.MINT_POSITION, + abi.encode(config, liquidityAlice, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, alice, ZERO_BYTES) + ); + planner.add(Actions.SETTLE_WITH_BALANCE, abi.encode(currency0)); + planner.add(Actions.SETTLE_WITH_BALANCE, abi.encode(currency1)); + planner.add(Actions.SWEEP, abi.encode(currency0, address(this))); + planner.add(Actions.SWEEP, abi.encode(currency1, address(this))); + + uint256 balanceBefore0 = currency0.balanceOf(address(this)); + uint256 balanceBefore1 = currency1.balanceOf(address(this)); + + assertEq(currency0.balanceOf(address(lpm)), 0); + assertEq(currency0.balanceOf(address(lpm)), 0); + + currency0.transfer(address(lpm), 100e18); + currency1.transfer(address(lpm), 100e18); + + assertEq(currency0.balanceOf(address(lpm)), 100e18); + assertEq(currency0.balanceOf(address(lpm)), 100e18); + + bytes memory calls = planner.encode(); + + vm.prank(alice); + lpm.modifyLiquidities(calls, _deadline); + BalanceDelta delta = getLastDelta(); + uint256 amount0 = uint128(-delta.amount0()); + uint256 amount1 = uint128(-delta.amount1()); + + // The balances were swept back to this address. + assertEq(IERC20(Currency.unwrap(currency0)).balanceOf(address(lpm)), 0); + assertEq(IERC20(Currency.unwrap(currency1)).balanceOf(address(lpm)), 0); + + assertEq(currency0.balanceOf(address(this)), balanceBefore0 - amount0); + assertEq(currency1.balanceOf(address(this)), balanceBefore1 - amount1); + } + + function test_increaseLiquidity_settleWithBalance() public { + uint256 liquidityAlice = 3_000e18; + + // alice provides liquidity + vm.prank(alice); + mint(config, liquidityAlice, alice, ZERO_BYTES); + uint256 tokenIdAlice = lpm.nextTokenId() - 1; + + bytes32 positionId = + Position.calculatePositionKey(address(lpm), config.tickLower, config.tickUpper, bytes32(tokenIdAlice)); + (uint256 liquidity,,) = manager.getPositionInfo(config.poolKey.toId(), positionId); + assertEq(liquidity, liquidityAlice); + + // alice increases with the balance in the position manager + Plan memory planner = Planner.init(); + planner.add( + Actions.INCREASE_LIQUIDITY, + abi.encode(tokenIdAlice, config, liquidityAlice, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, ZERO_BYTES) + ); + planner.add(Actions.SETTLE_WITH_BALANCE, abi.encode(currency0)); + planner.add(Actions.SETTLE_WITH_BALANCE, abi.encode(currency1)); + planner.add(Actions.SWEEP, abi.encode(currency0, address(this))); + planner.add(Actions.SWEEP, abi.encode(currency1, address(this))); + + uint256 balanceBefore0 = currency0.balanceOf(address(this)); + uint256 balanceBefore1 = currency1.balanceOf(address(this)); + + assertEq(currency0.balanceOf(address(lpm)), 0); + assertEq(currency0.balanceOf(address(lpm)), 0); + + currency0.transfer(address(lpm), 100e18); + currency1.transfer(address(lpm), 100e18); + + assertEq(currency0.balanceOf(address(lpm)), 100e18); + assertEq(currency0.balanceOf(address(lpm)), 100e18); + + bytes memory calls = planner.encode(); + + vm.prank(alice); + lpm.modifyLiquidities(calls, _deadline); + BalanceDelta delta = getLastDelta(); + uint256 amount0 = uint128(-delta.amount0()); + uint256 amount1 = uint128(-delta.amount1()); + + (liquidity,,) = manager.getPositionInfo(config.poolKey.toId(), positionId); + assertEq(liquidity, 2 * liquidityAlice); + + // The balances were swept back to this address. + assertEq(IERC20(Currency.unwrap(currency0)).balanceOf(address(lpm)), 0); + assertEq(IERC20(Currency.unwrap(currency1)).balanceOf(address(lpm)), 0); + + assertEq(currency0.balanceOf(address(this)), balanceBefore0 - amount0); + assertEq(currency1.balanceOf(address(this)), balanceBefore1 - amount1); + } + + function test_increaseLiquidity_clearExceeds_revert() public { + uint256 tokenId = lpm.nextTokenId(); + mint(config, 1000e18, address(this), ZERO_BYTES); + + // donate to create fee revenue + uint256 amountToDonate = 0.2e18; + donateRouter.donate(key, amountToDonate, amountToDonate, ZERO_BYTES); + + // calculate the amount of liquidity to add, using half of the proceeds + uint256 amountToReinvest = amountToDonate / 2; + uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( + SQRT_PRICE_1_1, + TickMath.getSqrtPriceAtTick(config.tickLower), + TickMath.getSqrtPriceAtTick(config.tickUpper), + amountToReinvest, + amountToReinvest + ); + + Plan memory planner = Planner.init(); + planner.add( + Actions.INCREASE_LIQUIDITY, + abi.encode(tokenId, config, liquidityDelta, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, ZERO_BYTES) + ); + planner.add(Actions.CLEAR, abi.encode(config.poolKey.currency0, amountToReinvest - 2 wei)); + planner.add(Actions.CLEAR, abi.encode(config.poolKey.currency1, amountToReinvest - 2 wei)); + bytes memory calls = planner.encode(); + + // revert since we're forfeiting beyond the max tolerance + vm.expectRevert( + abi.encodeWithSelector( + IPositionManager.ClearExceedsMaxAmount.selector, + config.poolKey.currency0, + int256(amountToReinvest - 1 wei), // imprecision, PM expects us to collect half of the fees (minus 1 wei) + uint256(amountToReinvest - 2 wei) // the maximum amount we were willing to forfeit + ) + ); + lpm.modifyLiquidities(calls, _deadline); + } + + /// @dev clearing a negative delta reverts in core with SafeCastOverflow + function test_increaseLiquidity_clearNegative_revert() public { + uint256 tokenId = lpm.nextTokenId(); + mint(config, 1000e18, address(this), ZERO_BYTES); + + // increase liquidity with new tokens but try clearing the negative delta + Plan memory planner = Planner.init(); + planner.add( + Actions.INCREASE_LIQUIDITY, + abi.encode(tokenId, config, 100e18, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, ZERO_BYTES) + ); + planner.add(Actions.CLEAR, abi.encode(config.poolKey.currency0, type(uint256).max)); + planner.add(Actions.CLEAR, abi.encode(config.poolKey.currency1, type(uint256).max)); + bytes memory calls = planner.encode(); + + // revert since we're forfeiting beyond the max tolerance + vm.expectRevert(SafeCast.SafeCastOverflow.selector); + lpm.modifyLiquidities(calls, _deadline); + } +} diff --git a/test/position-managers/NativeToken.t.sol b/test/position-managers/NativeToken.t.sol new file mode 100644 index 00000000..b7a8e901 --- /dev/null +++ b/test/position-managers/NativeToken.t.sol @@ -0,0 +1,405 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "forge-std/Test.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"; +import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol"; +import {LiquidityAmounts} from "@uniswap/v4-core/test/utils/LiquidityAmounts.sol"; +import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; +import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; +import {FixedPointMathLib} from "solmate/src/utils/FixedPointMathLib.sol"; +import {Constants} from "@uniswap/v4-core/test/utils/Constants.sol"; +import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; +import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; +import {Position} from "@uniswap/v4-core/src/libraries/Position.sol"; + +import {IERC20} from "forge-std/interfaces/IERC20.sol"; +import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import {IERC721} from "@openzeppelin/contracts/interfaces/IERC721.sol"; + +import {IPositionManager} from "../../src/interfaces/IPositionManager.sol"; +import {Actions} from "../../src/libraries/Actions.sol"; +import {PositionManager} from "../../src/PositionManager.sol"; + +import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; +import {PosmTestSetup} from "../shared/PosmTestSetup.sol"; +import {Planner, Plan} from "../shared/Planner.sol"; +import {PositionConfig, PositionConfigLibrary} from "../../src/libraries/PositionConfig.sol"; + +contract PositionManagerTest is Test, PosmTestSetup, LiquidityFuzzers { + using FixedPointMathLib for uint256; + using CurrencyLibrary for Currency; + using PositionConfigLibrary for PositionConfig; + using Planner for Plan; + using PoolIdLibrary for PoolKey; + using StateLibrary for IPoolManager; + using SafeCast for *; + + PoolId poolId; + + function setUp() public { + deployFreshManagerAndRouters(); + deployMintAndApprove2Currencies(); + + // This is needed to receive return deltas from modifyLiquidity calls. + deployPosmHookSavesDelta(); + + currency0 = CurrencyLibrary.NATIVE; + (nativeKey, poolId) = initPool(currency0, currency1, IHooks(hook), 3000, SQRT_PRICE_1_1, ZERO_BYTES); + + deployPosm(manager); + // currency0 is the native token so only execute approvals for currency1. + approvePosmCurrency(currency1); + + vm.deal(address(this), type(uint256).max); + } + + function test_fuzz_mint_native(IPoolManager.ModifyLiquidityParams memory params) public { + params = createFuzzyLiquidityParams(nativeKey, params, SQRT_PRICE_1_1); + vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // two-sided liquidity + + uint256 liquidityToAdd = + params.liquidityDelta < 0 ? uint256(-params.liquidityDelta) : uint256(params.liquidityDelta); + PositionConfig memory config = + PositionConfig({poolKey: nativeKey, tickLower: params.tickLower, tickUpper: params.tickUpper}); + + uint256 balance0Before = currency0.balanceOfSelf(); + uint256 balance1Before = currency1.balanceOfSelf(); + + uint256 tokenId = lpm.nextTokenId(); + bytes memory calls = getMintEncoded(config, liquidityToAdd, address(this), ZERO_BYTES); + + (uint256 amount0,) = LiquidityAmounts.getAmountsForLiquidity( + SQRT_PRICE_1_1, + TickMath.getSqrtPriceAtTick(params.tickLower), + TickMath.getSqrtPriceAtTick(params.tickUpper), + liquidityToAdd.toUint128() + ); + // add extra wei because modifyLiquidities may be rounding up, LiquidityAmounts is imprecise? + lpm.modifyLiquidities{value: amount0 + 1}(calls, _deadline); + BalanceDelta delta = getLastDelta(); + + bytes32 positionId = + Position.calculatePositionKey(address(lpm), config.tickLower, config.tickUpper, bytes32(tokenId)); + (uint256 liquidity,,) = manager.getPositionInfo(config.poolKey.toId(), positionId); + + assertEq(liquidity, uint256(params.liquidityDelta)); + assertEq(balance0Before - currency0.balanceOfSelf(), uint256(int256(-delta.amount0())), "incorrect amount0"); + assertEq(balance1Before - currency1.balanceOfSelf(), uint256(int256(-delta.amount1())), "incorrect amount1"); + } + + // minting with excess native tokens are returned to caller + function test_fuzz_mint_native_excess(IPoolManager.ModifyLiquidityParams memory params) public { + params = createFuzzyLiquidityParams(nativeKey, params, SQRT_PRICE_1_1); + vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // two-sided liquidity + + uint256 liquidityToAdd = + params.liquidityDelta < 0 ? uint256(-params.liquidityDelta) : uint256(params.liquidityDelta); + PositionConfig memory config = + PositionConfig({poolKey: nativeKey, tickLower: params.tickLower, tickUpper: params.tickUpper}); + + uint256 balance0Before = currency0.balanceOfSelf(); + uint256 balance1Before = currency1.balanceOfSelf(); + + uint256 tokenId = lpm.nextTokenId(); + + Plan memory planner = Planner.init(); + planner.add( + Actions.MINT_POSITION, + abi.encode(config, liquidityToAdd, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, address(this), ZERO_BYTES) + ); + planner.add(Actions.CLOSE_CURRENCY, abi.encode(nativeKey.currency0)); + planner.add(Actions.CLOSE_CURRENCY, abi.encode(nativeKey.currency1)); + // sweep the excess eth + planner.add(Actions.SWEEP, abi.encode(currency0, address(this))); + + bytes memory calls = planner.encode(); + + (uint256 amount0,) = LiquidityAmounts.getAmountsForLiquidity( + SQRT_PRICE_1_1, + TickMath.getSqrtPriceAtTick(params.tickLower), + TickMath.getSqrtPriceAtTick(params.tickUpper), + liquidityToAdd.toUint128() + ); + + // Mint with excess native tokens + lpm.modifyLiquidities{value: amount0 * 2 + 1}(calls, _deadline); + BalanceDelta delta = getLastDelta(); + + bytes32 positionId = + Position.calculatePositionKey(address(lpm), config.tickLower, config.tickUpper, bytes32(tokenId)); + (uint256 liquidity,,) = manager.getPositionInfo(config.poolKey.toId(), positionId); + assertEq(liquidity, uint256(params.liquidityDelta)); + + // only paid the delta amount, with excess tokens returned to caller + assertEq(balance0Before - currency0.balanceOfSelf(), uint256(int256(-delta.amount0()))); + assertEq(balance0Before - currency0.balanceOfSelf(), amount0 + 1); // TODO: off by one?? + assertEq(balance1Before - currency1.balanceOfSelf(), uint256(int256(-delta.amount1()))); + } + + function test_fuzz_burn_native_emptyPosition(IPoolManager.ModifyLiquidityParams memory params) public { + uint256 balance0Start = address(this).balance; + uint256 balance1Start = currency1.balanceOfSelf(); + + params = createFuzzyLiquidityParams(nativeKey, params, SQRT_PRICE_1_1); + vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // two-sided liquidity + + uint256 liquidityToAdd = + params.liquidityDelta < 0 ? uint256(-params.liquidityDelta) : uint256(params.liquidityDelta); + PositionConfig memory config = + PositionConfig({poolKey: nativeKey, tickLower: params.tickLower, tickUpper: params.tickUpper}); + + uint256 tokenId = lpm.nextTokenId(); + mintWithNative(SQRT_PRICE_1_1, config, liquidityToAdd, address(this), ZERO_BYTES); + + bytes32 positionId = + Position.calculatePositionKey(address(lpm), config.tickLower, config.tickUpper, bytes32(tokenId)); + (uint256 liquidity,,) = manager.getPositionInfo(config.poolKey.toId(), positionId); + assertEq(liquidity, uint256(params.liquidityDelta)); + + // burn liquidity + uint256 balance0BeforeBurn = currency0.balanceOfSelf(); + uint256 balance1BeforeBurn = currency1.balanceOfSelf(); + + decreaseLiquidity(tokenId, config, liquidity, ZERO_BYTES); + BalanceDelta deltaDecrease = getLastDelta(); + + uint256 numDeltas = hook.numberDeltasReturned(); + burn(tokenId, config, ZERO_BYTES); + // No decrease/modifyLiq call will actually happen on the call to burn so the deltas array will be the same length. + assertEq(numDeltas, hook.numberDeltasReturned()); + + (liquidity,,) = manager.getPositionInfo(config.poolKey.toId(), positionId); + assertEq(liquidity, 0); + + // TODO: slightly off by 1 bip (0.0001%) + assertApproxEqRel( + currency0.balanceOfSelf(), balance0BeforeBurn + uint256(uint128(deltaDecrease.amount0())), 0.0001e18 + ); + assertApproxEqRel( + currency1.balanceOfSelf(), balance1BeforeBurn + uint256(uint128(deltaDecrease.amount1())), 0.0001e18 + ); + + // OZ 721 will revert if the token does not exist + vm.expectRevert(); + lpm.ownerOf(1); + + // no tokens were lost, TODO: fuzzer showing off by 1 sometimes + assertApproxEqAbs(currency0.balanceOfSelf(), balance0Start, 1 wei); + assertApproxEqAbs(address(this).balance, balance0Start, 1 wei); + assertApproxEqAbs(currency1.balanceOfSelf(), balance1Start, 1 wei); + } + + function test_fuzz_burn_native_nonEmptyPosition(IPoolManager.ModifyLiquidityParams memory params) public { + uint256 balance0Start = address(this).balance; + uint256 balance1Start = currency1.balanceOfSelf(); + + params = createFuzzyLiquidityParams(nativeKey, params, SQRT_PRICE_1_1); + vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // two-sided liquidity + + uint256 liquidityToAdd = + params.liquidityDelta < 0 ? uint256(-params.liquidityDelta) : uint256(params.liquidityDelta); + PositionConfig memory config = + PositionConfig({poolKey: nativeKey, tickLower: params.tickLower, tickUpper: params.tickUpper}); + + uint256 tokenId = lpm.nextTokenId(); + mintWithNative(SQRT_PRICE_1_1, config, liquidityToAdd, address(this), ZERO_BYTES); + + bytes32 positionId = + Position.calculatePositionKey(address(lpm), config.tickLower, config.tickUpper, bytes32(tokenId)); + (uint256 liquidity,,) = manager.getPositionInfo(config.poolKey.toId(), positionId); + assertEq(liquidity, uint256(params.liquidityDelta)); + + // burn liquidity + uint256 balance0BeforeBurn = currency0.balanceOfSelf(); + uint256 balance1BeforeBurn = currency1.balanceOfSelf(); + + burn(tokenId, config, ZERO_BYTES); + BalanceDelta deltaBurn = getLastDelta(); + + (liquidity,,) = manager.getPositionInfo(config.poolKey.toId(), positionId); + assertEq(liquidity, 0); + + // TODO: slightly off by 1 bip (0.0001%) + assertApproxEqRel( + currency0.balanceOfSelf(), balance0BeforeBurn + uint256(uint128(deltaBurn.amount0())), 0.0001e18 + ); + assertApproxEqRel( + currency1.balanceOfSelf(), balance1BeforeBurn + uint256(uint128(deltaBurn.amount1())), 0.0001e18 + ); + + // OZ 721 will revert if the token does not exist + vm.expectRevert(); + lpm.ownerOf(1); + + // no tokens were lost, TODO: fuzzer showing off by 1 sometimes + assertApproxEqAbs(currency0.balanceOfSelf(), balance0Start, 1 wei); + assertApproxEqAbs(address(this).balance, balance0Start, 1 wei); + assertApproxEqAbs(currency1.balanceOfSelf(), balance1Start, 1 wei); + } + + function test_fuzz_increaseLiquidity_native(IPoolManager.ModifyLiquidityParams memory params) public { + // fuzz for the range + params = createFuzzyLiquidityParams(nativeKey, params, SQRT_PRICE_1_1); + vm.assume(params.tickLower < -60 && 60 < params.tickUpper); // two-sided liquidity + + // TODO: figure out if we can fuzz the increase liquidity delta. we're annoyingly getting TickLiquidityOverflow + uint256 liquidityToAdd = 1e18; + PositionConfig memory config = + PositionConfig({poolKey: nativeKey, tickLower: params.tickLower, tickUpper: params.tickUpper}); + + // mint the position with native token liquidity + uint256 tokenId = lpm.nextTokenId(); + mintWithNative(SQRT_PRICE_1_1, config, liquidityToAdd, address(this), ZERO_BYTES); + + uint256 balance0Before = address(this).balance; + uint256 balance1Before = currency1.balanceOfSelf(); + + // calculate how much native token is required for the liquidity increase (doubling the liquidity) + (uint256 amount0,) = LiquidityAmounts.getAmountsForLiquidity( + SQRT_PRICE_1_1, + TickMath.getSqrtPriceAtTick(params.tickLower), + TickMath.getSqrtPriceAtTick(params.tickUpper), + uint128(liquidityToAdd) + ); + + bytes memory calls = getIncreaseEncoded(tokenId, config, liquidityToAdd, ZERO_BYTES); // double the liquidity + lpm.modifyLiquidities{value: amount0 + 1 wei}(calls, _deadline); // TODO: off by one wei + BalanceDelta delta = getLastDelta(); + + // verify position liquidity increased + bytes32 positionId = + Position.calculatePositionKey(address(lpm), config.tickLower, config.tickUpper, bytes32(tokenId)); + (uint256 liquidity,,) = manager.getPositionInfo(config.poolKey.toId(), positionId); + assertEq(liquidity, liquidityToAdd + liquidityToAdd); // liquidity was doubled + + // verify native token balances changed as expected + assertEq(balance0Before - currency0.balanceOfSelf(), amount0 + 1 wei); + assertEq(balance0Before - currency0.balanceOfSelf(), uint256(int256(-delta.amount0()))); + assertEq(balance1Before - currency1.balanceOfSelf(), uint256(int256(-delta.amount1()))); + } + + // overpaying native tokens on increase liquidity is returned to caller + function test_fuzz_increaseLiquidity_native_excess(IPoolManager.ModifyLiquidityParams memory params) public { + // fuzz for the range + params = createFuzzyLiquidityParams(nativeKey, params, SQRT_PRICE_1_1); + vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // two-sided liquidity + + // TODO: figure out if we can fuzz the increase liquidity delta. we're annoyingly getting TickLiquidityOverflow + uint256 liquidityToAdd = 1e18; + PositionConfig memory config = + PositionConfig({poolKey: nativeKey, tickLower: params.tickLower, tickUpper: params.tickUpper}); + + // mint the position with native token liquidity + uint256 tokenId = lpm.nextTokenId(); + mintWithNative(SQRT_PRICE_1_1, config, liquidityToAdd, address(this), ZERO_BYTES); + + uint256 balance0Before = address(this).balance; + uint256 balance1Before = currency1.balanceOfSelf(); + + // calculate how much native token is required for the liquidity increase (doubling the liquidity) + (uint256 amount0,) = LiquidityAmounts.getAmountsForLiquidity( + SQRT_PRICE_1_1, + TickMath.getSqrtPriceAtTick(params.tickLower), + TickMath.getSqrtPriceAtTick(params.tickUpper), + uint128(liquidityToAdd) + ); + + Plan memory planner = Planner.init(); + planner.add( + Actions.INCREASE_LIQUIDITY, + abi.encode(tokenId, config, liquidityToAdd, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, ZERO_BYTES) + ); + planner.add(Actions.CLOSE_CURRENCY, abi.encode(nativeKey.currency0)); + planner.add(Actions.CLOSE_CURRENCY, abi.encode(nativeKey.currency1)); + // sweep the excess eth + planner.add(Actions.SWEEP, abi.encode(currency0, address(this))); + bytes memory calls = planner.encode(); + + lpm.modifyLiquidities{value: amount0 * 2}(calls, _deadline); // overpay on increase liquidity + BalanceDelta delta = getLastDelta(); + + // verify position liquidity increased + bytes32 positionId = + Position.calculatePositionKey(address(lpm), config.tickLower, config.tickUpper, bytes32(tokenId)); + (uint256 liquidity,,) = manager.getPositionInfo(config.poolKey.toId(), positionId); + assertEq(liquidity, liquidityToAdd + liquidityToAdd); // liquidity was doubled + + // verify native token balances changed as expected, with overpaid tokens returned + assertEq(balance0Before - currency0.balanceOfSelf(), amount0 + 1 wei); + assertEq(balance0Before - currency0.balanceOfSelf(), uint256(int256(-delta.amount0()))); + assertEq(balance1Before - currency1.balanceOfSelf(), uint256(int256(-delta.amount1()))); + } + + function test_fuzz_decreaseLiquidity_native( + IPoolManager.ModifyLiquidityParams memory params, + uint256 decreaseLiquidityDelta + ) public { + params = createFuzzyLiquidityParams(nativeKey, params, SQRT_PRICE_1_1); + vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // two-sided liquidity + decreaseLiquidityDelta = bound(decreaseLiquidityDelta, 1, uint256(params.liquidityDelta)); + + PositionConfig memory config = + PositionConfig({poolKey: nativeKey, tickLower: params.tickLower, tickUpper: params.tickUpper}); + + // mint the position with native token liquidity + uint256 tokenId = lpm.nextTokenId(); + mintWithNative(SQRT_PRICE_1_1, config, uint256(params.liquidityDelta), address(this), ZERO_BYTES); + + uint256 balance0Before = address(this).balance; + uint256 balance1Before = currency1.balanceOfSelf(); + + // decrease liquidity and receive native tokens + (uint256 amount0,) = LiquidityAmounts.getAmountsForLiquidity( + SQRT_PRICE_1_1, + TickMath.getSqrtPriceAtTick(params.tickLower), + TickMath.getSqrtPriceAtTick(params.tickUpper), + uint128(decreaseLiquidityDelta) + ); + decreaseLiquidity(tokenId, config, decreaseLiquidityDelta, ZERO_BYTES); + BalanceDelta delta = getLastDelta(); + + bytes32 positionId = + Position.calculatePositionKey(address(lpm), config.tickLower, config.tickUpper, bytes32(tokenId)); + (uint256 liquidity,,) = manager.getPositionInfo(config.poolKey.toId(), positionId); + assertEq(liquidity, uint256(params.liquidityDelta) - decreaseLiquidityDelta); + + // verify native token balances changed as expected + assertApproxEqAbs(currency0.balanceOfSelf() - balance0Before, amount0, 1 wei); + assertEq(currency0.balanceOfSelf() - balance0Before, uint128(delta.amount0())); + assertEq(currency1.balanceOfSelf() - balance1Before, uint128(delta.amount1())); + } + + function test_fuzz_collect_native(IPoolManager.ModifyLiquidityParams memory params) public { + params = createFuzzyLiquidityParams(nativeKey, params, SQRT_PRICE_1_1); + vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // two-sided liquidity + + PositionConfig memory config = + PositionConfig({poolKey: nativeKey, tickLower: params.tickLower, tickUpper: params.tickUpper}); + + // mint the position with native token liquidity + uint256 tokenId = lpm.nextTokenId(); + mintWithNative(SQRT_PRICE_1_1, config, uint256(params.liquidityDelta), address(this), ZERO_BYTES); + + // donate to generate fee revenue + uint256 feeRevenue0 = 1e18; + uint256 feeRevenue1 = 0.1e18; + donateRouter.donate{value: 1e18}(nativeKey, feeRevenue0, feeRevenue1, ZERO_BYTES); + + uint256 balance0Before = address(this).balance; + uint256 balance1Before = currency1.balanceOfSelf(); + collect(tokenId, config, ZERO_BYTES); + BalanceDelta delta = getLastDelta(); + + assertApproxEqAbs(currency0.balanceOfSelf() - balance0Before, feeRevenue0, 1 wei); // TODO: fuzzer off by 1 wei + assertEq(currency0.balanceOfSelf() - balance0Before, uint128(delta.amount0())); + assertEq(currency1.balanceOfSelf() - balance1Before, uint128(delta.amount1())); + } +} diff --git a/test/position-managers/PositionManager.gas.t.sol b/test/position-managers/PositionManager.gas.t.sol new file mode 100644 index 00000000..8f9aade0 --- /dev/null +++ b/test/position-managers/PositionManager.gas.t.sol @@ -0,0 +1,602 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; +import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.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"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {LiquidityAmounts} from "@uniswap/v4-core/test/utils/LiquidityAmounts.sol"; +import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; +import {FixedPointMathLib} from "solmate/src/utils/FixedPointMathLib.sol"; +import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; + +import {IERC20} from "forge-std/interfaces/IERC20.sol"; + +import {IPositionManager} from "../../src/interfaces/IPositionManager.sol"; +import {Actions} from "../../src/libraries/Actions.sol"; +import {PositionManager} from "../../src/PositionManager.sol"; +import {PositionConfig} from "../../src/libraries/PositionConfig.sol"; +import {IMulticall} from "../../src/interfaces/IMulticall.sol"; +import {Planner, Plan} from "../shared/Planner.sol"; +import {PosmTestSetup} from "../shared/PosmTestSetup.sol"; + +contract PosMGasTest is Test, PosmTestSetup, GasSnapshot { + using FixedPointMathLib for uint256; + using CurrencyLibrary for Currency; + using PoolIdLibrary for PoolKey; + using Planner for Plan; + + PoolId poolId; + address alice; + uint256 alicePK; + address bob; + uint256 bobPK; + + // expresses the fee as a wad (i.e. 3000 = 0.003e18 = 0.30%) + uint256 FEE_WAD; + + PositionConfig config; + PositionConfig configNative; + + function setUp() public { + (alice, alicePK) = makeAddrAndKey("ALICE"); + (bob, bobPK) = makeAddrAndKey("BOB"); + + deployFreshManagerAndRouters(); + deployMintAndApprove2Currencies(); + + (key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_PRICE_1_1, ZERO_BYTES); + (nativeKey,) = initPool(CurrencyLibrary.NATIVE, currency1, IHooks(hook), 3000, SQRT_PRICE_1_1, ZERO_BYTES); + FEE_WAD = uint256(key.fee).mulDivDown(FixedPointMathLib.WAD, 1_000_000); + + // Requires currency0 and currency1 to be set in base Deployers contract. + deployAndApprovePosm(manager); + + // Give tokens to Alice and Bob. + seedBalance(alice); + seedBalance(bob); + + // Approve posm for Alice and bob. + approvePosmFor(alice); + approvePosmFor(bob); + + // define a reusable range + config = PositionConfig({poolKey: key, tickLower: -300, tickUpper: 300}); + configNative = PositionConfig({poolKey: nativeKey, tickLower: -300, tickUpper: 300}); + } + + function test_gas_mint() public { + Plan memory planner = Planner.init().add( + Actions.MINT_POSITION, + abi.encode(config, 10_000 ether, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, address(this), ZERO_BYTES) + ); + bytes memory calls = planner.finalizeModifyLiquidity(config.poolKey); + lpm.modifyLiquidities(calls, _deadline); + snapLastCall("PositionManager_mint"); + } + + function test_gas_mint_differentRanges() public { + // Explicitly mint to a new range on the same pool. + PositionConfig memory bob_mint = PositionConfig({poolKey: key, tickLower: 0, tickUpper: 60}); + vm.startPrank(bob); + mint(bob_mint, 10_000 ether, address(bob), ZERO_BYTES); + vm.stopPrank(); + // Mint to a diff config, diff user. + Plan memory planner = Planner.init().add( + Actions.MINT_POSITION, + abi.encode(config, 10_000 ether, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, address(alice), ZERO_BYTES) + ); + bytes memory calls = planner.finalizeModifyLiquidity(config.poolKey); + vm.prank(alice); + lpm.modifyLiquidities(calls, _deadline); + snapLastCall("PositionManager_mint_warmedPool_differentRange"); + } + + function test_gas_mint_sameTickLower() public { + // Explicitly mint to range whos tickLower is the same. + PositionConfig memory bob_mint = PositionConfig({poolKey: key, tickLower: -300, tickUpper: -60}); + vm.startPrank(bob); + mint(bob_mint, 10_000 ether, address(bob), ZERO_BYTES); + vm.stopPrank(); + // Mint to a diff config, diff user. + Plan memory planner = Planner.init().add( + Actions.MINT_POSITION, + abi.encode(config, 10_000 ether, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, address(alice), ZERO_BYTES) + ); + bytes memory calls = planner.finalizeModifyLiquidity(config.poolKey); + vm.prank(alice); + lpm.modifyLiquidities(calls, _deadline); + snapLastCall("PositionManager_mint_onSameTickLower"); + } + + function test_gas_mint_sameTickUpper() public { + // Explicitly mint to range whos tickUpperis the same. + PositionConfig memory bob_mint = PositionConfig({poolKey: key, tickLower: 60, tickUpper: 300}); + vm.startPrank(bob); + mint(bob_mint, 10_000 ether, address(bob), ZERO_BYTES); + vm.stopPrank(); + // Mint to a diff config, diff user. + Plan memory planner = Planner.init().add( + Actions.MINT_POSITION, + abi.encode(config, 10_000 ether, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, address(alice), ZERO_BYTES) + ); + bytes memory calls = planner.finalizeModifyLiquidity(config.poolKey); + vm.prank(alice); + lpm.modifyLiquidities(calls, _deadline); + snapLastCall("PositionManager_mint_onSameTickUpper"); + } + + function test_gas_increaseLiquidity_erc20() public { + uint256 tokenId = lpm.nextTokenId(); + mint(config, 10_000 ether, address(this), ZERO_BYTES); + + Plan memory planner = Planner.init().add( + Actions.INCREASE_LIQUIDITY, + abi.encode(tokenId, config, 10_000 ether, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, ZERO_BYTES) + ); + + bytes memory calls = planner.finalizeModifyLiquidity(config.poolKey); + lpm.modifyLiquidities(calls, _deadline); + snapLastCall("PositionManager_increaseLiquidity_erc20"); + } + + function test_gas_autocompound_exactUnclaimedFees() public { + // Alice and Bob provide liquidity on the range + // Alice uses her exact fees to increase liquidity (compounding) + + uint256 liquidityAlice = 3_000e18; + uint256 liquidityBob = 1_000e18; + + // alice provides liquidity + vm.startPrank(alice); + uint256 tokenIdAlice = lpm.nextTokenId(); + mint(config, liquidityAlice, alice, ZERO_BYTES); + vm.stopPrank(); + + // bob provides liquidity + vm.startPrank(bob); + mint(config, liquidityBob, bob, ZERO_BYTES); + vm.stopPrank(); + + // donate to create fees + uint256 amountDonate = 0.2e18; + donateRouter.donate(key, amountDonate, amountDonate, ZERO_BYTES); + + // alice uses her exact fees to increase liquidity + uint256 tokensOwedAlice = amountDonate.mulDivDown(liquidityAlice, liquidityAlice + liquidityBob) - 1; + + (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, config.poolKey.toId()); + uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( + sqrtPriceX96, + TickMath.getSqrtPriceAtTick(config.tickLower), + TickMath.getSqrtPriceAtTick(config.tickUpper), + tokensOwedAlice, + tokensOwedAlice + ); + + Plan memory planner = Planner.init().add( + Actions.INCREASE_LIQUIDITY, + abi.encode(tokenIdAlice, config, liquidityDelta, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, ZERO_BYTES) + ); + // because its a perfect autocompound, the delta is exactly 0 and we dont need to "close" deltas + bytes memory calls = planner.encode(); + + vm.prank(alice); + lpm.modifyLiquidities(calls, _deadline); + snapLastCall("PositionManager_increase_autocompoundExactUnclaimedFees"); + } + + function test_gas_autocompound_clearExcess() public { + // Alice and Bob provide liquidity on the range + // Alice uses her exact fees to increase liquidity (compounding) + + uint256 liquidityAlice = 3_000e18; + uint256 liquidityBob = 1_000e18; + + // alice provides liquidity + vm.startPrank(alice); + uint256 tokenIdAlice = lpm.nextTokenId(); + mint(config, liquidityAlice, alice, ZERO_BYTES); + vm.stopPrank(); + + // bob provides liquidity + vm.startPrank(bob); + mint(config, liquidityBob, bob, ZERO_BYTES); + vm.stopPrank(); + + // donate to create fees + uint256 amountDonate = 0.2e18; + donateRouter.donate(key, amountDonate, amountDonate, ZERO_BYTES); + + // alice will use half of her fees to increase liquidity + uint256 halfTokensOwedAlice = (amountDonate.mulDivDown(liquidityAlice, liquidityAlice + liquidityBob) - 1) / 2; + + (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, config.poolKey.toId()); + uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( + sqrtPriceX96, + TickMath.getSqrtPriceAtTick(config.tickLower), + TickMath.getSqrtPriceAtTick(config.tickUpper), + halfTokensOwedAlice, + halfTokensOwedAlice + ); + + // Alice elects to forfeit unclaimed tokens + Plan memory planner = Planner.init(); + planner.add( + Actions.INCREASE_LIQUIDITY, + abi.encode(tokenIdAlice, config, liquidityDelta, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, ZERO_BYTES) + ); + planner.add(Actions.CLEAR, abi.encode(config.poolKey.currency0, halfTokensOwedAlice + 1 wei)); + planner.add(Actions.CLEAR, abi.encode(config.poolKey.currency1, halfTokensOwedAlice + 1 wei)); + bytes memory calls = planner.encode(); + + vm.prank(alice); + lpm.modifyLiquidities(calls, _deadline); + snapLastCall("PositionManager_increase_autocompound_clearExcess"); + } + + // Autocompounding but the excess fees are taken to the user + function test_gas_autocompound_excessFeesCredit() public { + // Alice and Bob provide liquidity on the range + // Alice uses her fees to increase liquidity. Excess fees are accounted to alice + uint256 liquidityAlice = 3_000e18; + uint256 liquidityBob = 1_000e18; + + // alice provides liquidity + vm.startPrank(alice); + uint256 tokenIdAlice = lpm.nextTokenId(); + mint(config, liquidityAlice, alice, ZERO_BYTES); + vm.stopPrank(); + + // bob provides liquidity + vm.startPrank(bob); + mint(config, liquidityBob, bob, ZERO_BYTES); + vm.stopPrank(); + + // donate to create fees + uint256 amountDonate = 20e18; + donateRouter.donate(key, amountDonate, amountDonate, ZERO_BYTES); + + // alice will use half of her fees to increase liquidity + uint256 halfTokensOwedAlice = (amountDonate.mulDivDown(liquidityAlice, liquidityAlice + liquidityBob) - 1) / 2; + + (uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, config.poolKey.toId()); + uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts( + sqrtPriceX96, + TickMath.getSqrtPriceAtTick(config.tickLower), + TickMath.getSqrtPriceAtTick(config.tickUpper), + halfTokensOwedAlice, + halfTokensOwedAlice + ); + + Plan memory planner = Planner.init().add( + Actions.INCREASE_LIQUIDITY, + abi.encode(tokenIdAlice, config, liquidityDelta, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, ZERO_BYTES) + ); + + bytes memory calls = planner.finalizeModifyLiquidity(config.poolKey); + + vm.prank(alice); + lpm.modifyLiquidities(calls, _deadline); + snapLastCall("PositionManager_increase_autocompoundExcessFeesCredit"); + } + + function test_gas_decreaseLiquidity() public { + uint256 tokenId = lpm.nextTokenId(); + mint(config, 10_000 ether, address(this), ZERO_BYTES); + + Plan memory planner = Planner.init().add( + Actions.DECREASE_LIQUIDITY, + abi.encode(tokenId, config, 10_000 ether, MIN_SLIPPAGE_DECREASE, MIN_SLIPPAGE_DECREASE, ZERO_BYTES) + ); + + bytes memory calls = planner.finalizeModifyLiquidity(config.poolKey); + lpm.modifyLiquidities(calls, _deadline); + snapLastCall("PositionManager_decreaseLiquidity"); + } + + function test_gas_multicall_initialize_mint() public { + key = PoolKey({currency0: currency0, currency1: currency1, fee: 0, tickSpacing: 10, hooks: IHooks(address(0))}); + + // 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); + + 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, 100e18, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, address(this), ZERO_BYTES) + ); + bytes memory actions = planner.finalizeModifyLiquidity(config.poolKey); + + calls[1] = abi.encodeWithSelector(IPositionManager.modifyLiquidities.selector, actions, _deadline); + + IMulticall(lpm).multicall(calls); + snapLastCall("PositionManager_multicall_initialize_mint"); + } + + function test_gas_collect() public { + uint256 tokenId = lpm.nextTokenId(); + mint(config, 10_000 ether, address(this), ZERO_BYTES); + + // donate to create fee revenue + donateRouter.donate(config.poolKey, 0.2e18, 0.2e18, ZERO_BYTES); + + // Collect by calling decrease with 0. + Plan memory planner = Planner.init().add( + Actions.DECREASE_LIQUIDITY, + abi.encode(tokenId, config, 0, MIN_SLIPPAGE_DECREASE, MIN_SLIPPAGE_DECREASE, ZERO_BYTES) + ); + + bytes memory calls = planner.finalizeModifyLiquidity(config.poolKey); + lpm.modifyLiquidities(calls, _deadline); + snapLastCall("PositionManager_collect"); + } + + // same-range gas tests + function test_gas_sameRange_mint() public { + mint(config, 10_000 ether, address(this), ZERO_BYTES); + + Plan memory planner = Planner.init().add( + Actions.MINT_POSITION, + abi.encode(config, 10_001 ether, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, address(alice), ZERO_BYTES) + ); + bytes memory calls = planner.finalizeModifyLiquidity(config.poolKey); + vm.prank(alice); + lpm.modifyLiquidities(calls, _deadline); + snapLastCall("PositionManager_mint_sameRange"); + } + + function test_gas_sameRange_decrease() public { + // two positions of the same config, one of them decreases the entirety of the liquidity + vm.startPrank(alice); + mint(config, 10_000 ether, address(this), ZERO_BYTES); + vm.stopPrank(); + + uint256 tokenId = lpm.nextTokenId(); + mint(config, 10_000 ether, address(this), ZERO_BYTES); + + Plan memory planner = Planner.init().add( + Actions.DECREASE_LIQUIDITY, + abi.encode(tokenId, config, 10_000 ether, MIN_SLIPPAGE_DECREASE, MIN_SLIPPAGE_DECREASE, ZERO_BYTES) + ); + + bytes memory calls = planner.finalizeModifyLiquidity(config.poolKey); + lpm.modifyLiquidities(calls, _deadline); + snapLastCall("PositionManager_decrease_sameRange_allLiquidity"); + } + + function test_gas_sameRange_collect() public { + // two positions of the same config, one of them collects all their fees + vm.startPrank(alice); + mint(config, 10_000 ether, address(this), ZERO_BYTES); + vm.stopPrank(); + + uint256 tokenId = lpm.nextTokenId(); + mint(config, 10_000 ether, address(this), ZERO_BYTES); + + // donate to create fee revenue + donateRouter.donate(config.poolKey, 0.2e18, 0.2e18, ZERO_BYTES); + + Plan memory planner = Planner.init().add( + Actions.DECREASE_LIQUIDITY, + abi.encode(tokenId, config, 0, MIN_SLIPPAGE_DECREASE, MIN_SLIPPAGE_DECREASE, ZERO_BYTES) + ); + + bytes memory calls = planner.finalizeModifyLiquidity(config.poolKey); + lpm.modifyLiquidities(calls, _deadline); + snapLastCall("PositionManager_collect_sameRange"); + } + + function test_gas_burn_nonEmptyPosition() public { + uint256 tokenId = lpm.nextTokenId(); + mint(config, 10_000 ether, address(this), ZERO_BYTES); + + Plan memory planner = Planner.init().add( + Actions.BURN_POSITION, abi.encode(tokenId, config, MIN_SLIPPAGE_DECREASE, MIN_SLIPPAGE_DECREASE, ZERO_BYTES) + ); + bytes memory calls = planner.finalizeModifyLiquidity(config.poolKey); + + lpm.modifyLiquidities(calls, _deadline); + snapLastCall("PositionManager_burn_nonEmpty"); + } + + function test_gas_burnEmpty() public { + uint256 tokenId = lpm.nextTokenId(); + mint(config, 10_000 ether, address(this), ZERO_BYTES); + + decreaseLiquidity(tokenId, config, 10_000 ether, ZERO_BYTES); + Plan memory planner = Planner.init().add( + Actions.BURN_POSITION, abi.encode(tokenId, config, MIN_SLIPPAGE_DECREASE, MIN_SLIPPAGE_DECREASE, ZERO_BYTES) + ); + + // There is no need to include CLOSE commands. + bytes memory calls = planner.encode(); + lpm.modifyLiquidities(calls, _deadline); + snapLastCall("PositionManager_burn_empty"); + } + + function test_gas_decrease_burnEmpty_batch() public { + // Will be more expensive than not encoding a decrease and just encoding a burn. + // ie. check this against PositionManager_burn_nonEmpty + uint256 tokenId = lpm.nextTokenId(); + mint(config, 10_000 ether, address(this), ZERO_BYTES); + + Plan memory planner = Planner.init().add( + Actions.DECREASE_LIQUIDITY, + abi.encode(tokenId, config, 10_000 ether, MIN_SLIPPAGE_DECREASE, MIN_SLIPPAGE_DECREASE, ZERO_BYTES) + ); + planner.add( + Actions.BURN_POSITION, abi.encode(tokenId, config, MIN_SLIPPAGE_DECREASE, MIN_SLIPPAGE_DECREASE, ZERO_BYTES) + ); + + // We must include CLOSE commands. + bytes memory calls = planner.finalizeModifyLiquidity(config.poolKey); + lpm.modifyLiquidities(calls, _deadline); + snapLastCall("PositionManager_decrease_burnEmpty"); + } + + // TODO: ERC6909 Support. + function test_gas_increaseLiquidity_erc6909() public {} + function test_gas_decreaseLiquidity_erc6909() public {} + + // Native Token Gas Tests + function test_gas_mint_native() public { + uint256 liquidityToAdd = 10_000 ether; + bytes memory calls = getMintEncoded(configNative, liquidityToAdd, address(this), ZERO_BYTES); + + (uint256 amount0,) = LiquidityAmounts.getAmountsForLiquidity( + SQRT_PRICE_1_1, + TickMath.getSqrtPriceAtTick(configNative.tickLower), + TickMath.getSqrtPriceAtTick(configNative.tickUpper), + uint128(liquidityToAdd) + ); + lpm.modifyLiquidities{value: amount0 + 1}(calls, _deadline); + snapLastCall("PositionManager_mint_native"); + } + + function test_gas_mint_native_excess() public { + uint256 liquidityToAdd = 10_000 ether; + + Plan memory planner = Planner.init(); + planner.add( + Actions.MINT_POSITION, + abi.encode( + configNative, liquidityToAdd, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, address(this), ZERO_BYTES + ) + ); + planner.add(Actions.CLOSE_CURRENCY, abi.encode(nativeKey.currency0)); + planner.add(Actions.CLOSE_CURRENCY, abi.encode(nativeKey.currency1)); + planner.add(Actions.SWEEP, abi.encode(CurrencyLibrary.NATIVE, address(this))); + bytes memory calls = planner.encode(); + + (uint256 amount0,) = LiquidityAmounts.getAmountsForLiquidity( + SQRT_PRICE_1_1, + TickMath.getSqrtPriceAtTick(configNative.tickLower), + TickMath.getSqrtPriceAtTick(configNative.tickUpper), + uint128(liquidityToAdd) + ); + // overpay on the native token + lpm.modifyLiquidities{value: amount0 * 2}(calls, _deadline); + snapLastCall("PositionManager_mint_nativeWithSweep"); + } + + function test_gas_increase_native() public { + uint256 tokenId = lpm.nextTokenId(); + mintWithNative(SQRT_PRICE_1_1, configNative, 10_000 ether, address(this), ZERO_BYTES); + + uint256 liquidityToAdd = 10_000 ether; + bytes memory calls = getIncreaseEncoded(tokenId, configNative, liquidityToAdd, ZERO_BYTES); + (uint256 amount0,) = LiquidityAmounts.getAmountsForLiquidity( + SQRT_PRICE_1_1, + TickMath.getSqrtPriceAtTick(configNative.tickLower), + TickMath.getSqrtPriceAtTick(configNative.tickUpper), + uint128(liquidityToAdd) + ); + lpm.modifyLiquidities{value: amount0 + 1}(calls, _deadline); + snapLastCall("PositionManager_increaseLiquidity_native"); + } + + function test_gas_decrease_native() public { + uint256 tokenId = lpm.nextTokenId(); + mintWithNative(SQRT_PRICE_1_1, configNative, 10_000 ether, address(this), ZERO_BYTES); + + uint256 liquidityToRemove = 10_000 ether; + bytes memory calls = getDecreaseEncoded(tokenId, configNative, liquidityToRemove, ZERO_BYTES); + lpm.modifyLiquidities(calls, _deadline); + snapLastCall("PositionManager_decreaseLiquidity_native"); + } + + function test_gas_collect_native() public { + uint256 tokenId = lpm.nextTokenId(); + mintWithNative(SQRT_PRICE_1_1, configNative, 10_000 ether, address(this), ZERO_BYTES); + + // donate to create fee revenue + donateRouter.donate{value: 0.2e18}(configNative.poolKey, 0.2e18, 0.2e18, ZERO_BYTES); + + bytes memory calls = getCollectEncoded(tokenId, configNative, ZERO_BYTES); + lpm.modifyLiquidities(calls, _deadline); + snapLastCall("PositionManager_collect_native"); + } + + function test_gas_burn_nonEmptyPosition_native() public { + uint256 tokenId = lpm.nextTokenId(); + mintWithNative(SQRT_PRICE_1_1, configNative, 10_000 ether, address(this), ZERO_BYTES); + + Plan memory planner = Planner.init().add( + Actions.BURN_POSITION, + abi.encode(tokenId, configNative, MIN_SLIPPAGE_DECREASE, MIN_SLIPPAGE_DECREASE, ZERO_BYTES) + ); + bytes memory calls = planner.finalizeModifyLiquidity(configNative.poolKey); + + lpm.modifyLiquidities(calls, _deadline); + snapLastCall("PositionManager_burn_nonEmpty_native"); + } + + function test_gas_burnEmpty_native() public { + uint256 tokenId = lpm.nextTokenId(); + mintWithNative(SQRT_PRICE_1_1, configNative, 10_000 ether, address(this), ZERO_BYTES); + + decreaseLiquidity(tokenId, configNative, 10_000 ether, ZERO_BYTES); + Plan memory planner = Planner.init().add( + Actions.BURN_POSITION, + abi.encode(tokenId, configNative, MIN_SLIPPAGE_DECREASE, MIN_SLIPPAGE_DECREASE, ZERO_BYTES) + ); + + // There is no need to include CLOSE commands. + bytes memory calls = planner.encode(); + lpm.modifyLiquidities(calls, _deadline); + snapLastCall("PositionManager_burn_empty_native"); + } + + function test_gas_decrease_burnEmpty_batch_native() public { + // Will be more expensive than not encoding a decrease and just encoding a burn. + // ie. check this against PositionManager_burn_nonEmpty + uint256 tokenId = lpm.nextTokenId(); + mintWithNative(SQRT_PRICE_1_1, configNative, 10_000 ether, address(this), ZERO_BYTES); + + Plan memory planner = Planner.init().add( + Actions.DECREASE_LIQUIDITY, + abi.encode(tokenId, configNative, 10_000 ether, MIN_SLIPPAGE_DECREASE, MIN_SLIPPAGE_DECREASE, ZERO_BYTES) + ); + planner.add(Actions.BURN_POSITION, abi.encode(tokenId, configNative, 0 wei, 0 wei, ZERO_BYTES)); + + // We must include CLOSE commands. + bytes memory calls = planner.finalizeModifyLiquidity(configNative.poolKey); + lpm.modifyLiquidities(calls, _deadline); + snapLastCall("PositionManager_decrease_burnEmpty_native"); + } + + function test_gas_mint_settleWithBalance_sweep() public { + uint256 liquidityAlice = 3_000e18; + + Plan memory planner = Planner.init(); + planner.add( + Actions.MINT_POSITION, + abi.encode(config, liquidityAlice, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, alice, ZERO_BYTES) + ); + planner.add(Actions.SETTLE_WITH_BALANCE, abi.encode(currency0)); + planner.add(Actions.SETTLE_WITH_BALANCE, abi.encode(currency1)); + planner.add(Actions.SWEEP, abi.encode(currency0, address(this))); + planner.add(Actions.SWEEP, abi.encode(currency1, address(this))); + + currency0.transfer(address(lpm), 100e18); + currency1.transfer(address(lpm), 100e18); + + bytes memory calls = planner.encode(); + + vm.prank(alice); + lpm.modifyLiquidities(calls, _deadline); + snapLastCall("PositionManager_mint_settleWithBalance_sweep"); + } +} diff --git a/test/position-managers/PositionManager.multicall.t.sol b/test/position-managers/PositionManager.multicall.t.sol new file mode 100644 index 00000000..da8774fd --- /dev/null +++ b/test/position-managers/PositionManager.multicall.t.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "forge-std/Test.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"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import {PoolId} from "@uniswap/v4-core/src/types/PoolId.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; +import {FixedPointMathLib} from "solmate/src/utils/FixedPointMathLib.sol"; + +import {IERC20} from "forge-std/interfaces/IERC20.sol"; + +import {IPositionManager} from "../../src/interfaces/IPositionManager.sol"; +import {Actions} from "../../src/libraries/Actions.sol"; +import {PositionManager} from "../../src/PositionManager.sol"; +import {PositionConfig} from "../../src/libraries/PositionConfig.sol"; +import {IMulticall} from "../../src/interfaces/IMulticall.sol"; +import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; +import {Planner, Plan} from "../shared/Planner.sol"; +import {PosmTestSetup} from "../shared/PosmTestSetup.sol"; + +contract PositionManagerMulticallTest is Test, PosmTestSetup, LiquidityFuzzers { + using FixedPointMathLib for uint256; + using CurrencyLibrary for Currency; + using Planner for Plan; + + PoolId poolId; + address alice = makeAddr("ALICE"); + + function setUp() public { + deployFreshManagerAndRouters(); + deployMintAndApprove2Currencies(); + + (key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_PRICE_1_1, ZERO_BYTES); + + // Requires currency0 and currency1 to be set in base Deployers contract. + deployAndApprovePosm(manager); + } + + function test_multicall_initializePool_mint() public { + key = PoolKey({currency0: currency0, currency1: currency1, fee: 0, tickSpacing: 10, hooks: IHooks(address(0))}); + + // 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); + + PositionConfig memory 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, 100e18, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, address(this), ZERO_BYTES) + ); + bytes memory actions = planner.finalizeModifyLiquidity(config.poolKey); + + calls[1] = abi.encodeWithSelector(IPositionManager.modifyLiquidities.selector, actions, _deadline); + + IMulticall(address(lpm)).multicall(calls); + + // test swap, doesn't revert, showing the pool was initialized + int256 amountSpecified = -1e18; + BalanceDelta result = swap(key, true, amountSpecified, ZERO_BYTES); + assertEq(result.amount0(), amountSpecified); + assertGt(result.amount1(), 0); + } +} diff --git a/test/position-managers/PositionManager.t.sol b/test/position-managers/PositionManager.t.sol new file mode 100644 index 00000000..d07468e9 --- /dev/null +++ b/test/position-managers/PositionManager.t.sol @@ -0,0 +1,827 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "forge-std/Test.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"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {LiquidityAmounts} from "@uniswap/v4-core/test/utils/LiquidityAmounts.sol"; +import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; +import {FixedPointMathLib} from "solmate/src/utils/FixedPointMathLib.sol"; +import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; +import {LPFeeLibrary} from "@uniswap/v4-core/src/libraries/LPFeeLibrary.sol"; +import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol"; +import {Position} from "@uniswap/v4-core/src/libraries/Position.sol"; +import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; + +import {IERC20} from "forge-std/interfaces/IERC20.sol"; + +import {IPositionManager} from "../../src/interfaces/IPositionManager.sol"; +import {Actions} from "../../src/libraries/Actions.sol"; +import {PositionManager} from "../../src/PositionManager.sol"; +import {PositionConfig} from "../../src/libraries/PositionConfig.sol"; +import {SlippageCheckLibrary} from "../../src/libraries/SlippageCheck.sol"; +import {BaseActionsRouter} from "../../src/base/BaseActionsRouter.sol"; + +import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; +import {Planner, Plan} from "../shared/Planner.sol"; +import {PosmTestSetup} from "../shared/PosmTestSetup.sol"; +import {ReentrantToken} from "../mocks/ReentrantToken.sol"; +import {ReentrancyLock} from "../../src/base/ReentrancyLock.sol"; + +contract PositionManagerTest is Test, PosmTestSetup, LiquidityFuzzers { + using FixedPointMathLib for uint256; + using CurrencyLibrary for Currency; + using Planner for Plan; + using PoolIdLibrary for PoolKey; + using StateLibrary for IPoolManager; + + PoolId poolId; + address alice = makeAddr("ALICE"); + + function setUp() public { + deployFreshManagerAndRouters(); + deployMintAndApprove2Currencies(); + + // 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); + + // Requires currency0 and currency1 to be set in base Deployers contract. + deployAndApprovePosm(manager); + + seedBalance(alice); + approvePosmFor(alice); + } + + function test_modifyLiquidities_reverts_mismatchedLengths() public { + Plan memory planner = Planner.init(); + planner.add(Actions.MINT_POSITION, abi.encode("test")); + planner.add(Actions.BURN_POSITION, abi.encode("test")); + + bytes[] memory badParams = new bytes[](1); + + vm.expectRevert(BaseActionsRouter.InputLengthMismatch.selector); + lpm.modifyLiquidities(abi.encode(planner.actions, badParams), block.timestamp + 1); + } + + function test_modifyLiquidities_reverts_reentrancy() public { + // Create a reentrant token and initialize the pool + Currency reentrantToken = Currency.wrap(address(new ReentrantToken(lpm))); + (currency0, currency1) = (Currency.unwrap(reentrantToken) < Currency.unwrap(currency1)) + ? (reentrantToken, currency1) + : (currency1, reentrantToken); + + // Set up approvals for the reentrant token + approvePosmCurrency(reentrantToken); + + (key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_PRICE_1_1, ZERO_BYTES); + + // Try to add liquidity at that range, but the token reenters posm + PositionConfig memory config = PositionConfig({poolKey: key, tickLower: 0, tickUpper: 60}); + bytes memory calls = getMintEncoded(config, 1e18, address(this), ""); + + // Permit2.transferFrom does not bubble the ContractLocked error and instead reverts with its own error + vm.expectRevert("TRANSFER_FROM_FAILED"); + lpm.modifyLiquidities(calls, block.timestamp + 1); + } + + function test_fuzz_mint_withLiquidityDelta(IPoolManager.ModifyLiquidityParams memory params, uint160 sqrtPriceX96) + public + { + bound(sqrtPriceX96, MIN_PRICE_LIMIT, MAX_PRICE_LIMIT); + params = createFuzzyLiquidityParams(key, params, sqrtPriceX96); + // liquidity is a uint + uint256 liquidityToAdd = + params.liquidityDelta < 0 ? uint256(-params.liquidityDelta) : uint256(params.liquidityDelta); + PositionConfig memory config = + PositionConfig({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); + + uint256 balance0Before = currency0.balanceOfSelf(); + uint256 balance1Before = currency1.balanceOfSelf(); + + uint256 tokenId = lpm.nextTokenId(); + mint(config, liquidityToAdd, address(this), ZERO_BYTES); + BalanceDelta delta = getLastDelta(); + + assertEq(tokenId, 1); + assertEq(lpm.nextTokenId(), 2); + assertEq(lpm.ownerOf(tokenId), address(this)); + + bytes32 positionId = + Position.calculatePositionKey(address(lpm), config.tickLower, config.tickUpper, bytes32(tokenId)); + (uint256 liquidity,,) = manager.getPositionInfo(config.poolKey.toId(), positionId); + + assertEq(liquidity, uint256(params.liquidityDelta)); + assertEq(balance0Before - currency0.balanceOfSelf(), uint256(int256(-delta.amount0())), "incorrect amount0"); + assertEq(balance1Before - currency1.balanceOfSelf(), uint256(int256(-delta.amount1())), "incorrect amount1"); + } + + function test_mint_exactTokenRatios() public { + int24 tickLower = -int24(key.tickSpacing); + int24 tickUpper = int24(key.tickSpacing); + 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 balance0Before = currency0.balanceOfSelf(); + uint256 balance1Before = currency1.balanceOfSelf(); + + uint256 tokenId = lpm.nextTokenId(); + mint(config, liquidityToAdd, address(this), ZERO_BYTES); + BalanceDelta delta = getLastDelta(); + + uint256 balance0After = currency0.balanceOfSelf(); + uint256 balance1After = currency1.balanceOfSelf(); + + assertEq(tokenId, 1); + assertEq(lpm.ownerOf(1), address(this)); + + assertEq(uint256(int256(-delta.amount0())), amount0Desired); + assertEq(uint256(int256(-delta.amount1())), amount1Desired); + assertEq(balance0Before - balance0After, uint256(int256(-delta.amount0()))); + assertEq(balance1Before - balance1After, uint256(int256(-delta.amount1()))); + } + + function test_fuzz_mint_recipient(IPoolManager.ModifyLiquidityParams memory seedParams) public { + IPoolManager.ModifyLiquidityParams memory params = createFuzzyLiquidityParams(key, seedParams, SQRT_PRICE_1_1); + uint256 liquidityToAdd = + params.liquidityDelta < 0 ? uint256(-params.liquidityDelta) : uint256(params.liquidityDelta); + + PositionConfig memory config = + PositionConfig({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); + + uint256 tokenId = lpm.nextTokenId(); + uint256 balance0Before = currency0.balanceOfSelf(); + uint256 balance1Before = currency1.balanceOfSelf(); + uint256 balance0BeforeAlice = currency0.balanceOf(alice); + uint256 balance1BeforeAlice = currency1.balanceOf(alice); + mint(config, liquidityToAdd, alice, ZERO_BYTES); + BalanceDelta delta = getLastDelta(); + + assertEq(tokenId, 1); + assertEq(lpm.ownerOf(tokenId), alice); + + // alice was not the payer + assertEq(balance0Before - currency0.balanceOfSelf(), uint256(int256(-delta.amount0()))); + assertEq(balance1Before - currency1.balanceOfSelf(), uint256(int256(-delta.amount1()))); + assertEq(currency0.balanceOf(alice), balance0BeforeAlice); + assertEq(currency1.balanceOf(alice), balance1BeforeAlice); + } + + /// @dev test that clear does not work on minting + function test_fuzz_mint_clear_revert(IPoolManager.ModifyLiquidityParams memory seedParams) public { + IPoolManager.ModifyLiquidityParams memory params = createFuzzyLiquidityParams(key, seedParams, SQRT_PRICE_1_1); + uint256 liquidityToAdd = + params.liquidityDelta < 0 ? uint256(-params.liquidityDelta) : uint256(params.liquidityDelta); + + PositionConfig memory config = + PositionConfig({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); + + Plan memory planner = Planner.init(); + planner.add( + Actions.MINT_POSITION, + abi.encode(config, liquidityToAdd, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, address(this), ZERO_BYTES) + ); + planner.add(Actions.CLEAR, abi.encode(key.currency0, type(uint256).max)); + planner.add(Actions.CLEAR, abi.encode(key.currency1, type(uint256).max)); + bytes memory calls = planner.encode(); + + vm.expectRevert(SafeCast.SafeCastOverflow.selector); + lpm.modifyLiquidities(calls, _deadline); + } + + function test_mint_slippage_revertAmount0() public { + PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); + + bytes memory calls = getMintEncoded(config, 1e18, 1 wei, MAX_SLIPPAGE_INCREASE, address(this), ZERO_BYTES); + vm.expectRevert(SlippageCheckLibrary.MaximumAmountExceeded.selector); + lpm.modifyLiquidities(calls, _deadline); + } + + function test_mint_slippage_revertAmount1() public { + PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); + + bytes memory calls = getMintEncoded(config, 1e18, MAX_SLIPPAGE_INCREASE, 1 wei, address(this), ZERO_BYTES); + vm.expectRevert(SlippageCheckLibrary.MaximumAmountExceeded.selector); + lpm.modifyLiquidities(calls, _deadline); + } + + function test_mint_slippage_exactDoesNotRevert() public { + PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); + + uint256 liquidity = 1e18; + (uint256 amount0, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity( + SQRT_PRICE_1_1, + TickMath.getSqrtPriceAtTick(config.tickLower), + TickMath.getSqrtPriceAtTick(config.tickUpper), + uint128(liquidity) + ); + assertEq(amount0, amount1); // symmetric liquidity + uint128 slippage = uint128(amount0) + 1; + + bytes memory calls = getMintEncoded(config, liquidity, slippage, slippage, address(this), ZERO_BYTES); + lpm.modifyLiquidities(calls, _deadline); + BalanceDelta delta = getLastDelta(); + assertEq(uint256(int256(-delta.amount0())), slippage); + assertEq(uint256(int256(-delta.amount1())), slippage); + } + + function test_mint_slippage_revert_swap() public { + // swapping will cause a slippage revert + PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); + + uint256 liquidity = 100e18; + (uint256 amount0, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity( + SQRT_PRICE_1_1, + TickMath.getSqrtPriceAtTick(config.tickLower), + TickMath.getSqrtPriceAtTick(config.tickUpper), + uint128(liquidity) + ); + assertEq(amount0, amount1); // symmetric liquidity + uint128 slippage = uint128(amount0) + 1; + + bytes memory calls = getMintEncoded(config, liquidity, slippage, slippage, address(this), ZERO_BYTES); + + // swap to move the price and cause a slippage revert + swap(key, true, -1e18, ZERO_BYTES); + + vm.expectRevert(SlippageCheckLibrary.MaximumAmountExceeded.selector); + lpm.modifyLiquidities(calls, _deadline); + } + + function test_fuzz_burn_emptyPosition(IPoolManager.ModifyLiquidityParams memory params) public { + uint256 balance0Start = currency0.balanceOfSelf(); + uint256 balance1Start = currency1.balanceOfSelf(); + + // create liquidity we can burn + uint256 tokenId; + (tokenId, params) = addFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); + PositionConfig memory config = + PositionConfig({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); + assertEq(tokenId, 1); + assertEq(lpm.ownerOf(1), address(this)); + + bytes32 positionId = + Position.calculatePositionKey(address(lpm), config.tickLower, config.tickUpper, bytes32(tokenId)); + (uint256 liquidity,,) = manager.getPositionInfo(config.poolKey.toId(), positionId); + + assertEq(liquidity, uint256(params.liquidityDelta)); + + // burn liquidity + uint256 balance0BeforeBurn = currency0.balanceOfSelf(); + uint256 balance1BeforeBurn = currency1.balanceOfSelf(); + + decreaseLiquidity(tokenId, config, liquidity, ZERO_BYTES); + BalanceDelta deltaDecrease = getLastDelta(); + uint256 numDeltas = hook.numberDeltasReturned(); + // No decrease/modifyLiq call will actually happen on the call to burn so the deltas array will be the same length. + burn(tokenId, config, ZERO_BYTES); + assertEq(numDeltas, hook.numberDeltasReturned()); + + (liquidity,,) = manager.getPositionInfo(config.poolKey.toId(), positionId); + + assertEq(liquidity, 0); + + assertEq(currency0.balanceOfSelf(), balance0BeforeBurn + uint256(int256(deltaDecrease.amount0()))); + assertEq(currency1.balanceOfSelf(), balance1BeforeBurn + uint256(uint128(deltaDecrease.amount1()))); + + // OZ 721 will revert if the token does not exist + vm.expectRevert(); + lpm.ownerOf(1); + + // no tokens were lost, TODO: fuzzer showing off by 1 sometimes + // Potentially because we round down in core. I believe this is known in V3. But let's check! + assertApproxEqAbs(currency0.balanceOfSelf(), balance0Start, 1 wei); + assertApproxEqAbs(currency1.balanceOfSelf(), balance1Start, 1 wei); + } + + function test_fuzz_burn_nonEmptyPosition(IPoolManager.ModifyLiquidityParams memory params) public { + uint256 balance0Start = currency0.balanceOfSelf(); + uint256 balance1Start = currency1.balanceOfSelf(); + + // create liquidity we can burn + uint256 tokenId; + (tokenId, params) = addFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); + PositionConfig memory config = + PositionConfig({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); + assertEq(tokenId, 1); + assertEq(lpm.ownerOf(1), address(this)); + + bytes32 positionId = + Position.calculatePositionKey(address(lpm), config.tickLower, config.tickUpper, bytes32(tokenId)); + (uint256 liquidity,,) = manager.getPositionInfo(config.poolKey.toId(), positionId); + + assertEq(liquidity, uint256(params.liquidityDelta)); + + (uint160 sqrtPriceX96,,,) = manager.getSlot0(key.toId()); + (uint256 amount0, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity( + sqrtPriceX96, + TickMath.getSqrtPriceAtTick(params.tickLower), + TickMath.getSqrtPriceAtTick(params.tickUpper), + uint128(int128(params.liquidityDelta)) + ); + + // burn liquidity + uint256 balance0BeforeBurn = currency0.balanceOfSelf(); + uint256 balance1BeforeBurn = currency1.balanceOfSelf(); + + burn(tokenId, config, ZERO_BYTES); + BalanceDelta deltaBurn = getLastDelta(); + + assertEq(uint256(int256(deltaBurn.amount0())), amount0); + assertEq(uint256(int256(deltaBurn.amount1())), amount1); + + (liquidity,,) = manager.getPositionInfo(config.poolKey.toId(), positionId); + + assertEq(liquidity, 0); + + assertEq(currency0.balanceOfSelf(), balance0BeforeBurn + uint256(int256(deltaBurn.amount0()))); + assertEq(currency1.balanceOfSelf(), balance1BeforeBurn + uint256(uint128(deltaBurn.amount1()))); + + // OZ 721 will revert if the token does not exist + vm.expectRevert(); + lpm.ownerOf(1); + + // no tokens were lost, TODO: fuzzer showing off by 1 sometimes + // Potentially because we round down in core. I believe this is known in V3. But let's check! + assertApproxEqAbs(currency0.balanceOfSelf(), balance0Start, 1 wei); + assertApproxEqAbs(currency1.balanceOfSelf(), balance1Start, 1 wei); + } + + function test_burn_slippage_revertAmount0() public { + PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); + uint256 tokenId = lpm.nextTokenId(); + mint(config, 1e18, address(this), ZERO_BYTES); + BalanceDelta delta = getLastDelta(); + + bytes memory calls = + getBurnEncoded(tokenId, config, uint128(-delta.amount0()) + 1 wei, MIN_SLIPPAGE_DECREASE, ZERO_BYTES); + vm.expectRevert(SlippageCheckLibrary.MinimumAmountInsufficient.selector); + lpm.modifyLiquidities(calls, _deadline); + } + + function test_burn_slippage_revertAmount1() public { + PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); + uint256 tokenId = lpm.nextTokenId(); + mint(config, 1e18, address(this), ZERO_BYTES); + BalanceDelta delta = getLastDelta(); + + bytes memory calls = + getBurnEncoded(tokenId, config, MIN_SLIPPAGE_DECREASE, uint128(-delta.amount1()) + 1 wei, ZERO_BYTES); + vm.expectRevert(SlippageCheckLibrary.MinimumAmountInsufficient.selector); + lpm.modifyLiquidities(calls, _deadline); + } + + function test_burn_slippage_exactDoesNotRevert() public { + PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); + uint256 tokenId = lpm.nextTokenId(); + mint(config, 1e18, address(this), ZERO_BYTES); + BalanceDelta delta = getLastDelta(); + + // TODO: why does burning a newly minted position return original delta - 1 wei? + bytes memory calls = getBurnEncoded( + tokenId, config, uint128(-delta.amount0()) - 1 wei, uint128(-delta.amount1()) - 1 wei, ZERO_BYTES + ); + lpm.modifyLiquidities(calls, _deadline); + BalanceDelta burnDelta = getLastDelta(); + + assertApproxEqAbs(-delta.amount0(), burnDelta.amount0(), 1 wei); + assertApproxEqAbs(-delta.amount1(), burnDelta.amount1(), 1 wei); + } + + function test_burn_slippage_revert_swap() public { + // swapping will cause a slippage revert + PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); + uint256 tokenId = lpm.nextTokenId(); + mint(config, 1e18, address(this), ZERO_BYTES); + BalanceDelta delta = getLastDelta(); + + bytes memory calls = getBurnEncoded( + tokenId, config, uint128(-delta.amount0()) - 1 wei, uint128(-delta.amount1()) - 1 wei, ZERO_BYTES + ); + + // swap to move the price and cause a slippage revert + swap(key, true, -1e18, ZERO_BYTES); + + vm.expectRevert(SlippageCheckLibrary.MinimumAmountInsufficient.selector); + lpm.modifyLiquidities(calls, _deadline); + } + + function test_fuzz_decreaseLiquidity( + IPoolManager.ModifyLiquidityParams memory params, + uint256 decreaseLiquidityDelta + ) public { + uint256 tokenId; + (tokenId, params) = addFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); + decreaseLiquidityDelta = uint256(bound(int256(decreaseLiquidityDelta), 0, params.liquidityDelta)); + + PositionConfig memory config = + PositionConfig({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); + + uint256 balance0Before = currency0.balanceOfSelf(); + uint256 balance1Before = currency1.balanceOfSelf(); + decreaseLiquidity(tokenId, config, decreaseLiquidityDelta, ZERO_BYTES); + BalanceDelta delta = getLastDelta(); + + bytes32 positionId = + Position.calculatePositionKey(address(lpm), config.tickLower, config.tickUpper, bytes32(tokenId)); + (uint256 liquidity,,) = manager.getPositionInfo(config.poolKey.toId(), positionId); + assertEq(liquidity, uint256(params.liquidityDelta) - decreaseLiquidityDelta); + + assertEq(currency0.balanceOfSelf(), balance0Before + uint256(uint128(delta.amount0()))); + assertEq(currency1.balanceOfSelf(), balance1Before + uint256(uint128(delta.amount1()))); + } + + /// @dev Clearing on decrease liquidity is allowed + function test_fuzz_decreaseLiquidity_clear( + IPoolManager.ModifyLiquidityParams memory params, + uint256 decreaseLiquidityDelta + ) public { + uint256 tokenId; + (tokenId, params) = addFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); + decreaseLiquidityDelta = uint256(bound(int256(decreaseLiquidityDelta), 0, params.liquidityDelta)); + + PositionConfig memory config = + PositionConfig({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); + + uint256 balance0Before = currency0.balanceOfSelf(); + uint256 balance1Before = currency1.balanceOfSelf(); + + // Clearing is allowed on decrease liquidity + Plan memory planner = Planner.init(); + planner.add( + Actions.DECREASE_LIQUIDITY, + abi.encode( + tokenId, config, decreaseLiquidityDelta, MIN_SLIPPAGE_DECREASE, MIN_SLIPPAGE_DECREASE, ZERO_BYTES + ) + ); + planner.add(Actions.CLEAR, abi.encode(key.currency0, type(uint256).max)); + planner.add(Actions.CLEAR, abi.encode(key.currency1, type(uint256).max)); + bytes memory calls = planner.encode(); + + lpm.modifyLiquidities(calls, _deadline); + + bytes32 positionId = + Position.calculatePositionKey(address(lpm), config.tickLower, config.tickUpper, bytes32(tokenId)); + (uint256 liquidity,,) = manager.getPositionInfo(config.poolKey.toId(), positionId); + assertEq(liquidity, uint256(params.liquidityDelta) - decreaseLiquidityDelta); + + // did not recieve tokens, as they were forfeited with CLEAR + assertEq(currency0.balanceOfSelf(), balance0Before); + assertEq(currency1.balanceOfSelf(), balance1Before); + } + + /// @dev Clearing on decrease reverts if it exceeds user threshold + function test_fuzz_decreaseLiquidity_clearRevert(IPoolManager.ModifyLiquidityParams memory params) public { + // use fuzzer for tick range + params = createFuzzyLiquidityParams(key, params, SQRT_PRICE_1_1); + vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity + + PositionConfig memory config = + PositionConfig({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); + + uint256 liquidityToAdd = 1e18; + uint256 liquidityToRemove = bound(liquidityToAdd, liquidityToAdd / 1000, liquidityToAdd); + uint256 tokenId = lpm.nextTokenId(); + mint(config, 1e18, address(this), ZERO_BYTES); + + (uint256 amount0, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity( + SQRT_PRICE_1_1, + TickMath.getSqrtPriceAtTick(config.tickLower), + TickMath.getSqrtPriceAtTick(config.tickUpper), + uint128(liquidityToRemove) + ); + + Plan memory planner = Planner.init(); + planner.add( + Actions.DECREASE_LIQUIDITY, + abi.encode(tokenId, config, liquidityToRemove, MIN_SLIPPAGE_DECREASE, MIN_SLIPPAGE_DECREASE, ZERO_BYTES) + ); + planner.add(Actions.CLEAR, abi.encode(key.currency0, amount0 - 1 wei)); + planner.add(Actions.CLEAR, abi.encode(key.currency1, amount1 - 1 wei)); + bytes memory calls = planner.encode(); + + vm.expectRevert( + abi.encodeWithSelector(IPositionManager.ClearExceedsMaxAmount.selector, currency0, amount0, amount0 - 1 wei) + ); + lpm.modifyLiquidities(calls, _deadline); + } + + function test_decreaseLiquidity_collectFees( + IPoolManager.ModifyLiquidityParams memory params, + uint256 decreaseLiquidityDelta + ) public { + uint256 tokenId; + (tokenId, params) = addFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); + vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity + decreaseLiquidityDelta = bound(decreaseLiquidityDelta, 1, uint256(params.liquidityDelta)); + + PositionConfig memory config = + PositionConfig({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); + + // donate to generate fee revenue + uint256 feeRevenue0 = 1e18; + uint256 feeRevenue1 = 0.1e18; + donateRouter.donate(key, feeRevenue0, feeRevenue1, ZERO_BYTES); + + uint256 balance0Before = currency0.balanceOfSelf(); + uint256 balance1Before = currency1.balanceOfSelf(); + decreaseLiquidity(tokenId, config, decreaseLiquidityDelta, ZERO_BYTES); + + bytes32 positionId = + Position.calculatePositionKey(address(lpm), config.tickLower, config.tickUpper, bytes32(tokenId)); + (uint256 liquidity,,) = manager.getPositionInfo(config.poolKey.toId(), positionId); + + assertEq(liquidity, uint256(params.liquidityDelta) - decreaseLiquidityDelta); + + (uint256 amount0, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity( + SQRT_PRICE_1_1, + TickMath.getSqrtPriceAtTick(config.tickLower), + TickMath.getSqrtPriceAtTick(config.tickUpper), + uint128(decreaseLiquidityDelta) + ); + + // claimed both principal liquidity and fee revenue + assertApproxEqAbs(currency0.balanceOfSelf() - balance0Before, amount0 + feeRevenue0, 1 wei); + assertApproxEqAbs(currency1.balanceOfSelf() - balance1Before, amount1 + feeRevenue1, 1 wei); + } + + function test_decreaseLiquidity_slippage_revertAmount0() public { + PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); + uint256 tokenId = lpm.nextTokenId(); + mint(config, 1e18, address(this), ZERO_BYTES); + BalanceDelta delta = getLastDelta(); + + bytes memory calls = getDecreaseEncoded( + tokenId, config, 1e18, uint128(-delta.amount0()) + 1 wei, MIN_SLIPPAGE_DECREASE, ZERO_BYTES + ); + vm.expectRevert(SlippageCheckLibrary.MinimumAmountInsufficient.selector); + lpm.modifyLiquidities(calls, _deadline); + } + + function test_decreaseLiquidity_slippage_revertAmount1() public { + PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); + uint256 tokenId = lpm.nextTokenId(); + mint(config, 1e18, address(this), ZERO_BYTES); + BalanceDelta delta = getLastDelta(); + + bytes memory calls = getDecreaseEncoded( + tokenId, config, 1e18, MIN_SLIPPAGE_DECREASE, uint128(-delta.amount1()) + 1 wei, ZERO_BYTES + ); + vm.expectRevert(SlippageCheckLibrary.MinimumAmountInsufficient.selector); + lpm.modifyLiquidities(calls, _deadline); + } + + function test_decreaseLiquidity_slippage_exactDoesNotRevert() public { + PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); + uint256 tokenId = lpm.nextTokenId(); + mint(config, 1e18, address(this), ZERO_BYTES); + BalanceDelta delta = getLastDelta(); + + // TODO: why does decreasing a newly minted position return original delta - 1 wei? + bytes memory calls = getDecreaseEncoded( + tokenId, config, 1e18, uint128(-delta.amount0()) - 1 wei, uint128(-delta.amount1()) - 1 wei, ZERO_BYTES + ); + lpm.modifyLiquidities(calls, _deadline); + BalanceDelta decreaseDelta = getLastDelta(); + + // TODO: why does decreasing a newly minted position return original delta - 1 wei? + assertApproxEqAbs(-delta.amount0(), decreaseDelta.amount0(), 1 wei); + assertApproxEqAbs(-delta.amount1(), decreaseDelta.amount1(), 1 wei); + } + + function test_decreaseLiquidity_slippage_revert_swap() public { + // swapping will cause a slippage revert + PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); + uint256 tokenId = lpm.nextTokenId(); + mint(config, 1e18, address(this), ZERO_BYTES); + BalanceDelta delta = getLastDelta(); + + bytes memory calls = getDecreaseEncoded( + tokenId, config, 1e18, uint128(-delta.amount0()) - 1 wei, uint128(-delta.amount1()) - 1 wei, ZERO_BYTES + ); + + // swap to move the price and cause a slippage revert + swap(key, true, -1e18, ZERO_BYTES); + + vm.expectRevert(SlippageCheckLibrary.MinimumAmountInsufficient.selector); + lpm.modifyLiquidities(calls, _deadline); + } + + function test_fuzz_decreaseLiquidity_assertCollectedBalance( + IPoolManager.ModifyLiquidityParams memory params, + uint256 decreaseLiquidityDelta + ) public { + uint256 tokenId; + (tokenId, params) = addFuzzyLiquidity(lpm, address(this), key, params, SQRT_PRICE_1_1, ZERO_BYTES); + vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity + vm.assume(0 < decreaseLiquidityDelta); + vm.assume(decreaseLiquidityDelta < uint256(type(int256).max)); + vm.assume(int256(decreaseLiquidityDelta) <= params.liquidityDelta); + + PositionConfig memory config = + PositionConfig({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); + + // swap to create fees + uint256 swapAmount = 0.01e18; + swap(key, false, int256(swapAmount), ZERO_BYTES); + + uint256 balance0Before = currency0.balanceOfSelf(); + uint256 balance1Before = currency1.balanceOfSelf(); + decreaseLiquidity(tokenId, config, decreaseLiquidityDelta, ZERO_BYTES); + BalanceDelta delta = getLastDelta(); + + bytes32 positionId = + Position.calculatePositionKey(address(lpm), config.tickLower, config.tickUpper, bytes32(tokenId)); + (uint256 liquidity,,) = manager.getPositionInfo(config.poolKey.toId(), positionId); + + assertEq(liquidity, uint256(params.liquidityDelta) - decreaseLiquidityDelta); + + // The change in balance equals the delta returned. + assertEq(currency0.balanceOfSelf() - balance0Before, uint256(int256(delta.amount0()))); + assertEq(currency1.balanceOfSelf() - balance1Before, uint256(int256(delta.amount1()))); + } + + function test_mintTransferBurn() public { + PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -600, tickUpper: 600}); + uint256 liquidity = 100e18; + uint256 tokenId = lpm.nextTokenId(); + mint(config, liquidity, address(this), ZERO_BYTES); + BalanceDelta mintDelta = getLastDelta(); + + // transfer to alice + lpm.transferFrom(address(this), alice, tokenId); + + // alice can burn the position + bytes memory calls = getBurnEncoded(tokenId, config, ZERO_BYTES); + + uint256 balance0BeforeAlice = currency0.balanceOf(alice); + uint256 balance1BeforeAlice = currency0.balanceOf(alice); + + vm.prank(alice); + lpm.modifyLiquidities(calls, _deadline); + + // token was burned and does not exist anymore + vm.expectRevert(); + lpm.ownerOf(tokenId); + + // alice received the principal liquidity + assertApproxEqAbs(currency0.balanceOf(alice) - balance0BeforeAlice, uint128(-mintDelta.amount0()), 1 wei); + assertApproxEqAbs(currency1.balanceOf(alice) - balance1BeforeAlice, uint128(-mintDelta.amount1()), 1 wei); + } + + function test_mintTransferCollect() public { + PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -600, tickUpper: 600}); + uint256 liquidity = 100e18; + uint256 tokenId = lpm.nextTokenId(); + mint(config, liquidity, address(this), ZERO_BYTES); + + // donate to generate fee revenue + uint256 feeRevenue0 = 1e18; + uint256 feeRevenue1 = 0.1e18; + donateRouter.donate(key, feeRevenue0, feeRevenue1, ZERO_BYTES); + + // transfer to alice + lpm.transferFrom(address(this), alice, tokenId); + + // alice can collect the fees + uint256 balance0BeforeAlice = currency0.balanceOf(alice); + uint256 balance1BeforeAlice = currency1.balanceOf(alice); + vm.startPrank(alice); + collect(tokenId, config, ZERO_BYTES); + BalanceDelta delta = getLastDelta(); + vm.stopPrank(); + + // alice received the fee revenue + assertApproxEqAbs(currency0.balanceOf(alice) - balance0BeforeAlice, feeRevenue0, 1 wei); + assertApproxEqAbs(currency1.balanceOf(alice) - balance1BeforeAlice, feeRevenue1, 1 wei); + assertApproxEqAbs(uint128(delta.amount0()), feeRevenue0, 1 wei); + assertApproxEqAbs(uint128(delta.amount1()), feeRevenue1, 1 wei); + } + + function test_mintTransferIncrease() public { + PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -600, tickUpper: 600}); + uint256 liquidity = 100e18; + uint256 tokenId = lpm.nextTokenId(); + mint(config, liquidity, address(this), ZERO_BYTES); + + // transfer to alice + lpm.transferFrom(address(this), alice, tokenId); + + // alice increases liquidity and is the payer + uint256 balance0BeforeAlice = currency0.balanceOf(alice); + uint256 balance1BeforeAlice = currency1.balanceOf(alice); + vm.startPrank(alice); + uint256 liquidityToAdd = 10e18; + increaseLiquidity(tokenId, config, liquidityToAdd, ZERO_BYTES); + BalanceDelta delta = getLastDelta(); + vm.stopPrank(); + + // position liquidity increased + bytes32 positionId = + Position.calculatePositionKey(address(lpm), config.tickLower, config.tickUpper, bytes32(tokenId)); + (uint256 newLiq,,) = manager.getPositionInfo(config.poolKey.toId(), positionId); + assertEq(newLiq, liquidity + liquidityToAdd); + + // alice paid the tokens + (uint256 amount0, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity( + SQRT_PRICE_1_1, + TickMath.getSqrtPriceAtTick(config.tickLower), + TickMath.getSqrtPriceAtTick(config.tickUpper), + uint128(liquidityToAdd) + ); + assertApproxEqAbs(balance0BeforeAlice - currency0.balanceOf(alice), amount0, 1 wei); + assertApproxEqAbs(balance1BeforeAlice - currency1.balanceOf(alice), amount1, 1 wei); + assertApproxEqAbs(uint128(-delta.amount0()), amount0, 1 wei); + assertApproxEqAbs(uint128(-delta.amount1()), amount1, 1 wei); + } + + function test_mintTransferDecrease() public { + PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -600, tickUpper: 600}); + uint256 liquidity = 100e18; + uint256 tokenId = lpm.nextTokenId(); + mint(config, liquidity, address(this), ZERO_BYTES); + + // donate to generate fee revenue + uint256 feeRevenue0 = 1e18; + uint256 feeRevenue1 = 0.1e18; + donateRouter.donate(key, feeRevenue0, feeRevenue1, ZERO_BYTES); + + // transfer to alice + lpm.transferFrom(address(this), alice, tokenId); + + { + // alice decreases liquidity and is the recipient + uint256 balance0BeforeAlice = currency0.balanceOf(alice); + uint256 balance1BeforeAlice = currency1.balanceOf(alice); + vm.startPrank(alice); + uint256 liquidityToRemove = 10e18; + decreaseLiquidity(tokenId, config, liquidityToRemove, ZERO_BYTES); + BalanceDelta delta = getLastDelta(); + vm.stopPrank(); + + { + // position liquidity decreased + bytes32 positionId = + Position.calculatePositionKey(address(lpm), config.tickLower, config.tickUpper, bytes32(tokenId)); + (uint256 newLiq,,) = manager.getPositionInfo(config.poolKey.toId(), positionId); + assertEq(newLiq, liquidity - liquidityToRemove); + } + + // alice received the principal + fees + (uint256 amount0, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity( + SQRT_PRICE_1_1, + TickMath.getSqrtPriceAtTick(config.tickLower), + TickMath.getSqrtPriceAtTick(config.tickUpper), + uint128(liquidityToRemove) + ); + assertApproxEqAbs(currency0.balanceOf(alice) - balance0BeforeAlice, amount0 + feeRevenue0, 1 wei); + assertApproxEqAbs(currency1.balanceOf(alice) - balance1BeforeAlice, amount1 + feeRevenue1, 1 wei); + assertApproxEqAbs(uint128(delta.amount0()), amount0 + feeRevenue0, 1 wei); + assertApproxEqAbs(uint128(delta.amount1()), amount1 + feeRevenue1, 1 wei); + } + } + + 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); + + (uint160 sqrtPriceX96, int24 tick, uint24 protocolFee, uint24 lpFee) = manager.getSlot0(key.toId()); + assertEq(sqrtPriceX96, SQRT_PRICE_1_1); + assertEq(tick, 0); + assertEq(protocolFee, 0); + assertEq(lpFee, key.fee); + } + + function test_fuzz_initialize(uint160 sqrtPrice, uint24 fee) public { + sqrtPrice = + uint160(bound(sqrtPrice, TickMath.MIN_SQRT_PRICE, TickMath.MAX_SQRT_PRICE_MINUS_MIN_SQRT_PRICE_MINUS_ONE)); + 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); + + (uint160 sqrtPriceX96, int24 tick, uint24 protocolFee, uint24 lpFee) = manager.getSlot0(key.toId()); + assertEq(sqrtPriceX96, sqrtPrice); + assertEq(tick, TickMath.getTickAtSqrtPrice(sqrtPrice)); + assertEq(protocolFee, 0); + assertEq(lpFee, fee); + } + + function test_mint_slippageRevert() public {} +} diff --git a/test/router/Payments.gas.t.sol b/test/router/Payments.gas.t.sol new file mode 100644 index 00000000..241278cd --- /dev/null +++ b/test/router/Payments.gas.t.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: UNLICENSED +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 {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"; + +contract PaymentsTests is RoutingTestHelpers, GasSnapshot { + using CurrencyLibrary for Currency; + using Planner for Plan; + + function setUp() public { + setupRouterCurrenciesAndPoolsWithLiquidity(); + plan = Planner.init(); + } + + function test_gas_swap_settleFromCaller_takeAll() public { + uint256 amountIn = 1 ether; + 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_ALL, abi.encode(key0.currency0)); + plan = plan.add(Actions.TAKE_ALL, abi.encode(key0.currency1, address(this))); + + bytes memory data = plan.encode(); + router.executeActions(data); + snapLastCall("Payments_swap_settleFromCaller_takeAll"); + } + + function test_gas_swap_settleFromRouter_takeAll() public { + uint256 amountIn = 1 ether; + IV4Router.ExactInputSingleParams memory params = + IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, 0, bytes("")); + + // seed the router with tokens + key0.currency0.transfer(address(router), amountIn); + + plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); + plan = plan.add(Actions.SETTLE_WITH_BALANCE, abi.encode(key0.currency0)); + plan = plan.add(Actions.TAKE_ALL, abi.encode(key0.currency1, address(this))); + + bytes memory data = plan.encode(); + router.executeActions(data); + snapLastCall("Payments_swap_settleFromRouter_takeAll"); + } +} diff --git a/test/router/Payments.t.sol b/test/router/Payments.t.sol new file mode 100644 index 00000000..51dde092 --- /dev/null +++ b/test/router/Payments.t.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: UNLICENSED +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 {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"; + +contract PaymentsTests is RoutingTestHelpers, GasSnapshot { + using CurrencyLibrary for Currency; + using Planner for Plan; + + function setUp() public { + setupRouterCurrenciesAndPoolsWithLiquidity(); + plan = Planner.init(); + } + + function test_settleFromCaller_takeAll() 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_ALL, abi.encode(key0.currency0)); + plan = plan.add(Actions.TAKE_ALL, abi.encode(key0.currency1, address(this))); + + 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_settleFromRouter_takeAll() public { + uint256 amountIn = 1 ether; + uint256 expectedAmountOut = 992054607780215625; + IV4Router.ExactInputSingleParams memory params = + IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, 0, bytes("")); + + // seed the router with tokens + key0.currency0.transfer(address(router), amountIn); + + uint256 inputBalanceBefore = key0.currency0.balanceOfSelf(); + uint256 outputBalanceBefore = key0.currency1.balanceOfSelf(); + + // seeded tokens are in the router + assertEq(currency0.balanceOf(address(router)), amountIn); + assertEq(currency1.balanceOf(address(router)), 0); + + plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); + plan = plan.add(Actions.SETTLE_WITH_BALANCE, abi.encode(key0.currency0)); + plan = plan.add(Actions.TAKE_ALL, abi.encode(key0.currency1, address(this))); + + 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); + // callers input balance didnt change, but output balance did + assertEq(inputBalanceBefore, inputBalanceAfter); + assertEq(outputBalanceAfter - outputBalanceBefore, expectedAmountOut); + } +} diff --git a/test/router/V4Router.gas.t.sol b/test/router/V4Router.gas.t.sol new file mode 100644 index 00000000..2c48b2c6 --- /dev/null +++ b/test/router/V4Router.gas.t.sol @@ -0,0 +1,371 @@ +// SPDX-License-Identifier: UNLICENSED +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 {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"; + +contract V4RouterTest is RoutingTestHelpers, GasSnapshot { + using CurrencyLibrary for Currency; + using Planner for Plan; + + function setUp() public { + setupRouterCurrenciesAndPoolsWithLiquidity(); + plan = Planner.init(); + } + + function test_gas_bytecodeSize() public { + snapSize("V4Router_Bytecode", address(router)); + } + + /*////////////////////////////////////////////////////////////// + ERC20 -> ERC20 EXACT INPUT + //////////////////////////////////////////////////////////////*/ + + function test_gas_swapExactInputSingle_zeroForOne() public { + uint256 amountIn = 1 ether; + + IV4Router.ExactInputSingleParams memory params = + IV4Router.ExactInputSingleParams(key0, true, uint128(amountIn), 0, 0, bytes("")); + + plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); + bytes memory data = plan.finalizeSwap(key0.currency0, key0.currency1, address(this)); + + router.executeActions(data); + snapLastCall("V4Router_ExactInputSingle"); + } + + function test_gas_swapExactIn_1Hop_zeroForOne() public { + uint256 amountIn = 1 ether; + + tokenPath.push(currency0); + tokenPath.push(currency1); + IV4Router.ExactInputParams memory params = _getExactInputParams(tokenPath, amountIn); + + plan = plan.add(Actions.SWAP_EXACT_IN, abi.encode(params)); + bytes memory data = plan.finalizeSwap(currency0, currency1, address(this)); + + router.executeActions(data); + snapLastCall("V4Router_ExactIn1Hop_zeroForOne"); + } + + function test_gas_swapExactIn_1Hop_oneForZero() public { + uint256 amountIn = 1 ether; + + tokenPath.push(currency1); + tokenPath.push(currency0); + IV4Router.ExactInputParams memory params = _getExactInputParams(tokenPath, amountIn); + + plan = plan.add(Actions.SWAP_EXACT_IN, abi.encode(params)); + bytes memory data = plan.finalizeSwap(currency1, currency0, address(this)); + + router.executeActions(data); + snapLastCall("V4Router_ExactIn1Hop_oneForZero"); + } + + function test_gas_swapExactIn_2Hops() public { + uint256 amountIn = 1 ether; + + tokenPath.push(currency0); + tokenPath.push(currency1); + tokenPath.push(currency2); + IV4Router.ExactInputParams memory params = _getExactInputParams(tokenPath, amountIn); + + plan = plan.add(Actions.SWAP_EXACT_IN, abi.encode(params)); + bytes memory data = plan.finalizeSwap(currency0, currency2, address(this)); + + router.executeActions(data); + snapLastCall("V4Router_ExactIn2Hops"); + } + + function test_gas_swapExactIn_3Hops() public { + uint256 amountIn = 1 ether; + + tokenPath.push(currency0); + tokenPath.push(currency1); + tokenPath.push(currency2); + tokenPath.push(currency3); + IV4Router.ExactInputParams memory params = _getExactInputParams(tokenPath, amountIn); + + plan = plan.add(Actions.SWAP_EXACT_IN, abi.encode(params)); + bytes memory data = plan.finalizeSwap(currency0, currency3, address(this)); + + router.executeActions(data); + snapLastCall("V4Router_ExactIn3Hops"); + } + + /*////////////////////////////////////////////////////////////// + ETH -> ERC20 and ERC20 -> ETH EXACT INPUT + //////////////////////////////////////////////////////////////*/ + + function test_gas_nativeIn_swapExactInputSingle() public { + uint256 amountIn = 1 ether; + + IV4Router.ExactInputSingleParams memory params = + IV4Router.ExactInputSingleParams(nativeKey, true, uint128(amountIn), 0, 0, bytes("")); + + plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); + bytes memory data = plan.finalizeSwap(nativeKey.currency0, nativeKey.currency1, address(this)); + + router.executeActions{value: amountIn}(data); + snapLastCall("V4Router_ExactInputSingle_nativeIn"); + } + + function test_gas_nativeOut_swapExactInputSingle() public { + uint256 amountIn = 1 ether; + + IV4Router.ExactInputSingleParams memory params = + IV4Router.ExactInputSingleParams(nativeKey, false, uint128(amountIn), 0, 0, bytes("")); + + plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); + bytes memory data = plan.finalizeSwap(nativeKey.currency1, nativeKey.currency0, address(this)); + + router.executeActions(data); + snapLastCall("V4Router_ExactInputSingle_nativeOut"); + } + + function test_gas_nativeIn_swapExactIn_1Hop() public { + uint256 amountIn = 1 ether; + + tokenPath.push(CurrencyLibrary.NATIVE); + tokenPath.push(currency0); + IV4Router.ExactInputParams memory params = _getExactInputParams(tokenPath, amountIn); + + plan = plan.add(Actions.SWAP_EXACT_IN, abi.encode(params)); + bytes memory data = plan.finalizeSwap(CurrencyLibrary.NATIVE, currency0, address(this)); + + router.executeActions{value: amountIn}(data); + snapLastCall("V4Router_ExactIn1Hop_nativeIn"); + } + + function test_gas_nativeOut_swapExactIn_1Hop() public { + uint256 amountIn = 1 ether; + + tokenPath.push(currency0); + tokenPath.push(CurrencyLibrary.NATIVE); + IV4Router.ExactInputParams memory params = _getExactInputParams(tokenPath, amountIn); + + plan = plan.add(Actions.SWAP_EXACT_IN, abi.encode(params)); + bytes memory data = plan.finalizeSwap(currency0, CurrencyLibrary.NATIVE, address(this)); + + router.executeActions(data); + snapLastCall("V4Router_ExactIn1Hop_nativeOut"); + } + + function test_gas_nativeIn_swapExactIn_2Hops() public { + uint256 amountIn = 1 ether; + + tokenPath.push(CurrencyLibrary.NATIVE); + tokenPath.push(currency0); + tokenPath.push(currency1); + IV4Router.ExactInputParams memory params = _getExactInputParams(tokenPath, amountIn); + + plan = plan.add(Actions.SWAP_EXACT_IN, abi.encode(params)); + bytes memory data = plan.finalizeSwap(CurrencyLibrary.NATIVE, currency1, address(this)); + + router.executeActions{value: amountIn}(data); + snapLastCall("V4Router_ExactIn2Hops_nativeIn"); + } + + function test_gas_nativeIn_swapExactIn_3Hops() public { + uint256 amountIn = 1 ether; + + tokenPath.push(CurrencyLibrary.NATIVE); + tokenPath.push(currency0); + tokenPath.push(currency1); + tokenPath.push(currency2); + IV4Router.ExactInputParams memory params = _getExactInputParams(tokenPath, amountIn); + + plan = plan.add(Actions.SWAP_EXACT_IN, abi.encode(params)); + bytes memory data = plan.finalizeSwap(CurrencyLibrary.NATIVE, currency2, address(this)); + + router.executeActions{value: amountIn}(data); + snapLastCall("V4Router_ExactIn3Hops_nativeIn"); + } + + /*////////////////////////////////////////////////////////////// + ERC20 -> ERC20 EXACT OUTPUT + //////////////////////////////////////////////////////////////*/ + + function test_gas_swapExactOutputSingle_zeroForOne() public { + uint256 amountOut = 1 ether; + + IV4Router.ExactOutputSingleParams memory params = + IV4Router.ExactOutputSingleParams(key0, true, uint128(amountOut), type(uint128).max, 0, bytes("")); + + plan = plan.add(Actions.SWAP_EXACT_OUT_SINGLE, abi.encode(params)); + bytes memory data = plan.finalizeSwap(key0.currency0, key0.currency1, address(this)); + + router.executeActions(data); + snapLastCall("V4Router_ExactOutputSingle"); + } + + function test_gas_swapExactOut_1Hop_zeroForOne() public { + uint256 amountOut = 1 ether; + + tokenPath.push(currency0); + tokenPath.push(currency1); + IV4Router.ExactOutputParams memory params = _getExactOutputParams(tokenPath, amountOut); + + plan = plan.add(Actions.SWAP_EXACT_OUT, abi.encode(params)); + bytes memory data = plan.finalizeSwap(currency0, currency1, address(this)); + + router.executeActions(data); + snapLastCall("V4Router_ExactOut1Hop_zeroForOne"); + } + + function test_gas_swapExactOut_1Hop_oneForZero() public { + uint256 amountOut = 1 ether; + + tokenPath.push(currency1); + tokenPath.push(currency0); + IV4Router.ExactOutputParams memory params = _getExactOutputParams(tokenPath, amountOut); + + plan = plan.add(Actions.SWAP_EXACT_OUT, abi.encode(params)); + bytes memory data = plan.finalizeSwap(currency1, currency0, address(this)); + + router.executeActions(data); + snapLastCall("V4Router_ExactOut1Hop_oneForZero"); + } + + function test_gas_swapExactOut_2Hops() public { + uint256 amountOut = 1 ether; + + tokenPath.push(currency0); + tokenPath.push(currency1); + tokenPath.push(currency2); + IV4Router.ExactOutputParams memory params = _getExactOutputParams(tokenPath, amountOut); + + plan = plan.add(Actions.SWAP_EXACT_OUT, abi.encode(params)); + bytes memory data = plan.finalizeSwap(currency0, currency2, address(this)); + + router.executeActions(data); + snapLastCall("V4Router_ExactOut2Hops"); + } + + function test_gas_swapExactOut_3Hops() public { + uint256 amountOut = 1 ether; + + tokenPath.push(currency0); + tokenPath.push(currency1); + tokenPath.push(currency2); + tokenPath.push(currency3); + IV4Router.ExactOutputParams memory params = _getExactOutputParams(tokenPath, amountOut); + + plan = plan.add(Actions.SWAP_EXACT_OUT, abi.encode(params)); + bytes memory data = plan.finalizeSwap(currency0, currency3, address(this)); + + router.executeActions(data); + snapLastCall("V4Router_ExactOut3Hops"); + } + + /*////////////////////////////////////////////////////////////// + ETH -> ERC20 and ERC20 -> ETH EXACT OUTPUT + //////////////////////////////////////////////////////////////*/ + + function test_gas_nativeIn_swapExactOutputSingle_sweepExcessETH() public { + uint256 amountOut = 1 ether; + + IV4Router.ExactOutputSingleParams memory params = + IV4Router.ExactOutputSingleParams(nativeKey, true, uint128(amountOut), type(uint128).max, 0, bytes("")); + + plan = plan.add(Actions.SWAP_EXACT_OUT_SINGLE, abi.encode(params)); + bytes memory data = plan.finalizeSwap(nativeKey.currency0, nativeKey.currency1, address(this)); + + router.executeActionsAndSweepExcessETH{value: 2 ether}(data); + snapLastCall("V4Router_ExactOutputSingle_nativeIn_sweepETH"); + } + + function test_gas_nativeOut_swapExactOutputSingle() public { + uint256 amountOut = 1 ether; + + IV4Router.ExactOutputSingleParams memory params = + IV4Router.ExactOutputSingleParams(nativeKey, false, uint128(amountOut), type(uint128).max, 0, bytes("")); + + plan = plan.add(Actions.SWAP_EXACT_OUT_SINGLE, abi.encode(params)); + bytes memory data = plan.finalizeSwap(nativeKey.currency1, nativeKey.currency0, address(this)); + + router.executeActionsAndSweepExcessETH(data); + snapLastCall("V4Router_ExactOutputSingle_nativeOut"); + } + + function test_gas_nativeIn_swapExactOut_1Hop_sweepExcessETH() public { + uint256 amountOut = 1 ether; + + tokenPath.push(CurrencyLibrary.NATIVE); + tokenPath.push(currency0); + IV4Router.ExactOutputParams memory params = _getExactOutputParams(tokenPath, amountOut); + + plan = plan.add(Actions.SWAP_EXACT_OUT, abi.encode(params)); + bytes memory data = plan.finalizeSwap(CurrencyLibrary.NATIVE, currency0, address(this)); + + router.executeActionsAndSweepExcessETH{value: 2 ether}(data); + snapLastCall("V4Router_ExactOut1Hop_nativeIn_sweepETH"); + } + + function test_gas_nativeOut_swapExactOut_1Hop() public { + uint256 amountOut = 1 ether; + + tokenPath.push(currency0); + tokenPath.push(CurrencyLibrary.NATIVE); + IV4Router.ExactOutputParams memory params = _getExactOutputParams(tokenPath, amountOut); + + plan = plan.add(Actions.SWAP_EXACT_OUT, abi.encode(params)); + bytes memory data = plan.finalizeSwap(currency0, CurrencyLibrary.NATIVE, address(this)); + + router.executeActions(data); + snapLastCall("V4Router_ExactOut1Hop_nativeOut"); + } + + function test_gas_nativeIn_swapExactOut_2Hops_sweepExcessETH() public { + uint256 amountOut = 1 ether; + + tokenPath.push(CurrencyLibrary.NATIVE); + tokenPath.push(currency0); + tokenPath.push(currency1); + IV4Router.ExactOutputParams memory params = _getExactOutputParams(tokenPath, amountOut); + + plan = plan.add(Actions.SWAP_EXACT_OUT, abi.encode(params)); + bytes memory data = plan.finalizeSwap(CurrencyLibrary.NATIVE, currency1, address(this)); + + router.executeActionsAndSweepExcessETH{value: 2 ether}(data); + snapLastCall("V4Router_ExactOut2Hops_nativeIn"); + } + + function test_gas_nativeIn_swapExactOut_3Hops_sweepExcessETH() public { + uint256 amountOut = 1 ether; + + tokenPath.push(CurrencyLibrary.NATIVE); + tokenPath.push(currency0); + tokenPath.push(currency1); + tokenPath.push(currency2); + IV4Router.ExactOutputParams memory params = _getExactOutputParams(tokenPath, amountOut); + + plan = plan.add(Actions.SWAP_EXACT_OUT, abi.encode(params)); + bytes memory data = plan.finalizeSwap(CurrencyLibrary.NATIVE, currency2, address(this)); + + router.executeActionsAndSweepExcessETH{value: 2 ether}(data); + snapLastCall("V4Router_ExactOut3Hops_nativeIn"); + } + + function test_gas_nativeOut_swapExactOut_3Hops() public { + uint256 amountOut = 1 ether; + + tokenPath.push(currency2); + tokenPath.push(currency1); + tokenPath.push(currency0); + tokenPath.push(CurrencyLibrary.NATIVE); + + IV4Router.ExactOutputParams memory params = _getExactOutputParams(tokenPath, amountOut); + + plan = plan.add(Actions.SWAP_EXACT_OUT, abi.encode(params)); + bytes memory data = plan.finalizeSwap(currency2, CurrencyLibrary.NATIVE, address(this)); + + router.executeActions(data); + snapLastCall("V4Router_ExactOut3Hops_nativeOut"); + } +} diff --git a/test/router/V4Router.t.sol b/test/router/V4Router.t.sol new file mode 100644 index 00000000..bc34f0a5 --- /dev/null +++ b/test/router/V4Router.t.sol @@ -0,0 +1,591 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.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"; + +contract V4RouterTest is RoutingTestHelpers { + using CurrencyLibrary for Currency; + using Planner for Plan; + + function setUp() public { + setupRouterCurrenciesAndPoolsWithLiquidity(); + plan = Planner.init(); + } + + /*////////////////////////////////////////////////////////////// + ERC20 -> ERC20 EXACT INPUT + //////////////////////////////////////////////////////////////*/ + + function test_swapExactInputSingle_revertsForAmountOut() public { + uint256 amountIn = 1 ether; + 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("") + ); + + plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); + bytes memory data = plan.finalizeSwap(key0.currency0, key0.currency1, address(this)); + + vm.expectRevert(IV4Router.TooLittleReceived.selector); + router.executeActions(data); + } + + function test_swapExactInputSingle_zeroForOne() 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)); + (uint256 inputBalanceBefore, uint256 outputBalanceBefore, uint256 inputBalanceAfter, uint256 outputBalanceAfter) + = _finalizeAndExecuteSwap(key0.currency0, key0.currency1, amountIn); + + assertEq(currency0.balanceOf(address(router)), 0); + assertEq(currency1.balanceOf(address(router)), 0); + + assertEq(inputBalanceBefore - inputBalanceAfter, amountIn); + assertEq(outputBalanceAfter - outputBalanceBefore, expectedAmountOut); + } + + function test_swapExactInputSingle_oneForZero() public { + uint256 amountIn = 1 ether; + uint256 expectedAmountOut = 992054607780215625; + + IV4Router.ExactInputSingleParams memory params = + IV4Router.ExactInputSingleParams(key0, false, uint128(amountIn), 0, 0, bytes("")); + plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); + + (uint256 inputBalanceBefore, uint256 outputBalanceBefore, uint256 inputBalanceAfter, uint256 outputBalanceAfter) + = _finalizeAndExecuteSwap(key0.currency1, key0.currency0, amountIn); + + assertEq(currency0.balanceOf(address(router)), 0); + assertEq(currency1.balanceOf(address(router)), 0); + + assertEq(inputBalanceBefore - inputBalanceAfter, amountIn); + assertEq(outputBalanceAfter - outputBalanceBefore, expectedAmountOut); + } + + function test_swapExactInput_revertsForAmountOut() public { + uint256 amountIn = 1 ether; + uint256 expectedAmountOut = 992054607780215625; + + tokenPath.push(currency0); + tokenPath.push(currency1); + IV4Router.ExactInputParams memory params = _getExactInputParams(tokenPath, amountIn); + params.amountOutMinimum = uint128(expectedAmountOut + 1); + + plan = plan.add(Actions.SWAP_EXACT_IN, abi.encode(params)); + bytes memory data = plan.finalizeSwap(key0.currency0, key0.currency1, address(this)); + + vm.expectRevert(IV4Router.TooLittleReceived.selector); + router.executeActions(data); + } + + function test_swapExactIn_1Hop_zeroForOne() public { + uint256 amountIn = 1 ether; + uint256 expectedAmountOut = 992054607780215625; + + tokenPath.push(currency0); + tokenPath.push(currency1); + IV4Router.ExactInputParams memory params = _getExactInputParams(tokenPath, amountIn); + plan = plan.add(Actions.SWAP_EXACT_IN, abi.encode(params)); + + (uint256 inputBalanceBefore, uint256 outputBalanceBefore, uint256 inputBalanceAfter, uint256 outputBalanceAfter) + = _finalizeAndExecuteSwap(currency0, currency1, amountIn); + + assertEq(currency0.balanceOf(address(router)), 0); + assertEq(currency1.balanceOf(address(router)), 0); + + assertEq(inputBalanceBefore - inputBalanceAfter, amountIn); + assertEq(outputBalanceAfter - outputBalanceBefore, expectedAmountOut); + } + + function test_swapExactIn_1Hop_oneForZero() public { + uint256 amountIn = 1 ether; + uint256 expectedAmountOut = 992054607780215625; + + tokenPath.push(currency1); + tokenPath.push(currency0); + IV4Router.ExactInputParams memory params = _getExactInputParams(tokenPath, amountIn); + + plan = plan.add(Actions.SWAP_EXACT_IN, abi.encode(params)); + + (uint256 inputBalanceBefore, uint256 outputBalanceBefore, uint256 inputBalanceAfter, uint256 outputBalanceAfter) + = _finalizeAndExecuteSwap(currency1, currency0, amountIn); + + assertEq(currency0.balanceOf(address(router)), 0); + assertEq(currency1.balanceOf(address(router)), 0); + + assertEq(inputBalanceBefore - inputBalanceAfter, amountIn); + assertEq(outputBalanceAfter - outputBalanceBefore, expectedAmountOut); + } + + function test_swapExactIn_2Hops() public { + uint256 amountIn = 1 ether; + uint256 expectedAmountOut = 984211133872795298; + + tokenPath.push(currency0); + tokenPath.push(currency1); + tokenPath.push(currency2); + IV4Router.ExactInputParams memory params = _getExactInputParams(tokenPath, amountIn); + + plan = plan.add(Actions.SWAP_EXACT_IN, abi.encode(params)); + + uint256 intermediateBalanceBefore = currency1.balanceOfSelf(); + + (uint256 inputBalanceBefore, uint256 outputBalanceBefore, uint256 inputBalanceAfter, uint256 outputBalanceAfter) + = _finalizeAndExecuteSwap(currency0, currency2, amountIn); + + // check intermediate token balances + assertEq(intermediateBalanceBefore, currency1.balanceOfSelf()); + assertEq(currency0.balanceOf(address(router)), 0); + assertEq(currency1.balanceOf(address(router)), 0); + assertEq(currency2.balanceOf(address(router)), 0); + + assertEq(inputBalanceBefore - inputBalanceAfter, amountIn); + assertEq(outputBalanceAfter - outputBalanceBefore, expectedAmountOut); + } + + function test_swapExactIn_3Hops() public { + uint256 amountIn = 1 ether; + uint256 expectedAmountOut = 976467664490096191; + + tokenPath.push(currency0); + tokenPath.push(currency1); + tokenPath.push(currency2); + tokenPath.push(currency3); + IV4Router.ExactInputParams memory params = _getExactInputParams(tokenPath, amountIn); + + plan = plan.add(Actions.SWAP_EXACT_IN, abi.encode(params)); + + (uint256 inputBalanceBefore, uint256 outputBalanceBefore, uint256 inputBalanceAfter, uint256 outputBalanceAfter) + = _finalizeAndExecuteSwap(currency0, currency3, amountIn); + + // check intermediate tokens werent left in the router + assertEq(currency0.balanceOf(address(router)), 0); + assertEq(currency1.balanceOf(address(router)), 0); + assertEq(currency2.balanceOf(address(router)), 0); + assertEq(currency3.balanceOf(address(router)), 0); + + assertEq(inputBalanceBefore - inputBalanceAfter, amountIn); + assertEq(outputBalanceAfter - outputBalanceBefore, expectedAmountOut); + } + + /*////////////////////////////////////////////////////////////// + ETH -> ERC20 and ERC20 -> ETH EXACT INPUT + //////////////////////////////////////////////////////////////*/ + + function test_nativeIn_swapExactInputSingle() public { + uint256 amountIn = 1 ether; + uint256 expectedAmountOut = 992054607780215625; + + IV4Router.ExactInputSingleParams memory params = + IV4Router.ExactInputSingleParams(nativeKey, true, uint128(amountIn), 0, 0, bytes("")); + + plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); + + (uint256 inputBalanceBefore, uint256 outputBalanceBefore, uint256 inputBalanceAfter, uint256 outputBalanceAfter) + = _finalizeAndExecuteSwap(nativeKey.currency0, nativeKey.currency1, amountIn); + + assertEq(nativeKey.currency0.balanceOf(address(router)), 0); + assertEq(nativeKey.currency1.balanceOf(address(router)), 0); + + assertEq(inputBalanceBefore - inputBalanceAfter, amountIn); + assertEq(outputBalanceAfter - outputBalanceBefore, expectedAmountOut); + } + + function test_nativeOut_swapExactInputSingle() public { + uint256 amountIn = 1 ether; + uint256 expectedAmountOut = 992054607780215625; + + // native output means we need !zeroForOne + IV4Router.ExactInputSingleParams memory params = + IV4Router.ExactInputSingleParams(nativeKey, false, uint128(amountIn), 0, 0, bytes("")); + + plan = plan.add(Actions.SWAP_EXACT_IN_SINGLE, abi.encode(params)); + + (uint256 inputBalanceBefore, uint256 outputBalanceBefore, uint256 inputBalanceAfter, uint256 outputBalanceAfter) + = _finalizeAndExecuteSwap(nativeKey.currency1, nativeKey.currency0, amountIn); + + assertEq(nativeKey.currency0.balanceOf(address(router)), 0); + assertEq(nativeKey.currency1.balanceOf(address(router)), 0); + + assertEq(inputBalanceBefore - inputBalanceAfter, amountIn); + assertEq(outputBalanceAfter - outputBalanceBefore, expectedAmountOut); + } + + function test_nativeIn_swapExactIn_1Hop() public { + uint256 amountIn = 1 ether; + uint256 expectedAmountOut = 992054607780215625; + + tokenPath.push(CurrencyLibrary.NATIVE); + tokenPath.push(nativeKey.currency1); + IV4Router.ExactInputParams memory params = _getExactInputParams(tokenPath, amountIn); + plan = plan.add(Actions.SWAP_EXACT_IN, abi.encode(params)); + + (uint256 inputBalanceBefore, uint256 outputBalanceBefore, uint256 inputBalanceAfter, uint256 outputBalanceAfter) + = _finalizeAndExecuteSwap(CurrencyLibrary.NATIVE, nativeKey.currency1, amountIn); + + assertEq(nativeKey.currency0.balanceOf(address(router)), 0); + assertEq(nativeKey.currency1.balanceOf(address(router)), 0); + + assertEq(inputBalanceBefore - inputBalanceAfter, amountIn); + assertEq(outputBalanceAfter - outputBalanceBefore, expectedAmountOut); + } + + function test_nativeOut_swapExactIn_1Hop() public { + uint256 amountIn = 1 ether; + uint256 expectedAmountOut = 992054607780215625; + + tokenPath.push(nativeKey.currency1); + tokenPath.push(CurrencyLibrary.NATIVE); + IV4Router.ExactInputParams memory params = _getExactInputParams(tokenPath, amountIn); + + plan = plan.add(Actions.SWAP_EXACT_IN, abi.encode(params)); + + (uint256 inputBalanceBefore, uint256 outputBalanceBefore, uint256 inputBalanceAfter, uint256 outputBalanceAfter) + = _finalizeAndExecuteSwap(nativeKey.currency1, CurrencyLibrary.NATIVE, amountIn); + + assertEq(nativeKey.currency0.balanceOf(address(router)), 0); + assertEq(nativeKey.currency1.balanceOf(address(router)), 0); + + assertEq(inputBalanceBefore - inputBalanceAfter, amountIn); + assertEq(outputBalanceAfter - outputBalanceBefore, expectedAmountOut); + } + + function test_nativeIn_swapExactIn_2Hops() public { + uint256 amountIn = 1 ether; + uint256 expectedAmountOut = 984211133872795298; + + // the initialized nativeKey is (native, currency0) + tokenPath.push(CurrencyLibrary.NATIVE); + tokenPath.push(currency0); + tokenPath.push(currency1); + IV4Router.ExactInputParams memory params = _getExactInputParams(tokenPath, amountIn); + + plan = plan.add(Actions.SWAP_EXACT_IN, abi.encode(params)); + + uint256 intermediateBalanceBefore = currency0.balanceOfSelf(); + + (uint256 inputBalanceBefore, uint256 outputBalanceBefore, uint256 inputBalanceAfter, uint256 outputBalanceAfter) + = _finalizeAndExecuteSwap(CurrencyLibrary.NATIVE, currency1, amountIn); + + // check intermediate token balances + assertEq(intermediateBalanceBefore, currency0.balanceOfSelf()); + + assertEq(nativeKey.currency0.balanceOf(address(router)), 0); + assertEq(nativeKey.currency1.balanceOf(address(router)), 0); + assertEq(currency1.balanceOf(address(router)), 0); + + assertEq(inputBalanceBefore - inputBalanceAfter, amountIn); + assertEq(outputBalanceAfter - outputBalanceBefore, expectedAmountOut); + } + + function test_nativeOut_swapExactIn_2Hops() public { + uint256 amountIn = 1 ether; + uint256 expectedAmountOut = 984211133872795298; + + // the initialized nativeKey is (native, currency0) + tokenPath.push(currency1); + tokenPath.push(currency0); + tokenPath.push(CurrencyLibrary.NATIVE); + IV4Router.ExactInputParams memory params = _getExactInputParams(tokenPath, amountIn); + + plan = plan.add(Actions.SWAP_EXACT_IN, abi.encode(params)); + + uint256 intermediateBalanceBefore = currency0.balanceOfSelf(); + + (uint256 inputBalanceBefore, uint256 outputBalanceBefore, uint256 inputBalanceAfter, uint256 outputBalanceAfter) + = _finalizeAndExecuteSwap(currency1, CurrencyLibrary.NATIVE, amountIn); + + // check intermediate token balances + assertEq(intermediateBalanceBefore, currency0.balanceOfSelf()); + assertEq(nativeKey.currency0.balanceOf(address(router)), 0); + assertEq(nativeKey.currency1.balanceOf(address(router)), 0); + assertEq(currency1.balanceOf(address(router)), 0); + + assertEq(inputBalanceBefore - inputBalanceAfter, amountIn); + assertEq(outputBalanceAfter - outputBalanceBefore, expectedAmountOut); + } + + /*//////////////////////////////////////////////////////////////å + ERC20 -> ERC20 EXACT OUTPUT + //////////////////////////////////////////////////////////////*/ + + function test_swapExactOutputSingle_revertsForAmountIn() public { + uint256 amountOut = 1 ether; + uint256 expectedAmountIn = 1008049273448486163; + + IV4Router.ExactOutputSingleParams memory params = IV4Router.ExactOutputSingleParams( + key0, true, uint128(amountOut), uint128(expectedAmountIn - 1), 0, bytes("") + ); + + plan = plan.add(Actions.SWAP_EXACT_OUT_SINGLE, abi.encode(params)); + bytes memory data = plan.finalizeSwap(key0.currency0, key0.currency1, address(this)); + + vm.expectRevert(IV4Router.TooMuchRequested.selector); + router.executeActions(data); + } + + function test_swapExactOutputSingle_zeroForOne() public { + uint256 amountOut = 1 ether; + uint256 expectedAmountIn = 1008049273448486163; + + IV4Router.ExactOutputSingleParams memory params = IV4Router.ExactOutputSingleParams( + key0, true, uint128(amountOut), uint128(expectedAmountIn + 1), 0, bytes("") + ); + + plan = plan.add(Actions.SWAP_EXACT_OUT_SINGLE, abi.encode(params)); + + (uint256 inputBalanceBefore, uint256 outputBalanceBefore, uint256 inputBalanceAfter, uint256 outputBalanceAfter) + = _finalizeAndExecuteSwap(key0.currency0, key0.currency1, expectedAmountIn); + + assertEq(currency0.balanceOf(address(router)), 0); + assertEq(currency1.balanceOf(address(router)), 0); + + assertEq(inputBalanceBefore - inputBalanceAfter, expectedAmountIn); + assertEq(outputBalanceAfter - outputBalanceBefore, amountOut); + } + + function test_swapExactOutputSingle_oneForZero() public { + uint256 amountOut = 1 ether; + uint256 expectedAmountIn = 1008049273448486163; + + IV4Router.ExactOutputSingleParams memory params = IV4Router.ExactOutputSingleParams( + key0, false, uint128(amountOut), uint128(expectedAmountIn + 1), 0, bytes("") + ); + + plan = plan.add(Actions.SWAP_EXACT_OUT_SINGLE, abi.encode(params)); + + (uint256 inputBalanceBefore, uint256 outputBalanceBefore, uint256 inputBalanceAfter, uint256 outputBalanceAfter) + = _finalizeAndExecuteSwap(key0.currency1, key0.currency0, expectedAmountIn); + + assertEq(currency0.balanceOf(address(router)), 0); + assertEq(currency1.balanceOf(address(router)), 0); + + assertEq(inputBalanceBefore - inputBalanceAfter, expectedAmountIn); + assertEq(outputBalanceAfter - outputBalanceBefore, amountOut); + } + + function test_swapExactOut_revertsForAmountIn() public { + uint256 amountOut = 1 ether; + uint256 expectedAmountIn = 1008049273448486163; + + tokenPath.push(currency0); + tokenPath.push(currency1); + IV4Router.ExactOutputParams memory params = _getExactOutputParams(tokenPath, amountOut); + params.amountInMaximum = uint128(expectedAmountIn - 1); + + plan = plan.add(Actions.SWAP_EXACT_OUT, abi.encode(params)); + bytes memory data = plan.finalizeSwap(key0.currency0, key0.currency1, address(this)); + + vm.expectRevert(IV4Router.TooMuchRequested.selector); + router.executeActions(data); + } + + function test_swapExactOut_1Hop_zeroForOne() public { + uint256 amountOut = 1 ether; + uint256 expectedAmountIn = 1008049273448486163; + + tokenPath.push(currency0); + tokenPath.push(currency1); + IV4Router.ExactOutputParams memory params = _getExactOutputParams(tokenPath, amountOut); + + plan = plan.add(Actions.SWAP_EXACT_OUT, abi.encode(params)); + + (uint256 inputBalanceBefore, uint256 outputBalanceBefore, uint256 inputBalanceAfter, uint256 outputBalanceAfter) + = _finalizeAndExecuteSwap(key0.currency0, key0.currency1, expectedAmountIn); + + assertEq(currency0.balanceOf(address(router)), 0); + assertEq(currency1.balanceOf(address(router)), 0); + + assertEq(inputBalanceBefore - inputBalanceAfter, expectedAmountIn); + assertEq(outputBalanceAfter - outputBalanceBefore, amountOut); + } + + function test_swapExactOut_1Hop_oneForZero() public { + uint256 amountOut = 1 ether; + uint256 expectedAmountIn = 1008049273448486163; + + tokenPath.push(currency1); + tokenPath.push(currency0); + IV4Router.ExactOutputParams memory params = _getExactOutputParams(tokenPath, amountOut); + + plan = plan.add(Actions.SWAP_EXACT_OUT, abi.encode(params)); + + (uint256 inputBalanceBefore, uint256 outputBalanceBefore, uint256 inputBalanceAfter, uint256 outputBalanceAfter) + = _finalizeAndExecuteSwap(currency1, currency0, expectedAmountIn); + + assertEq(currency0.balanceOf(address(router)), 0); + assertEq(currency1.balanceOf(address(router)), 0); + + assertEq(inputBalanceBefore - inputBalanceAfter, expectedAmountIn); + assertEq(outputBalanceAfter - outputBalanceBefore, amountOut); + } + + function test_swapExactOut_2Hops() public { + uint256 amountOut = 1 ether; + uint256 expectedAmountIn = 1016204441757464409; + + tokenPath.push(currency0); + tokenPath.push(currency1); + tokenPath.push(currency2); + IV4Router.ExactOutputParams memory params = _getExactOutputParams(tokenPath, amountOut); + + uint256 intermediateBalanceBefore = currency1.balanceOfSelf(); + + plan = plan.add(Actions.SWAP_EXACT_OUT, abi.encode(params)); + + (uint256 inputBalanceBefore, uint256 outputBalanceBefore, uint256 inputBalanceAfter, uint256 outputBalanceAfter) + = _finalizeAndExecuteSwap(currency0, currency2, expectedAmountIn); + + assertEq(intermediateBalanceBefore, currency1.balanceOfSelf()); + assertEq(currency0.balanceOf(address(router)), 0); + assertEq(currency1.balanceOf(address(router)), 0); + assertEq(currency2.balanceOf(address(router)), 0); + + assertEq(inputBalanceBefore - inputBalanceAfter, expectedAmountIn); + assertEq(outputBalanceAfter - outputBalanceBefore, amountOut); + } + + function test_swapExactOut_3Hops() public { + uint256 amountOut = 1 ether; + uint256 expectedAmountIn = 1024467570922834110; + + tokenPath.push(currency0); + tokenPath.push(currency1); + tokenPath.push(currency2); + tokenPath.push(currency3); + IV4Router.ExactOutputParams memory params = _getExactOutputParams(tokenPath, amountOut); + + plan = plan.add(Actions.SWAP_EXACT_OUT, abi.encode(params)); + + (uint256 inputBalanceBefore, uint256 outputBalanceBefore, uint256 inputBalanceAfter, uint256 outputBalanceAfter) + = _finalizeAndExecuteSwap(currency0, currency3, expectedAmountIn); + + assertEq(currency0.balanceOf(address(router)), 0); + assertEq(currency1.balanceOf(address(router)), 0); + assertEq(currency2.balanceOf(address(router)), 0); + assertEq(currency3.balanceOf(address(router)), 0); + + assertEq(inputBalanceBefore - inputBalanceAfter, expectedAmountIn); + assertEq(outputBalanceAfter - outputBalanceBefore, amountOut); + } + + /*////////////////////////////////////////////////////////////// + ETH -> ERC20 and ERC20 -> ETH EXACT OUTPUT + //////////////////////////////////////////////////////////////*/ + + function test_nativeIn_swapExactOutputSingle_sweepExcessETH() public { + uint256 amountOut = 1 ether; + uint256 expectedAmountIn = 1008049273448486163; + + IV4Router.ExactOutputSingleParams memory params = IV4Router.ExactOutputSingleParams( + nativeKey, true, uint128(amountOut), uint128(expectedAmountIn + 1), 0, bytes("") + ); + + plan = plan.add(Actions.SWAP_EXACT_OUT_SINGLE, abi.encode(params)); + + (uint256 inputBalanceBefore, uint256 outputBalanceBefore, uint256 inputBalanceAfter, uint256 outputBalanceAfter) + = _finalizeAndExecuteNativeInputExactOutputSwap(nativeKey.currency0, nativeKey.currency1, expectedAmountIn); + + assertEq(nativeKey.currency0.balanceOf(address(router)), 0); + assertEq(nativeKey.currency1.balanceOf(address(router)), 0); + + assertEq(inputBalanceBefore - inputBalanceAfter, expectedAmountIn); + assertEq(outputBalanceAfter - outputBalanceBefore, amountOut); + } + + function test_nativeOut_swapExactOutputSingle() public { + uint256 amountOut = 1 ether; + uint256 expectedAmountIn = 1008049273448486163; + + IV4Router.ExactOutputSingleParams memory params = IV4Router.ExactOutputSingleParams( + nativeKey, false, uint128(amountOut), uint128(expectedAmountIn + 1), 0, bytes("") + ); + + plan = plan.add(Actions.SWAP_EXACT_OUT_SINGLE, abi.encode(params)); + + (uint256 inputBalanceBefore, uint256 outputBalanceBefore, uint256 inputBalanceAfter, uint256 outputBalanceAfter) + = _finalizeAndExecuteSwap(nativeKey.currency1, nativeKey.currency0, expectedAmountIn); + + assertEq(nativeKey.currency0.balanceOf(address(router)), 0); + assertEq(nativeKey.currency1.balanceOf(address(router)), 0); + + assertEq(inputBalanceBefore - inputBalanceAfter, expectedAmountIn); + assertEq(outputBalanceAfter - outputBalanceBefore, amountOut); + } + + function test_nativeIn_swapExactOut_1Hop_sweepExcessETH() public { + uint256 amountOut = 1 ether; + uint256 expectedAmountIn = 1008049273448486163; + + tokenPath.push(CurrencyLibrary.NATIVE); + tokenPath.push(nativeKey.currency1); + IV4Router.ExactOutputParams memory params = _getExactOutputParams(tokenPath, amountOut); + + plan = plan.add(Actions.SWAP_EXACT_OUT, abi.encode(params)); + + (uint256 inputBalanceBefore, uint256 outputBalanceBefore, uint256 inputBalanceAfter, uint256 outputBalanceAfter) + = _finalizeAndExecuteNativeInputExactOutputSwap(CurrencyLibrary.NATIVE, nativeKey.currency1, expectedAmountIn); + + assertEq(nativeKey.currency0.balanceOf(address(router)), 0); + assertEq(nativeKey.currency1.balanceOf(address(router)), 0); + + assertEq(inputBalanceBefore - inputBalanceAfter, expectedAmountIn); + assertEq(outputBalanceAfter - outputBalanceBefore, amountOut); + } + + function test_nativeOut_swapExactOut_1Hop() public { + uint256 amountOut = 1 ether; + uint256 expectedAmountIn = 1008049273448486163; + + tokenPath.push(nativeKey.currency1); + tokenPath.push(CurrencyLibrary.NATIVE); + IV4Router.ExactOutputParams memory params = _getExactOutputParams(tokenPath, amountOut); + + plan = plan.add(Actions.SWAP_EXACT_OUT, abi.encode(params)); + + (uint256 inputBalanceBefore, uint256 outputBalanceBefore, uint256 inputBalanceAfter, uint256 outputBalanceAfter) + = _finalizeAndExecuteSwap(nativeKey.currency1, CurrencyLibrary.NATIVE, expectedAmountIn); + + assertEq(nativeKey.currency0.balanceOf(address(router)), 0); + assertEq(nativeKey.currency1.balanceOf(address(router)), 0); + + assertEq(inputBalanceBefore - inputBalanceAfter, expectedAmountIn); + assertEq(outputBalanceAfter - outputBalanceBefore, amountOut); + } + + function test_nativeIn_swapExactOut_2Hops_sweepExcessETH() public { + uint256 amountOut = 1 ether; + uint256 expectedAmountIn = 1016204441757464409; + + // the initialized nativeKey is (native, currency0) + tokenPath.push(CurrencyLibrary.NATIVE); + tokenPath.push(currency0); + tokenPath.push(currency1); + IV4Router.ExactOutputParams memory params = _getExactOutputParams(tokenPath, amountOut); + + uint256 intermediateBalanceBefore = currency0.balanceOfSelf(); + + plan = plan.add(Actions.SWAP_EXACT_OUT, abi.encode(params)); + + (uint256 inputBalanceBefore, uint256 outputBalanceBefore, uint256 inputBalanceAfter, uint256 outputBalanceAfter) + = _finalizeAndExecuteNativeInputExactOutputSwap(CurrencyLibrary.NATIVE, currency1, expectedAmountIn); + + assertEq(intermediateBalanceBefore, currency0.balanceOfSelf()); + assertEq(currency1.balanceOf(address(router)), 0); + assertEq(currency0.balanceOf(address(router)), 0); + assertEq(CurrencyLibrary.NATIVE.balanceOf(address(router)), 0); + + assertEq(inputBalanceBefore - inputBalanceAfter, expectedAmountIn); + assertEq(outputBalanceAfter - outputBalanceBefore, amountOut); + } +} diff --git a/test/shared/FeeMath.sol b/test/shared/FeeMath.sol new file mode 100644 index 00000000..25bdba5f --- /dev/null +++ b/test/shared/FeeMath.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import {FullMath} from "@uniswap/v4-core/src/libraries/FullMath.sol"; +import {FixedPoint128} from "@uniswap/v4-core/src/libraries/FixedPoint128.sol"; +import {Position} from "@uniswap/v4-core/src/libraries/Position.sol"; +import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; +import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; + +import {IPositionManager} from "../../src/interfaces/IPositionManager.sol"; +import {PositionManager} from "../../src/PositionManager.sol"; +import {PositionConfig} from "../../src/libraries/PositionConfig.sol"; + +library FeeMath { + using SafeCast for uint256; + using StateLibrary for IPoolManager; + using PoolIdLibrary for PoolKey; + using PoolIdLibrary for PoolKey; + + /// @notice Calculates the fees accrued to a position. Used for testing purposes. + function getFeesOwed(IPositionManager posm, IPoolManager manager, PositionConfig memory config, uint256 tokenId) + internal + view + returns (BalanceDelta feesOwed) + { + PoolId poolId = config.poolKey.toId(); + + // getPositionInfo(poolId, owner, tL, tU, salt) + // owner is the position manager + // salt is the tokenId + (uint128 liquidity, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128) = + manager.getPositionInfo(poolId, address(posm), config.tickLower, config.tickUpper, bytes32(tokenId)); + + (uint256 feeGrowthInside0X218, uint256 feeGrowthInside1X128) = + manager.getFeeGrowthInside(poolId, config.tickLower, config.tickUpper); + + feesOwed = getFeesOwed( + feeGrowthInside0X218, feeGrowthInside1X128, feeGrowthInside0LastX128, feeGrowthInside1LastX128, liquidity + ); + } + + function getFeesOwed( + uint256 feeGrowthInside0X128, + uint256 feeGrowthInside1X128, + uint256 feeGrowthInside0LastX128, + uint256 feeGrowthInside1LastX128, + uint256 liquidity + ) internal pure returns (BalanceDelta feesOwed) { + uint128 token0Owed = getFeeOwed(feeGrowthInside0X128, feeGrowthInside0LastX128, liquidity); + uint128 token1Owed = getFeeOwed(feeGrowthInside1X128, feeGrowthInside1LastX128, liquidity); + feesOwed = toBalanceDelta(uint256(token0Owed).toInt128(), uint256(token1Owed).toInt128()); + } + + function getFeeOwed(uint256 feeGrowthInsideX128, uint256 feeGrowthInsideLastX128, uint256 liquidity) + internal + pure + returns (uint128 tokenOwed) + { + tokenOwed = + (FullMath.mulDiv(feeGrowthInsideX128 - feeGrowthInsideLastX128, liquidity, FixedPoint128.Q128)).toUint128(); + } +} diff --git a/test/shared/GetSender.sol b/test/shared/GetSender.sol deleted file mode 100644 index d1709219..00000000 --- a/test/shared/GetSender.sol +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; - -contract GetSender { - function sender() external view returns (address) { - return msg.sender; - } -} diff --git a/test/shared/HookSavesDelta.sol b/test/shared/HookSavesDelta.sol new file mode 100644 index 00000000..8ff86ac1 --- /dev/null +++ b/test/shared/HookSavesDelta.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {BalanceDelta, BalanceDeltaLibrary} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; + +import {BaseTestHooks} from "@uniswap/v4-core/src/test/BaseTestHooks.sol"; + +/// @notice This contract is NOT a production use contract. It is meant to be used in testing to verify the delta amounts against changes in a user's balance. +contract HookSavesDelta is BaseTestHooks { + BalanceDelta[] public deltas; + + function afterAddLiquidity( + address, /* sender **/ + PoolKey calldata, /* key **/ + IPoolManager.ModifyLiquidityParams calldata, /* params **/ + BalanceDelta delta, + bytes calldata /* hookData **/ + ) external override returns (bytes4, BalanceDelta) { + _storeDelta(delta); + return (this.afterAddLiquidity.selector, BalanceDeltaLibrary.ZERO_DELTA); + } + + function afterRemoveLiquidity( + address, /* sender **/ + PoolKey calldata, /* key **/ + IPoolManager.ModifyLiquidityParams calldata, /* params **/ + BalanceDelta delta, + bytes calldata /* hookData **/ + ) external override returns (bytes4, BalanceDelta) { + _storeDelta(delta); + return (this.afterRemoveLiquidity.selector, BalanceDeltaLibrary.ZERO_DELTA); + } + + function _storeDelta(BalanceDelta delta) internal { + deltas.push(delta); + } + + function numberDeltasReturned() external view returns (uint256) { + return deltas.length; + } + + function clearDeltas() external { + delete deltas; + } +} diff --git a/test/shared/LiquidityOperations.sol b/test/shared/LiquidityOperations.sol new file mode 100644 index 00000000..a4623092 --- /dev/null +++ b/test/shared/LiquidityOperations.sol @@ -0,0 +1,199 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {CommonBase} from "forge-std/Base.sol"; +import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; +import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; +import {LiquidityAmounts} from "@uniswap/v4-core/test/utils/LiquidityAmounts.sol"; +import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; + +import {PositionManager, Actions} from "../../src/PositionManager.sol"; +import {PositionConfig} from "../../src/libraries/PositionConfig.sol"; +import {Planner, Plan} from "../shared/Planner.sol"; +import {HookSavesDelta} from "./HookSavesDelta.sol"; + +abstract contract LiquidityOperations is CommonBase { + using Planner for Plan; + using SafeCast for *; + + PositionManager lpm; + + uint256 _deadline = block.timestamp + 1; + + uint128 constant MAX_SLIPPAGE_INCREASE = type(uint128).max; + uint128 constant MIN_SLIPPAGE_DECREASE = 0 wei; + + function mint(PositionConfig memory config, uint256 liquidity, address recipient, bytes memory hookData) internal { + bytes memory calls = getMintEncoded(config, liquidity, recipient, hookData); + lpm.modifyLiquidities(calls, _deadline); + } + + function mintWithNative( + uint160 sqrtPriceX96, + PositionConfig memory config, + uint256 liquidity, + address recipient, + bytes memory hookData + ) internal { + // determine the amount of ETH to send on-mint + (uint256 amount0,) = LiquidityAmounts.getAmountsForLiquidity( + sqrtPriceX96, + TickMath.getSqrtPriceAtTick(config.tickLower), + TickMath.getSqrtPriceAtTick(config.tickUpper), + liquidity.toUint128() + ); + bytes memory calls = getMintEncoded(config, liquidity, recipient, hookData); + // add extra wei because modifyLiquidities may be rounding up, LiquidityAmounts is imprecise? + lpm.modifyLiquidities{value: amount0 + 1}(calls, _deadline); + } + + function increaseLiquidity( + uint256 tokenId, + PositionConfig memory config, + uint256 liquidityToAdd, + bytes memory hookData + ) internal { + bytes memory calls = getIncreaseEncoded(tokenId, config, liquidityToAdd, hookData); + lpm.modifyLiquidities(calls, _deadline); + } + + // do not make external call before unlockAndExecute, allows us to test reverts + function decreaseLiquidity( + uint256 tokenId, + PositionConfig memory config, + uint256 liquidityToRemove, + bytes memory hookData + ) internal { + bytes memory calls = getDecreaseEncoded(tokenId, config, liquidityToRemove, hookData); + lpm.modifyLiquidities(calls, _deadline); + } + + function collect(uint256 tokenId, PositionConfig memory config, bytes memory hookData) internal { + bytes memory calls = getCollectEncoded(tokenId, config, hookData); + lpm.modifyLiquidities(calls, _deadline); + } + + // This is encoded with close calls. Not all burns need to be encoded with closes if there is no liquidity in the position. + function burn(uint256 tokenId, PositionConfig memory config, bytes memory hookData) internal { + bytes memory calls = getBurnEncoded(tokenId, config, hookData); + lpm.modifyLiquidities(calls, _deadline); + } + + // Helper functions for getting encoded calldata for .modifyLiquidities + function getMintEncoded(PositionConfig memory config, uint256 liquidity, address recipient, bytes memory hookData) + internal + pure + returns (bytes memory) + { + return getMintEncoded(config, liquidity, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, recipient, hookData); + } + + function getMintEncoded( + PositionConfig memory config, + uint256 liquidity, + uint128 amount0Max, + uint128 amount1Max, + address recipient, + bytes memory hookData + ) internal pure returns (bytes memory) { + Plan memory planner = Planner.init(); + planner.add(Actions.MINT_POSITION, abi.encode(config, liquidity, amount0Max, amount1Max, recipient, hookData)); + + return planner.finalizeModifyLiquidity(config.poolKey); + } + + function getIncreaseEncoded( + uint256 tokenId, + PositionConfig memory config, + uint256 liquidityToAdd, + bytes memory hookData + ) internal pure returns (bytes memory) { + // max slippage + return + getIncreaseEncoded(tokenId, config, liquidityToAdd, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, hookData); + } + + function getIncreaseEncoded( + uint256 tokenId, + PositionConfig memory config, + uint256 liquidityToAdd, + uint128 amount0Max, + uint128 amount1Max, + bytes memory hookData + ) internal pure returns (bytes memory) { + Plan memory planner = Planner.init(); + planner.add( + Actions.INCREASE_LIQUIDITY, abi.encode(tokenId, config, liquidityToAdd, amount0Max, amount1Max, hookData) + ); + return planner.finalizeModifyLiquidity(config.poolKey); + } + + function getDecreaseEncoded( + uint256 tokenId, + PositionConfig memory config, + uint256 liquidityToRemove, + bytes memory hookData + ) internal pure returns (bytes memory) { + return getDecreaseEncoded( + tokenId, config, liquidityToRemove, MIN_SLIPPAGE_DECREASE, MIN_SLIPPAGE_DECREASE, hookData + ); + } + + function getDecreaseEncoded( + uint256 tokenId, + PositionConfig memory config, + uint256 liquidityToRemove, + uint128 amount0Min, + uint128 amount1Min, + bytes memory hookData + ) internal pure returns (bytes memory) { + Plan memory planner = Planner.init(); + planner.add( + Actions.DECREASE_LIQUIDITY, abi.encode(tokenId, config, liquidityToRemove, amount0Min, amount1Min, hookData) + ); + return planner.finalizeModifyLiquidity(config.poolKey); + } + + function getCollectEncoded(uint256 tokenId, PositionConfig memory config, bytes memory hookData) + internal + pure + returns (bytes memory) + { + return getCollectEncoded(tokenId, config, MIN_SLIPPAGE_DECREASE, MIN_SLIPPAGE_DECREASE, hookData); + } + + function getCollectEncoded( + uint256 tokenId, + PositionConfig memory config, + uint128 amount0Min, + uint128 amount1Min, + bytes memory hookData + ) internal pure returns (bytes memory) { + Plan memory planner = Planner.init(); + planner.add(Actions.DECREASE_LIQUIDITY, abi.encode(tokenId, config, 0, amount0Min, amount1Min, hookData)); + return planner.finalizeModifyLiquidity(config.poolKey); + } + + function getBurnEncoded(uint256 tokenId, PositionConfig memory config, bytes memory hookData) + internal + pure + returns (bytes memory) + { + return getBurnEncoded(tokenId, config, MIN_SLIPPAGE_DECREASE, MIN_SLIPPAGE_DECREASE, hookData); + } + + function getBurnEncoded( + uint256 tokenId, + PositionConfig memory config, + uint128 amount0Min, + uint128 amount1Min, + bytes memory hookData + ) internal pure returns (bytes memory) { + Plan memory planner = Planner.init(); + planner.add(Actions.BURN_POSITION, abi.encode(tokenId, config, amount0Min, amount1Min, hookData)); + // Close needed on burn in case there is liquidity left in the position. + return planner.finalizeModifyLiquidity(config.poolKey); + } +} diff --git a/test/shared/Planner.sol b/test/shared/Planner.sol new file mode 100644 index 00000000..71020abe --- /dev/null +++ b/test/shared/Planner.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; + +import {IPositionManager} from "../../src/interfaces/IPositionManager.sol"; +import {Actions} from "../../src/libraries/Actions.sol"; +import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; + +struct Plan { + bytes actions; + bytes[] params; +} + +library Planner { + using Planner for Plan; + + function init() internal pure returns (Plan memory plan) { + return Plan({actions: bytes(""), params: new bytes[](0)}); + } + + function add(Plan memory plan, uint256 action, bytes memory param) internal pure returns (Plan memory) { + bytes memory actions = new bytes(plan.params.length + 1); + bytes[] memory params = new bytes[](plan.params.length + 1); + + for (uint256 i; i < params.length - 1; i++) { + // Copy from plan. + params[i] = plan.params[i]; + actions[i] = plan.actions[i]; + } + params[params.length - 1] = param; + actions[params.length - 1] = bytes1(uint8(action)); + + plan.actions = actions; + plan.params = params; + + return plan; + } + + function finalizeModifyLiquidity(Plan memory plan, PoolKey memory poolKey) internal pure returns (bytes memory) { + plan.add(Actions.CLOSE_CURRENCY, abi.encode(poolKey.currency0)); + plan.add(Actions.CLOSE_CURRENCY, abi.encode(poolKey.currency1)); + return plan.encode(); + } + + function encode(Plan memory plan) internal pure returns (bytes memory) { + return abi.encode(plan.actions, plan.params); + } + + function finalizeSwap(Plan memory plan, Currency inputCurrency, Currency outputCurrency, address recipient) + internal + pure + returns (bytes memory) + { + plan = plan.add(Actions.SETTLE_ALL, abi.encode(inputCurrency)); + plan = plan.add(Actions.TAKE_ALL, abi.encode(outputCurrency, recipient)); + return plan.encode(); + } +} diff --git a/test/shared/PosmTestSetup.sol b/test/shared/PosmTestSetup.sol new file mode 100644 index 00000000..4cfe740f --- /dev/null +++ b/test/shared/PosmTestSetup.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; +import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; +import {PositionManager} from "../../src/PositionManager.sol"; +import {IERC20} from "forge-std/interfaces/IERC20.sol"; +import {LiquidityOperations} from "./LiquidityOperations.sol"; +import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol"; +import {DeployPermit2} from "permit2/test/utils/DeployPermit2.sol"; +import {HookSavesDelta} from "./HookSavesDelta.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; + HookSavesDelta hook; + address hookAddr = address(uint160(Hooks.AFTER_ADD_LIQUIDITY_FLAG | Hooks.AFTER_REMOVE_LIQUIDITY_FLAG)); + + function deployPosmHookSavesDelta() public { + HookSavesDelta impl = new HookSavesDelta(); + vm.etch(hookAddr, address(impl).code); + hook = HookSavesDelta(hookAddr); + } + + function deployAndApprovePosm(IPoolManager poolManager) public { + deployPosm(poolManager); + approvePosm(); + } + + 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); + } + + function seedBalance(address to) internal { + IERC20(Currency.unwrap(currency0)).transfer(to, STARTING_USER_BALANCE); + IERC20(Currency.unwrap(currency1)).transfer(to, STARTING_USER_BALANCE); + } + + function approvePosm() internal { + approvePosmCurrency(currency0); + approvePosmCurrency(currency1); + } + + function approvePosmCurrency(Currency currency) internal { + // Because POSM uses permit2, we must execute 2 permits/approvals. + // 1. First, the caller must approve permit2 on the token. + IERC20(Currency.unwrap(currency)).approve(address(permit2), type(uint256).max); + // 2. Then, the caller must approve POSM as a spender of permit2. TODO: This could also be a signature. + permit2.approve(Currency.unwrap(currency), address(lpm), type(uint160).max, type(uint48).max); + } + + // Does the same approvals as approvePosm, but for a specific address. + function approvePosmFor(address addr) internal { + vm.startPrank(addr); + approvePosm(); + vm.stopPrank(); + } + + function getLastDelta() internal view returns (BalanceDelta delta) { + delta = hook.deltas(hook.numberDeltasReturned() - 1); // just want the most recently written delta + } + + function getNetDelta() internal view returns (BalanceDelta delta) { + uint256 numDeltas = hook.numberDeltasReturned(); + for (uint256 i = 0; i < numDeltas; i++) { + delta = delta + hook.deltas(i); + } + } +} diff --git a/test/shared/RoutingTestHelpers.sol b/test/shared/RoutingTestHelpers.sol new file mode 100644 index 00000000..d09b77ec --- /dev/null +++ b/test/shared/RoutingTestHelpers.sol @@ -0,0 +1,174 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; +import {PoolModifyLiquidityTest} from "@uniswap/v4-core/src/test/PoolModifyLiquidityTest.sol"; +import {MockV4Router} from "../mocks/MockV4Router.sol"; +import {Plan, Planner} from "../shared/Planner.sol"; +import {MockERC20} from "solmate/src/test/utils/mocks/MockERC20.sol"; +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; +import {PathKey} from "../../src/libraries/PathKey.sol"; +import {Actions} from "../../src/libraries/Actions.sol"; + +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; +import {PositionManager} from "../../src/PositionManager.sol"; +import {IERC20} from "forge-std/interfaces/IERC20.sol"; +import {LiquidityOperations} from "./LiquidityOperations.sol"; +import {IV4Router} from "../../src/interfaces/IV4Router.sol"; + +/// @notice A shared test contract that wraps the v4-core deployers contract and exposes basic helpers for swapping with the router. +contract RoutingTestHelpers is Test, Deployers { + using Planner for Plan; + + PoolModifyLiquidityTest positionManager; + MockV4Router router; + + // nativeKey is already defined in Deployers.sol + PoolKey key0; + PoolKey key1; + PoolKey key2; + + // currency0 and currency1 are defined in Deployers.sol + Currency currency2; + Currency currency3; + + Currency[] tokenPath; + Plan plan; + + function setupRouterCurrenciesAndPoolsWithLiquidity() public { + deployFreshManager(); + + router = new MockV4Router(manager); + positionManager = new PoolModifyLiquidityTest(manager); + + MockERC20[] memory tokens = deployTokensMintAndApprove(4); + + currency0 = Currency.wrap(address(tokens[0])); + currency1 = Currency.wrap(address(tokens[1])); + currency2 = Currency.wrap(address(tokens[2])); + currency3 = Currency.wrap(address(tokens[3])); + + nativeKey = createNativePoolWithLiquidity(currency0, address(0)); + key0 = createPoolWithLiquidity(currency0, currency1, address(0)); + key1 = createPoolWithLiquidity(currency1, currency2, address(0)); + key2 = createPoolWithLiquidity(currency2, currency3, address(0)); + } + + function deployTokensMintAndApprove(uint8 count) internal returns (MockERC20[] memory) { + MockERC20[] memory tokens = deployTokens(count, 2 ** 128); + for (uint256 i = 0; i < count; i++) { + tokens[i].approve(address(router), type(uint256).max); + } + return tokens; + } + + function createPoolWithLiquidity(Currency currencyA, Currency currencyB, address hookAddr) + internal + returns (PoolKey memory _key) + { + 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); + 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"); + } + + function createNativePoolWithLiquidity(Currency currency, address hookAddr) + internal + returns (PoolKey memory _key) + { + _key = PoolKey(CurrencyLibrary.NATIVE, currency, 3000, 60, IHooks(hookAddr)); + + manager.initialize(_key, SQRT_PRICE_1_1, ZERO_BYTES); + MockERC20(Currency.unwrap(currency)).approve(address(positionManager), type(uint256).max); + positionManager.modifyLiquidity{value: 200 ether}( + _key, IPoolManager.ModifyLiquidityParams(-887220, 887220, 200 ether, 0), "0x" + ); + } + + function _getExactInputParams(Currency[] memory _tokenPath, uint256 amountIn) + internal + pure + returns (IV4Router.ExactInputParams memory params) + { + PathKey[] memory path = new PathKey[](_tokenPath.length - 1); + for (uint256 i = 0; i < _tokenPath.length - 1; i++) { + path[i] = PathKey(_tokenPath[i + 1], 3000, 60, IHooks(address(0)), bytes("")); + } + + params.currencyIn = _tokenPath[0]; + params.path = path; + params.amountIn = uint128(amountIn); + params.amountOutMinimum = 0; + } + + function _getExactOutputParams(Currency[] memory _tokenPath, uint256 amountOut) + internal + pure + returns (IV4Router.ExactOutputParams memory params) + { + PathKey[] memory path = new PathKey[](_tokenPath.length - 1); + for (uint256 i = _tokenPath.length - 1; i > 0; i--) { + path[i - 1] = PathKey(_tokenPath[i - 1], 3000, 60, IHooks(address(0)), bytes("")); + } + + params.currencyOut = _tokenPath[_tokenPath.length - 1]; + params.path = path; + params.amountOut = uint128(amountOut); + params.amountInMaximum = type(uint128).max; + } + + function _finalizeAndExecuteSwap(Currency inputCurrency, Currency outputCurrency, uint256 amountIn) + internal + returns ( + uint256 inputBalanceBefore, + uint256 outputBalanceBefore, + uint256 inputBalanceAfter, + uint256 outputBalanceAfter + ) + { + inputBalanceBefore = inputCurrency.balanceOfSelf(); + outputBalanceBefore = outputCurrency.balanceOfSelf(); + + bytes memory data = plan.finalizeSwap(inputCurrency, outputCurrency, address(this)); + + uint256 value = (inputCurrency.isNative()) ? amountIn : 0; + + // otherwise just execute as normal + router.executeActions{value: value}(data); + + inputBalanceAfter = inputCurrency.balanceOfSelf(); + outputBalanceAfter = outputCurrency.balanceOfSelf(); + } + + function _finalizeAndExecuteNativeInputExactOutputSwap( + Currency inputCurrency, + Currency outputCurrency, + uint256 expectedAmountIn + ) + internal + returns ( + uint256 inputBalanceBefore, + uint256 outputBalanceBefore, + uint256 inputBalanceAfter, + uint256 outputBalanceAfter + ) + { + inputBalanceBefore = inputCurrency.balanceOfSelf(); + outputBalanceBefore = outputCurrency.balanceOfSelf(); + + bytes memory data = plan.finalizeSwap(inputCurrency, outputCurrency, address(this)); + + // send too much ETH to mimic slippage + uint256 value = expectedAmountIn + 0.1 ether; + router.executeActionsAndSweepExcessETH{value: value}(data); + + inputBalanceAfter = inputCurrency.balanceOfSelf(); + outputBalanceAfter = outputCurrency.balanceOfSelf(); + } +} diff --git a/test/shared/fuzz/LiquidityFuzzers.sol b/test/shared/fuzz/LiquidityFuzzers.sol new file mode 100644 index 00000000..5bbfbc73 --- /dev/null +++ b/test/shared/fuzz/LiquidityFuzzers.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; +import {Fuzzers} from "@uniswap/v4-core/src/test/Fuzzers.sol"; + +import {IPositionManager} from "../../../src/interfaces/IPositionManager.sol"; +import {Actions} from "../../../src/libraries/Actions.sol"; +import {PositionConfig} from "../../../src/libraries/PositionConfig.sol"; +import {Planner, Plan} from "../../shared/Planner.sol"; + +contract LiquidityFuzzers is Fuzzers { + using Planner for Plan; + + function addFuzzyLiquidity( + IPositionManager lpm, + address recipient, + PoolKey memory key, + IPoolManager.ModifyLiquidityParams memory params, + uint160 sqrtPriceX96, + bytes memory hookData + ) internal returns (uint256, IPoolManager.ModifyLiquidityParams memory) { + params = Fuzzers.createFuzzyLiquidityParams(key, params, sqrtPriceX96); + PositionConfig memory config = + PositionConfig({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); + + uint128 MAX_SLIPPAGE_INCREASE = type(uint128).max; + Plan memory planner = Planner.init().add( + Actions.MINT_POSITION, + abi.encode( + config, + uint256(params.liquidityDelta), + MAX_SLIPPAGE_INCREASE, + MAX_SLIPPAGE_INCREASE, + recipient, + hookData + ) + ); + + uint256 tokenId = lpm.nextTokenId(); + bytes memory calls = planner.finalizeModifyLiquidity(config.poolKey); + lpm.modifyLiquidities(calls, block.timestamp + 1); + + return (tokenId, params); + } +} diff --git a/test/shared/implementation/FullRangeImplementation.sol b/test/shared/implementation/FullRangeImplementation.sol deleted file mode 100644 index 2d4ce3cc..00000000 --- a/test/shared/implementation/FullRangeImplementation.sol +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; - -import {BaseHook} from "../../../contracts/BaseHook.sol"; -import {FullRange} from "../../../contracts/hooks/examples/FullRange.sol"; -import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; - -contract FullRangeImplementation is FullRange { - constructor(IPoolManager _poolManager, FullRange addressToEtch) FullRange(_poolManager) { - Hooks.validateHookPermissions(addressToEtch, getHookPermissions()); - } - - // make this a no-op in testing - function validateHookAddress(BaseHook _this) internal pure override {} -} diff --git a/test/shared/implementation/GeomeanOracleImplementation.sol b/test/shared/implementation/GeomeanOracleImplementation.sol deleted file mode 100644 index b953a3b6..00000000 --- a/test/shared/implementation/GeomeanOracleImplementation.sol +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; - -import {BaseHook} from "../../../contracts/BaseHook.sol"; -import {GeomeanOracle} from "../../../contracts/hooks/examples/GeomeanOracle.sol"; -import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; - -contract GeomeanOracleImplementation is GeomeanOracle { - uint32 public time; - - constructor(IPoolManager _poolManager, GeomeanOracle addressToEtch) GeomeanOracle(_poolManager) { - Hooks.validateHookPermissions(addressToEtch, getHookPermissions()); - } - - // make this a no-op in testing - function validateHookAddress(BaseHook _this) internal pure override {} - - function setTime(uint32 _time) external { - time = _time; - } - - function _blockTimestamp() internal view override returns (uint32) { - return time; - } -} diff --git a/test/shared/implementation/LimitOrderImplementation.sol b/test/shared/implementation/LimitOrderImplementation.sol deleted file mode 100644 index 11625771..00000000 --- a/test/shared/implementation/LimitOrderImplementation.sol +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; - -import {BaseHook} from "../../../contracts/BaseHook.sol"; -import {LimitOrder} from "../../../contracts/hooks/examples/LimitOrder.sol"; -import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; - -contract LimitOrderImplementation is LimitOrder { - constructor(IPoolManager _poolManager, LimitOrder addressToEtch) LimitOrder(_poolManager) { - Hooks.validateHookPermissions(addressToEtch, getHookPermissions()); - } - - // make this a no-op in testing - function validateHookAddress(BaseHook _this) internal pure override {} -} diff --git a/test/shared/implementation/OracleImplementation.sol b/test/shared/implementation/OracleImplementation.sol deleted file mode 100644 index 7eefe3d3..00000000 --- a/test/shared/implementation/OracleImplementation.sol +++ /dev/null @@ -1,95 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; - -import {Oracle} from "../../../contracts/libraries/Oracle.sol"; - -contract OracleImplementation { - using Oracle for Oracle.Observation[65535]; - - Oracle.Observation[65535] public observations; - - uint32 public time; - int24 public tick; - uint128 public liquidity; - uint16 public index; - uint16 public cardinality; - uint16 public cardinalityNext; - - struct InitializeParams { - uint32 time; - int24 tick; - uint128 liquidity; - } - - function initialize(InitializeParams calldata params) external { - require(cardinality == 0, "already initialized"); - time = params.time; - tick = params.tick; - liquidity = params.liquidity; - (cardinality, cardinalityNext) = observations.initialize(params.time); - } - - function advanceTime(uint32 by) public { - unchecked { - time += by; - } - } - - struct UpdateParams { - uint32 advanceTimeBy; - int24 tick; - uint128 liquidity; - } - - // write an observation, then change tick and liquidity - function update(UpdateParams calldata params) external { - advanceTime(params.advanceTimeBy); - (index, cardinality) = observations.write(index, time, tick, liquidity, cardinality, cardinalityNext); - tick = params.tick; - liquidity = params.liquidity; - } - - function batchUpdate(UpdateParams[] calldata params) external { - // sload everything - int24 _tick = tick; - uint128 _liquidity = liquidity; - uint16 _index = index; - uint16 _cardinality = cardinality; - uint16 _cardinalityNext = cardinalityNext; - uint32 _time = time; - - for (uint256 i = 0; i < params.length; i++) { - _time += params[i].advanceTimeBy; - (_index, _cardinality) = - observations.write(_index, _time, _tick, _liquidity, _cardinality, _cardinalityNext); - _tick = params[i].tick; - _liquidity = params[i].liquidity; - } - - // sstore everything - tick = _tick; - liquidity = _liquidity; - index = _index; - cardinality = _cardinality; - time = _time; - } - - function grow(uint16 _cardinalityNext) external { - cardinalityNext = observations.grow(cardinalityNext, _cardinalityNext); - } - - function observe(uint32[] calldata secondsAgos) - external - view - returns (int56[] memory tickCumulatives, uint160[] memory secondsPerLiquidityCumulativeX128s) - { - return observations.observe(time, secondsAgos, tick, index, liquidity, cardinality); - } - - function getGasCostOfObserve(uint32[] calldata secondsAgos) external view returns (uint256) { - (uint32 _time, int24 _tick, uint128 _liquidity, uint16 _index) = (time, tick, liquidity, index); - uint256 gasBefore = gasleft(); - observations.observe(_time, secondsAgos, _tick, _index, _liquidity, cardinality); - return gasBefore - gasleft(); - } -} diff --git a/test/shared/implementation/TWAMMImplementation.sol b/test/shared/implementation/TWAMMImplementation.sol deleted file mode 100644 index f217db8c..00000000 --- a/test/shared/implementation/TWAMMImplementation.sol +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; - -import {BaseHook} from "../../../contracts/BaseHook.sol"; -import {TWAMM} from "../../../contracts/hooks/examples/TWAMM.sol"; -import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; - -contract TWAMMImplementation is TWAMM { - constructor(IPoolManager poolManager, uint256 interval, TWAMM addressToEtch) TWAMM(poolManager, interval) { - Hooks.validateHookPermissions(addressToEtch, getHookPermissions()); - } - - // make this a no-op in testing - function validateHookAddress(BaseHook _this) internal pure override {} -} diff --git a/test/utils/HookMiner.sol b/test/utils/HookMiner.sol index 9e556e46..a4e9edab 100644 --- a/test/utils/HookMiner.sol +++ b/test/utils/HookMiner.sol @@ -8,7 +8,7 @@ library HookMiner { uint160 constant FLAG_MASK = 0x3FFF; // Maximum number of iterations to find a salt, avoid infinite loops - uint256 constant MAX_LOOP = 100_000; + uint256 constant MAX_LOOP = 1_000_000; /// @notice Find a salt that produces a hook address with the desired `flags` /// @param deployer The address that will deploy the hook. In `forge test`, this will be the test contract `address(this)` or the pranking address diff --git a/test/utils/MiddlewareMiner.sol b/test/utils/MiddlewareMiner.sol new file mode 100644 index 00000000..36d24538 --- /dev/null +++ b/test/utils/MiddlewareMiner.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.24; + +import {MiddlewareRemove} from "../../src/middleware/MiddlewareRemove.sol"; +import {MiddlewareRemoveNoDeltas} from "../../src/middleware/MiddlewareRemoveNoDeltas.sol"; + +/// @title MiddlewareMiner - a library for mining middleware addresses +/// @dev This library is intended for `forge test` environments. There may be gotchas when using salts in `forge script` or `forge create` +library MiddlewareMiner { + // mask to slice out the bottom 14 bit of the address + uint160 constant FLAG_MASK = 0x3FFF; + + /// @notice Find a salt that produces a hook address with the desired `flags` + /// @param factory The factory address that will deploy the hook. + /// @param flags The desired flags for the hook address + /// @param manager The pool manager + /// @param implementation The implementation address + /// @param maxFeeBips The max fee in bips + /// @return hookAddress salt and corresponding address that was found. The salt can be used in `new Hook{salt: salt}()` + function find(address factory, uint160 flags, address manager, address implementation, uint256 maxFeeBips) + internal + view + returns (address, bytes32) + { + bytes memory creationCodeWithArgs; + if (maxFeeBips == 0) { + creationCodeWithArgs = + abi.encodePacked(type(MiddlewareRemoveNoDeltas).creationCode, abi.encode(manager, implementation)); + } else { + creationCodeWithArgs = + abi.encodePacked(type(MiddlewareRemove).creationCode, abi.encode(manager, implementation, maxFeeBips)); + } + address hookAddress; + + // prevents coliisions during testing + uint256 salt = uint256(keccak256(abi.encode(implementation, maxFeeBips))); + while (true) { + hookAddress = computeAddress(factory, salt, creationCodeWithArgs); + if (uint160(hookAddress) & FLAG_MASK == flags && hookAddress.code.length == 0) { + return (hookAddress, bytes32(salt)); + } + unchecked { + ++salt; + } + } + revert("HookMiner: could not find salt"); + } + + /// @notice Precompute a contract address deployed via CREATE2 + /// @param factory The address that will deploy the hook. In `forge test`, this will be the test contract `address(this)` or the pranking address + /// In `forge script`, this should be `0x4e59b44847b379578588920cA78FbF26c0B4956C` (CREATE2 factory Proxy) + /// @param salt The salt used to deploy the hook + /// @param creationCode The creation code of a hook contract + function computeAddress(address factory, uint256 salt, bytes memory creationCode) + internal + pure + returns (address hookAddress) + { + return + address(uint160(uint256(keccak256(abi.encodePacked(bytes1(0xFF), factory, salt, keccak256(creationCode)))))); + } +}