diff --git a/.forge-snapshots/PositionManager_burn_empty.snap b/.forge-snapshots/PositionManager_burn_empty.snap index f3d6d7c0..637bc1f0 100644 --- a/.forge-snapshots/PositionManager_burn_empty.snap +++ b/.forge-snapshots/PositionManager_burn_empty.snap @@ -1 +1 @@ -47307 \ No newline at end of file +46864 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_burn_empty_native.snap b/.forge-snapshots/PositionManager_burn_empty_native.snap index cbff577e..122463b6 100644 --- a/.forge-snapshots/PositionManager_burn_empty_native.snap +++ b/.forge-snapshots/PositionManager_burn_empty_native.snap @@ -1 +1 @@ -47124 \ No newline at end of file +46681 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_burn_nonEmpty.snap b/.forge-snapshots/PositionManager_burn_nonEmpty.snap index 9bb02f5c..78556cf9 100644 --- a/.forge-snapshots/PositionManager_burn_nonEmpty.snap +++ b/.forge-snapshots/PositionManager_burn_nonEmpty.snap @@ -1 +1 @@ -131968 \ No newline at end of file +132224 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_burn_nonEmpty_native.snap b/.forge-snapshots/PositionManager_burn_nonEmpty_native.snap index a580c09a..6329ffb5 100644 --- a/.forge-snapshots/PositionManager_burn_nonEmpty_native.snap +++ b/.forge-snapshots/PositionManager_burn_nonEmpty_native.snap @@ -1 +1 @@ -124890 \ No newline at end of file +125145 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_collect.snap b/.forge-snapshots/PositionManager_collect.snap index 81f12dc6..0bcdd3dc 100644 --- a/.forge-snapshots/PositionManager_collect.snap +++ b/.forge-snapshots/PositionManager_collect.snap @@ -1 +1 @@ -153510 \ No newline at end of file +152889 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_collect_native.snap b/.forge-snapshots/PositionManager_collect_native.snap index 1e5a5c2f..3fca37bf 100644 --- a/.forge-snapshots/PositionManager_collect_native.snap +++ b/.forge-snapshots/PositionManager_collect_native.snap @@ -1 +1 @@ -144662 \ No newline at end of file +144041 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_collect_sameRange.snap b/.forge-snapshots/PositionManager_collect_sameRange.snap index 81f12dc6..0bcdd3dc 100644 --- a/.forge-snapshots/PositionManager_collect_sameRange.snap +++ b/.forge-snapshots/PositionManager_collect_sameRange.snap @@ -1 +1 @@ -153510 \ No newline at end of file +152889 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_decreaseLiquidity.snap b/.forge-snapshots/PositionManager_decreaseLiquidity.snap index 2929882e..07620f5d 100644 --- a/.forge-snapshots/PositionManager_decreaseLiquidity.snap +++ b/.forge-snapshots/PositionManager_decreaseLiquidity.snap @@ -1 +1 @@ -119053 \ No newline at end of file +118432 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_decreaseLiquidity_native.snap b/.forge-snapshots/PositionManager_decreaseLiquidity_native.snap index 57fcdd61..bf0f5aae 100644 --- a/.forge-snapshots/PositionManager_decreaseLiquidity_native.snap +++ b/.forge-snapshots/PositionManager_decreaseLiquidity_native.snap @@ -1 +1 @@ -111204 \ No newline at end of file +110708 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_decrease_burnEmpty.snap b/.forge-snapshots/PositionManager_decrease_burnEmpty.snap index cdc7728f..7feacdc1 100644 --- a/.forge-snapshots/PositionManager_decrease_burnEmpty.snap +++ b/.forge-snapshots/PositionManager_decrease_burnEmpty.snap @@ -1 +1 @@ -137008 \ No newline at end of file +136102 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_decrease_burnEmpty_native.snap b/.forge-snapshots/PositionManager_decrease_burnEmpty_native.snap index 63895f7c..ad7d951c 100644 --- a/.forge-snapshots/PositionManager_decrease_burnEmpty_native.snap +++ b/.forge-snapshots/PositionManager_decrease_burnEmpty_native.snap @@ -1 +1 @@ -129747 \ No newline at end of file +128841 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_decrease_sameRange_allLiquidity.snap b/.forge-snapshots/PositionManager_decrease_sameRange_allLiquidity.snap index 73bf6d2e..6772976d 100644 --- a/.forge-snapshots/PositionManager_decrease_sameRange_allLiquidity.snap +++ b/.forge-snapshots/PositionManager_decrease_sameRange_allLiquidity.snap @@ -1 +1 @@ -131769 \ No newline at end of file +131148 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_increaseLiquidity_erc20.snap b/.forge-snapshots/PositionManager_increaseLiquidity_erc20.snap index 8ff40a42..0647cc74 100644 --- a/.forge-snapshots/PositionManager_increaseLiquidity_erc20.snap +++ b/.forge-snapshots/PositionManager_increaseLiquidity_erc20.snap @@ -1 +1 @@ -155013 \ No newline at end of file +154374 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_increaseLiquidity_native.snap b/.forge-snapshots/PositionManager_increaseLiquidity_native.snap index aae3436f..229e8148 100644 --- a/.forge-snapshots/PositionManager_increaseLiquidity_native.snap +++ b/.forge-snapshots/PositionManager_increaseLiquidity_native.snap @@ -1 +1 @@ -136896 \ No newline at end of file +136174 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_increase_autocompoundExactUnclaimedFees.snap b/.forge-snapshots/PositionManager_increase_autocompoundExactUnclaimedFees.snap index f2aab688..d6b384f9 100644 --- a/.forge-snapshots/PositionManager_increase_autocompoundExactUnclaimedFees.snap +++ b/.forge-snapshots/PositionManager_increase_autocompoundExactUnclaimedFees.snap @@ -1 +1 @@ -137719 \ No newline at end of file +137095 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_increase_autocompoundExcessFeesCredit.snap b/.forge-snapshots/PositionManager_increase_autocompoundExcessFeesCredit.snap index 4766ba9f..860f2794 100644 --- a/.forge-snapshots/PositionManager_increase_autocompoundExcessFeesCredit.snap +++ b/.forge-snapshots/PositionManager_increase_autocompoundExcessFeesCredit.snap @@ -1 +1 @@ -173875 \ No newline at end of file +173251 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint.snap b/.forge-snapshots/PositionManager_mint.snap index 519f622c..2df70fcd 100644 --- a/.forge-snapshots/PositionManager_mint.snap +++ b/.forge-snapshots/PositionManager_mint.snap @@ -1 +1 @@ -375114 \ No newline at end of file +374409 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_native.snap b/.forge-snapshots/PositionManager_mint_native.snap index 4ac64e89..4322f052 100644 --- a/.forge-snapshots/PositionManager_mint_native.snap +++ b/.forge-snapshots/PositionManager_mint_native.snap @@ -1 +1 @@ -339897 \ No newline at end of file +339109 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_nativeWithSweep.snap b/.forge-snapshots/PositionManager_mint_nativeWithSweep.snap index fb9011ee..c70b99e3 100644 --- a/.forge-snapshots/PositionManager_mint_nativeWithSweep.snap +++ b/.forge-snapshots/PositionManager_mint_nativeWithSweep.snap @@ -1 +1 @@ -346835 \ No newline at end of file +347739 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_onSameTickLower.snap b/.forge-snapshots/PositionManager_mint_onSameTickLower.snap index 87502097..839fcaa4 100644 --- a/.forge-snapshots/PositionManager_mint_onSameTickLower.snap +++ b/.forge-snapshots/PositionManager_mint_onSameTickLower.snap @@ -1 +1 @@ -317796 \ No newline at end of file +317091 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_onSameTickUpper.snap b/.forge-snapshots/PositionManager_mint_onSameTickUpper.snap index 6e353286..2a0691d0 100644 --- a/.forge-snapshots/PositionManager_mint_onSameTickUpper.snap +++ b/.forge-snapshots/PositionManager_mint_onSameTickUpper.snap @@ -1 +1 @@ -318438 \ No newline at end of file +317733 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_sameRange.snap b/.forge-snapshots/PositionManager_mint_sameRange.snap index a08cc1c6..bb99e63f 100644 --- a/.forge-snapshots/PositionManager_mint_sameRange.snap +++ b/.forge-snapshots/PositionManager_mint_sameRange.snap @@ -1 +1 @@ -244020 \ No newline at end of file +243315 \ 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..d6415c90 --- /dev/null +++ b/.forge-snapshots/PositionManager_mint_settleWithBalance_sweep.snap @@ -0,0 +1 @@ +372583 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_warmedPool_differentRange.snap b/.forge-snapshots/PositionManager_mint_warmedPool_differentRange.snap index 6ea59de9..0893f494 100644 --- a/.forge-snapshots/PositionManager_mint_warmedPool_differentRange.snap +++ b/.forge-snapshots/PositionManager_mint_warmedPool_differentRange.snap @@ -1 +1 @@ -323814 \ No newline at end of file +323109 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_multicall_initialize_mint.snap b/.forge-snapshots/PositionManager_multicall_initialize_mint.snap index 57ab2638..f82d06fe 100644 --- a/.forge-snapshots/PositionManager_multicall_initialize_mint.snap +++ b/.forge-snapshots/PositionManager_multicall_initialize_mint.snap @@ -1 +1 @@ -419520 \ No newline at end of file +418855 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_Bytecode.snap b/.forge-snapshots/V4Router_Bytecode.snap index e11616e3..42bd3cdc 100644 --- a/.forge-snapshots/V4Router_Bytecode.snap +++ b/.forge-snapshots/V4Router_Bytecode.snap @@ -1 +1 @@ -6595 \ No newline at end of file +6628 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactIn1Hop_nativeIn.snap b/.forge-snapshots/V4Router_ExactIn1Hop_nativeIn.snap index f6f8b33c..24c9822b 100644 --- a/.forge-snapshots/V4Router_ExactIn1Hop_nativeIn.snap +++ b/.forge-snapshots/V4Router_ExactIn1Hop_nativeIn.snap @@ -1 +1 @@ -120991 \ No newline at end of file +121090 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactIn1Hop_nativeOut.snap b/.forge-snapshots/V4Router_ExactIn1Hop_nativeOut.snap index 0a624153..24ccfd9d 100644 --- a/.forge-snapshots/V4Router_ExactIn1Hop_nativeOut.snap +++ b/.forge-snapshots/V4Router_ExactIn1Hop_nativeOut.snap @@ -1 +1 @@ -120158 \ No newline at end of file +120257 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactIn1Hop_oneForZero.snap b/.forge-snapshots/V4Router_ExactIn1Hop_oneForZero.snap index 6f2f9ce5..f891c6dd 100644 --- a/.forge-snapshots/V4Router_ExactIn1Hop_oneForZero.snap +++ b/.forge-snapshots/V4Router_ExactIn1Hop_oneForZero.snap @@ -1 +1 @@ -129030 \ No newline at end of file +129129 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactIn1Hop_zeroForOne.snap b/.forge-snapshots/V4Router_ExactIn1Hop_zeroForOne.snap index 612b68dd..f6834076 100644 --- a/.forge-snapshots/V4Router_ExactIn1Hop_zeroForOne.snap +++ b/.forge-snapshots/V4Router_ExactIn1Hop_zeroForOne.snap @@ -1 +1 @@ -135860 \ No newline at end of file +135959 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactIn2Hops.snap b/.forge-snapshots/V4Router_ExactIn2Hops.snap index 8eb82f04..e70fa136 100644 --- a/.forge-snapshots/V4Router_ExactIn2Hops.snap +++ b/.forge-snapshots/V4Router_ExactIn2Hops.snap @@ -1 +1 @@ -187212 \ No newline at end of file +187311 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactIn2Hops_nativeIn.snap b/.forge-snapshots/V4Router_ExactIn2Hops_nativeIn.snap index c14560ee..69edacf1 100644 --- a/.forge-snapshots/V4Router_ExactIn2Hops_nativeIn.snap +++ b/.forge-snapshots/V4Router_ExactIn2Hops_nativeIn.snap @@ -1 +1 @@ -179175 \ No newline at end of file +179274 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactIn3Hops.snap b/.forge-snapshots/V4Router_ExactIn3Hops.snap index 0409f93c..6de237e2 100644 --- a/.forge-snapshots/V4Router_ExactIn3Hops.snap +++ b/.forge-snapshots/V4Router_ExactIn3Hops.snap @@ -1 +1 @@ -238593 \ No newline at end of file +238692 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactIn3Hops_nativeIn.snap b/.forge-snapshots/V4Router_ExactIn3Hops_nativeIn.snap index 913a30c6..33ade7d1 100644 --- a/.forge-snapshots/V4Router_ExactIn3Hops_nativeIn.snap +++ b/.forge-snapshots/V4Router_ExactIn3Hops_nativeIn.snap @@ -1 +1 @@ -230580 \ No newline at end of file +230679 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactInputSingle.snap b/.forge-snapshots/V4Router_ExactInputSingle.snap index af44576c..2e3423fd 100644 --- a/.forge-snapshots/V4Router_ExactInputSingle.snap +++ b/.forge-snapshots/V4Router_ExactInputSingle.snap @@ -1 +1 @@ -134618 \ No newline at end of file +134717 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactInputSingle_nativeIn.snap b/.forge-snapshots/V4Router_ExactInputSingle_nativeIn.snap index c73f471b..b7011dc2 100644 --- a/.forge-snapshots/V4Router_ExactInputSingle_nativeIn.snap +++ b/.forge-snapshots/V4Router_ExactInputSingle_nativeIn.snap @@ -1 +1 @@ -119749 \ No newline at end of file +119848 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactInputSingle_nativeOut.snap b/.forge-snapshots/V4Router_ExactInputSingle_nativeOut.snap index c2705e54..cdff580e 100644 --- a/.forge-snapshots/V4Router_ExactInputSingle_nativeOut.snap +++ b/.forge-snapshots/V4Router_ExactInputSingle_nativeOut.snap @@ -1 +1 @@ -118894 \ No newline at end of file +118993 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactOut1Hop_nativeIn_sweepETH.snap b/.forge-snapshots/V4Router_ExactOut1Hop_nativeIn_sweepETH.snap index 05f02403..77e59bce 100644 --- a/.forge-snapshots/V4Router_ExactOut1Hop_nativeIn_sweepETH.snap +++ b/.forge-snapshots/V4Router_ExactOut1Hop_nativeIn_sweepETH.snap @@ -1 +1 @@ -126794 \ No newline at end of file +126893 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactOut1Hop_nativeOut.snap b/.forge-snapshots/V4Router_ExactOut1Hop_nativeOut.snap index cd9382b3..f276517c 100644 --- a/.forge-snapshots/V4Router_ExactOut1Hop_nativeOut.snap +++ b/.forge-snapshots/V4Router_ExactOut1Hop_nativeOut.snap @@ -1 +1 @@ -120999 \ No newline at end of file +121098 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactOut1Hop_oneForZero.snap b/.forge-snapshots/V4Router_ExactOut1Hop_oneForZero.snap index 1d29c498..efacec37 100644 --- a/.forge-snapshots/V4Router_ExactOut1Hop_oneForZero.snap +++ b/.forge-snapshots/V4Router_ExactOut1Hop_oneForZero.snap @@ -1 +1 @@ -129871 \ No newline at end of file +129970 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactOut1Hop_zeroForOne.snap b/.forge-snapshots/V4Router_ExactOut1Hop_zeroForOne.snap index d1018c3e..bd7e8a65 100644 --- a/.forge-snapshots/V4Router_ExactOut1Hop_zeroForOne.snap +++ b/.forge-snapshots/V4Router_ExactOut1Hop_zeroForOne.snap @@ -1 +1 @@ -134672 \ No newline at end of file +134771 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactOut2Hops.snap b/.forge-snapshots/V4Router_ExactOut2Hops.snap index 3ecc5f00..be849ee5 100644 --- a/.forge-snapshots/V4Router_ExactOut2Hops.snap +++ b/.forge-snapshots/V4Router_ExactOut2Hops.snap @@ -1 +1 @@ -186633 \ No newline at end of file +186732 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactOut2Hops_nativeIn.snap b/.forge-snapshots/V4Router_ExactOut2Hops_nativeIn.snap index a76127c1..1ae42da1 100644 --- a/.forge-snapshots/V4Router_ExactOut2Hops_nativeIn.snap +++ b/.forge-snapshots/V4Router_ExactOut2Hops_nativeIn.snap @@ -1 +1 @@ -183556 \ No newline at end of file +183655 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactOut3Hops.snap b/.forge-snapshots/V4Router_ExactOut3Hops.snap index 45e158a2..9883a932 100644 --- a/.forge-snapshots/V4Router_ExactOut3Hops.snap +++ b/.forge-snapshots/V4Router_ExactOut3Hops.snap @@ -1 +1 @@ -238638 \ No newline at end of file +238737 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactOut3Hops_nativeIn.snap b/.forge-snapshots/V4Router_ExactOut3Hops_nativeIn.snap index 7cba59fe..f55c5a60 100644 --- a/.forge-snapshots/V4Router_ExactOut3Hops_nativeIn.snap +++ b/.forge-snapshots/V4Router_ExactOut3Hops_nativeIn.snap @@ -1 +1 @@ -235585 \ No newline at end of file +235684 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactOut3Hops_nativeOut.snap b/.forge-snapshots/V4Router_ExactOut3Hops_nativeOut.snap index 72b7d53b..4301df86 100644 --- a/.forge-snapshots/V4Router_ExactOut3Hops_nativeOut.snap +++ b/.forge-snapshots/V4Router_ExactOut3Hops_nativeOut.snap @@ -1 +1 @@ -229790 \ No newline at end of file +229889 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactOutputSingle.snap b/.forge-snapshots/V4Router_ExactOutputSingle.snap index a538db14..ef03db55 100644 --- a/.forge-snapshots/V4Router_ExactOutputSingle.snap +++ b/.forge-snapshots/V4Router_ExactOutputSingle.snap @@ -1 +1 @@ -133152 \ No newline at end of file +133251 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactOutputSingle_nativeIn_sweepETH.snap b/.forge-snapshots/V4Router_ExactOutputSingle_nativeIn_sweepETH.snap index baf2e400..d3168270 100644 --- a/.forge-snapshots/V4Router_ExactOutputSingle_nativeIn_sweepETH.snap +++ b/.forge-snapshots/V4Router_ExactOutputSingle_nativeIn_sweepETH.snap @@ -1 +1 @@ -125274 \ No newline at end of file +125373 \ No newline at end of file diff --git a/.forge-snapshots/V4Router_ExactOutputSingle_nativeOut.snap b/.forge-snapshots/V4Router_ExactOutputSingle_nativeOut.snap index 8cf4fc55..78729eea 100644 --- a/.forge-snapshots/V4Router_ExactOutputSingle_nativeOut.snap +++ b/.forge-snapshots/V4Router_ExactOutputSingle_nativeOut.snap @@ -1 +1 @@ -119537 \ No newline at end of file +119636 \ No newline at end of file diff --git a/src/PositionManager.sol b/src/PositionManager.sol index 9fcc1379..3b97a565 100644 --- a/src/PositionManager.sol +++ b/src/PositionManager.sol @@ -7,8 +7,10 @@ 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"; @@ -23,6 +25,7 @@ import {PositionConfig, PositionConfigLibrary} from "./libraries/PositionConfig. import {BaseActionsRouter} from "./base/BaseActionsRouter.sol"; import {Actions} from "./libraries/Actions.sol"; import {StakingNotifier, StakingConfig} from "./base/StakingNotifier.sol"; +import {CalldataDecoder} from "./libraries/CalldataDecoder.sol"; contract PositionManager is IPositionManager, @@ -41,6 +44,7 @@ contract PositionManager is using StateLibrary for IPoolManager; using TransientStateLibrary for IPoolManager; using SafeCast for uint256; + using CalldataDecoder for bytes; /// @dev The ID of the next token that will be minted. Skips 0 uint256 public nextTokenId = 1; @@ -76,16 +80,30 @@ contract PositionManager is function _handleAction(uint256 action, bytes calldata params) internal override { if (action == Actions.INCREASE_LIQUIDITY) { - _increase(params); + (uint256 tokenId, PositionConfig calldata config, uint256 liquidity, bytes calldata hookData) = + params.decodeModifyLiquidityParams(); + _increase(tokenId, config, liquidity, hookData); } else if (action == Actions.DECREASE_LIQUIDITY) { - _decrease(params); + (uint256 tokenId, PositionConfig calldata config, uint256 liquidity, bytes calldata hookData) = + params.decodeModifyLiquidityParams(); + _decrease(tokenId, config, liquidity, hookData); } else if (action == Actions.MINT_POSITION) { - _mint(params); + (PositionConfig calldata config, uint256 liquidity, address owner, bytes calldata hookData) = + params.decodeMintParams(); + _mint(config, liquidity, owner, hookData); } else if (action == Actions.CLOSE_CURRENCY) { - _close(params); + Currency currency = params.decodeCurrency(); + _close(currency); } else if (action == Actions.BURN_POSITION) { // Will automatically decrease liquidity to 0 if the position is not already empty. - _burn(params); + (uint256 tokenId, PositionConfig calldata config, bytes calldata hookData) = params.decodeBurnParams(); + _burn(tokenId, config, 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); } @@ -95,23 +113,19 @@ contract PositionManager is return _getLocker(); } - /// @param params is an encoding of uint256 tokenId, PositionConfig memory config, uint256 liquidity, bytes hookData /// @dev Calling increase with 0 liquidity will credit the caller with any underlying fees of the position - function _increase(bytes memory params) internal { - (uint256 tokenId, PositionConfig memory config, uint256 liquidity, bytes memory hookData) = - abi.decode(params, (uint256, PositionConfig, uint256, bytes)); - + function _increase(uint256 tokenId, PositionConfig calldata config, uint256 liquidity, 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); } - /// @param params is an encoding of uint256 tokenId, PositionConfig memory config, uint256 liquidity, bytes hookData /// @dev Calling decrease with 0 liquidity will credit the caller with any underlying fees of the position - function _decrease(bytes memory params) internal { - (uint256 tokenId, PositionConfig memory config, uint256 liquidity, bytes memory hookData) = - abi.decode(params, (uint256, PositionConfig, uint256, bytes)); - + function _decrease(uint256 tokenId, PositionConfig calldata config, uint256 liquidity, bytes calldata hookData) + internal + { if (!_isApprovedOrOwner(_msgSender(), tokenId)) revert NotApproved(_msgSender()); if (positionConfigs[tokenId] != config.toId()) revert IncorrectPositionConfigForTokenId(tokenId); @@ -119,11 +133,9 @@ contract PositionManager is BalanceDelta liquidityDelta = _modifyLiquidity(config, -(liquidity.toInt256()), bytes32(tokenId), hookData); } - /// @param params is an encoding of PositionConfig memory config, uint256 liquidity, address recipient, bytes hookData where recipient is the receiver / owner of the ERC721 - function _mint(bytes memory params) internal { - (PositionConfig memory config, uint256 liquidity, address owner, bytes memory hookData) = - abi.decode(params, (PositionConfig, uint256, address, bytes)); - + function _mint(PositionConfig calldata config, uint256 liquidity, address owner, bytes calldata hookData) + internal + { // mint receipt token uint256 tokenId; // tokenId is assigned to current nextTokenId before incrementing it @@ -138,9 +150,7 @@ contract PositionManager is positionConfigs[tokenId] = config.toId(); } - /// @param params is an encoding of the Currency to close - function _close(bytes memory params) internal { - (Currency currency) = abi.decode(params, (Currency)); + 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); @@ -149,20 +159,19 @@ contract PositionManager is address caller = _msgSender(); if (currencyDelta < 0) { _settle(currency, caller, uint256(-currencyDelta)); - - // if there are native tokens left over after settling, return to locker - if (currency.isNative()) _sweepNativeToken(caller); } else if (currencyDelta > 0) { _take(currency, caller, uint256(currencyDelta)); } } - /// @param params is an encoding of uint256 tokenId, PositionConfig memory config, bytes hookData - /// @dev this is overloaded with ERC721Permit._burn - function _burn(bytes memory params) internal { - (uint256 tokenId, PositionConfig memory config, bytes memory hookData) = - abi.decode(params, (uint256, PositionConfig, bytes)); + /// @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, 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)); @@ -178,10 +187,12 @@ contract PositionManager is _burn(tokenId); } - function _modifyLiquidity(PositionConfig memory config, int256 liquidityChange, bytes32 salt, bytes memory hookData) - internal - returns (BalanceDelta liquidityDelta) - { + function _modifyLiquidity( + PositionConfig calldata config, + int256 liquidityChange, + bytes32 salt, + bytes calldata hookData + ) internal returns (BalanceDelta liquidityDelta) { (liquidityDelta,) = poolManager.modifyLiquidity( config.poolKey, IPoolManager.ModifyLiquidityParams({ @@ -196,33 +207,35 @@ contract PositionManager is _notifyModifyLiquidity(uint256(salt), liquidityChange, config); } - function stake(uint256 tokenId, StakingConfig memory stakingConfig, PositionConfig memory config) external { + function stake(uint256 tokenId, StakingConfig calldata stakingConfig, PositionConfig calldata config) external { if (!_isApprovedOrOwner(_msgSender(), tokenId)) revert NotApproved(_msgSender()); if (positionConfigs[tokenId] != config.toId()) revert IncorrectPositionConfigForTokenId(tokenId); _notifyStake(tokenId, stakingConfig, config); } - function _getPositionLiquidity(PositionConfig memory config, uint256 tokenId) + function _getPositionLiquidity(PositionConfig calldata config, uint256 tokenId) internal view returns (uint128 liquidity) { - // TODO: Calculate positionId with Position.calculatePositionKey in v4-core. bytes32 positionId = - keccak256(abi.encodePacked(address(this), config.tickLower, config.tickUpper, bytes32(tokenId))); + Position.calculatePositionKey(address(this), config.tickLower, config.tickUpper, bytes32(tokenId)); liquidity = poolManager.getPositionLiquidity(config.poolKey.toId(), positionId); } - /// @dev Send excess native tokens back to the recipient (locker) - /// @param recipient the receiver of the excess native tokens. Should be the caller, the one that sent the native tokens - function _sweepNativeToken(address recipient) internal { - uint256 nativeBalance = address(this).balance; - if (nativeBalance > 0) recipient.safeTransferETH(nativeBalance); + /// @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 token, address payer, uint256 amount) internal override { - // TODO: Should we also support direct transfer? - permit2.transferFrom(payer, address(poolManager), uint160(amount), Currency.unwrap(token)); + 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 index 5e7e81ab..5f679193 100644 --- a/src/V4Router.sol +++ b/src/V4Router.sol @@ -6,7 +6,6 @@ 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 {TransientStateLibrary} from "@uniswap/v4-core/src/libraries/TransientStateLibrary.sol"; import {PathKey, PathKeyLib} from "./libraries/PathKey.sol"; import {CalldataDecoder} from "./libraries/CalldataDecoder.sol"; @@ -14,17 +13,16 @@ 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 {SafeCast} from "./libraries/SafeCast.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 SafeCast for *; + using SafeCastTemp for *; using PathKeyLib for PathKey; using CalldataDecoder for bytes; - using TransientStateLibrary for IPoolManager; constructor(IPoolManager _poolManager) BaseActionsRouter(_poolManager) {} @@ -45,33 +43,19 @@ abstract contract V4Router is IV4Router, BaseActionsRouter, DeltaResolver { } } else { if (action == Actions.SETTLE_ALL) { - // equivalent: abi.decode(params, (Currency)) - Currency currency; - assembly ("memory-safe") { - currency := calldataload(params.offset) - } - - int256 delta = poolManager.currencyDelta(address(this), currency); - if (delta > 0) revert InvalidDeltaForAction(); + Currency currency = params.decodeCurrency(); + uint256 amount = _getFullSettleAmount(currency); // TODO support address(this) paying too // TODO should it have a maxAmountOut added slippage protection? - _settle(currency, _msgSender(), uint256(-delta)); + _settle(currency, _msgSender(), amount); } else if (action == Actions.TAKE_ALL) { - // equivalent: abi.decode(params, (Currency, address)) - Currency currency; - address recipient; - assembly ("memory-safe") { - currency := calldataload(params.offset) - recipient := calldataload(add(params.offset, 0x20)) - } - - int256 delta = poolManager.currencyDelta(address(this), currency); - if (delta < 0) revert InvalidDeltaForAction(); + (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, uint256(delta)); + _take(currency, recipient, amount); } else { revert UnsupportedAction(action); } diff --git a/src/base/DeltaResolver.sol b/src/base/DeltaResolver.sol index e67721cc..9ddd81db 100644 --- a/src/base/DeltaResolver.sol +++ b/src/base/DeltaResolver.sol @@ -2,13 +2,19 @@ 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 { - /// @notice Emitted trying to settle a positive delta, or take a negative delta - error InvalidDeltaForAction(); + 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 @@ -39,4 +45,18 @@ abstract contract DeltaResolver is ImmutableState { /// @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/libraries/Actions.sol b/src/libraries/Actions.sol index 07613b9a..132fde68 100644 --- a/src/libraries/Actions.sol +++ b/src/libraries/Actions.sol @@ -5,31 +5,35 @@ pragma solidity ^0.8.24; /// @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 SWAP_EXACT_IN_SINGLE = 0x02; - uint256 constant SWAP_EXACT_IN = 0x03; - uint256 constant SWAP_EXACT_OUT_SINGLE = 0x04; - uint256 constant SWAP_EXACT_OUT = 0x05; - uint256 constant DONATE = 0x06; + 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 TAKE = 0x12; - uint256 constant TAKE_ALL = 0x13; - uint256 constant TAKE_PORTION = 0x14; - - uint256 constant CLOSE_CURRENCY = 0x15; - uint256 constant CLOSE_PAIR = 0x16; - uint256 constant CLEAR = 0x17; + 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; - - // mint + burn ERC721 position - uint256 constant MINT_POSITION = 0x22; - uint256 constant BURN_POSITION = 0x23; } diff --git a/src/libraries/CalldataDecoder.sol b/src/libraries/CalldataDecoder.sol index 813bc46a..a9dca15b 100644 --- a/src/libraries/CalldataDecoder.sol +++ b/src/libraries/CalldataDecoder.sol @@ -1,17 +1,19 @@ // 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"; + /// @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; - /// @notice Performs the equivalent of `abi.decode(data, (uint256[], bytes[]))` in calldata - /// @param _bytes The input bytes string to extract input arrays from - /// @return actions The uint256 calldata array of actions - /// @return params The bytes calldata array of parameters + /// @dev equivalent to: abi.decode(params, (uint256[], bytes[])) in calldata function decodeActionsRouterParams(bytes calldata _bytes) internal pure @@ -40,4 +42,99 @@ library CalldataDecoder { } } } + + /// @dev equivalent to: abi.decode(params, (uint256, PositionConfig, uint256, bytes)) in calldata + function decodeModifyLiquidityParams(bytes calldata params) + internal + pure + returns (uint256 tokenId, PositionConfig calldata config, uint256 liquidity, bytes calldata hookData) + { + assembly ("memory-safe") { + tokenId := calldataload(params.offset) + config := add(params.offset, 0x20) + liquidity := calldataload(add(params.offset, 0x100)) + } + hookData = params.toBytes(9); + } + + /// @dev equivalent to: abi.decode(params, (PositionConfig, uint256, address, bytes)) in calldata + function decodeMintParams(bytes calldata params) + internal + pure + returns (PositionConfig calldata config, uint256 liquidity, address owner, bytes calldata hookData) + { + assembly ("memory-safe") { + config := params.offset + liquidity := calldataload(add(params.offset, 0xe0)) + owner := calldataload(add(params.offset, 0x100)) + } + hookData = params.toBytes(9); + } + + /// @dev equivalent to: abi.decode(params, (uint256, PositionConfig, bytes)) in calldata + function decodeBurnParams(bytes calldata params) + internal + pure + returns (uint256 tokenId, PositionConfig calldata config, bytes calldata hookData) + { + assembly ("memory-safe") { + tokenId := calldataload(params.offset) + config := add(params.offset, 0x20) + } + hookData = params.toBytes(8); + } + + /// @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)) + } + } + + /// @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/PositionConfig.sol b/src/libraries/PositionConfig.sol index dcdb7c40..d3bd8e24 100644 --- a/src/libraries/PositionConfig.sol +++ b/src/libraries/PositionConfig.sol @@ -12,18 +12,24 @@ struct PositionConfig { /// @notice Library for computing the configId given a PositionConfig library PositionConfigLibrary { - function toId(PositionConfig memory config) internal pure returns (bytes32 id) { - PoolKey memory poolKey = config.poolKey; - return keccak256( - abi.encodePacked( - poolKey.currency0, - poolKey.currency1, - poolKey.fee, - poolKey.tickSpacing, - poolKey.hooks, - config.tickLower, - config.tickUpper - ) - ); + 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 index 75fddf53..ae16b2d1 100644 --- a/src/libraries/SafeCast.sol +++ b/src/libraries/SafeCast.sol @@ -5,7 +5,8 @@ import {CustomRevert} from "@uniswap/v4-core/src/libraries/CustomRevert.sol"; /// @title Safe casting methods /// @notice Contains methods for safely casting between types -library SafeCast { +/// TODO after audits move this function to core's SafeCast.sol! +library SafeCastTemp { using CustomRevert for bytes4; error SafeCastOverflow(); 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 index 8bf00a40..392d7048 100644 --- a/test/StateViewTest.t.sol +++ b/test/StateViewTest.t.sol @@ -432,7 +432,7 @@ contract StateViewTest is Test, Deployers, Fuzzers, GasSnapshot { modifyLiquidityRouter.modifyLiquidity(key, IPoolManager.ModifyLiquidityParams(-60, 60, 0, 0), ZERO_BYTES); bytes32 positionId = - keccak256(abi.encodePacked(address(modifyLiquidityRouter), int24(-60), int24(60), bytes32(0))); + Position.calculatePositionKey(address(modifyLiquidityRouter), int24(-60), int24(60), bytes32(0)); (, uint256 feeGrowthInside0X128_, uint256 feeGrowthInside1X128_) = state.getPositionInfo(poolId, positionId); @@ -480,7 +480,7 @@ contract StateViewTest is Test, Deployers, Fuzzers, GasSnapshot { ); bytes32 positionId = - keccak256(abi.encodePacked(address(modifyLiquidityRouter), int24(-60), int24(60), bytes32(0))); + Position.calculatePositionKey(address(modifyLiquidityRouter), int24(-60), int24(60), bytes32(0)); uint128 liquidity = state.getPositionLiquidity(poolId, positionId); snapLastCall("StateView_extsload_getPositionLiquidity"); diff --git a/test/libraries/CalldataDecoder.t.sol b/test/libraries/CalldataDecoder.t.sol new file mode 100644 index 00000000..1366bd3d --- /dev/null +++ b/test/libraries/CalldataDecoder.t.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; +import {MockCalldataDecoder} from "../mocks/MockCalldataDecoder.sol"; +import {PositionConfig} from "../../src/libraries/PositionConfig.sol"; +import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; + +contract CalldataDecoderTest is Test { + MockCalldataDecoder decoder; + + function setUp() public { + decoder = new MockCalldataDecoder(); + } + + function test_fuzz_decodeModifyLiquidityParams( + uint256 _tokenId, + PositionConfig calldata _config, + uint256 _liquidity, + bytes calldata _hookData + ) public view { + bytes memory params = abi.encode(_tokenId, _config, _liquidity, _hookData); + (uint256 tokenId, PositionConfig memory config, uint256 liquidity, bytes memory hookData) = + decoder.decodeModifyLiquidityParams(params); + + assertEq(tokenId, _tokenId); + assertEq(liquidity, _liquidity); + assertEq(hookData, _hookData); + _assertEq(_config, config); + } + + function test_fuzz_decodeBurnParams(uint256 _tokenId, PositionConfig calldata _config, bytes calldata _hookData) + public + view + { + bytes memory params = abi.encode(_tokenId, _config, _hookData); + (uint256 tokenId, PositionConfig memory config, bytes memory hookData) = decoder.decodeBurnParams(params); + + assertEq(tokenId, _tokenId); + assertEq(hookData, _hookData); + _assertEq(_config, config); + } + + function test_fuzz_decodeMintParams( + PositionConfig calldata _config, + uint256 _liquidity, + address _owner, + bytes calldata _hookData + ) public view { + bytes memory params = abi.encode(_config, _liquidity, _owner, _hookData); + (PositionConfig memory config, uint256 liquidity, address owner, bytes memory hookData) = + decoder.decodeMintParams(params); + + assertEq(liquidity, _liquidity); + assertEq(owner, _owner); + assertEq(hookData, _hookData); + _assertEq(_config, config); + } + + 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 _assertEq(PositionConfig memory config1, PositionConfig memory config2) internal pure { + assertEq(Currency.unwrap(config1.poolKey.currency0), Currency.unwrap(config2.poolKey.currency0)); + assertEq(Currency.unwrap(config1.poolKey.currency1), Currency.unwrap(config2.poolKey.currency1)); + assertEq(config1.poolKey.fee, config2.poolKey.fee); + assertEq(config1.poolKey.tickSpacing, config2.poolKey.tickSpacing); + assertEq(address(config1.poolKey.hooks), address(config2.poolKey.hooks)); + assertEq(config1.tickLower, config2.tickLower); + assertEq(config1.tickUpper, config2.tickUpper); + } +} 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/mocks/MockCalldataDecoder.sol b/test/mocks/MockCalldataDecoder.sol new file mode 100644 index 00000000..88332553 --- /dev/null +++ b/test/mocks/MockCalldataDecoder.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {PositionConfig} from "../../src/libraries/PositionConfig.sol"; +import {CalldataDecoder} from "../../src/libraries/CalldataDecoder.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, bytes calldata hookData) + { + return params.decodeModifyLiquidityParams(); + } + + function decodeBurnParams(bytes calldata params) + external + pure + returns (uint256 tokenId, PositionConfig calldata config, bytes calldata hookData) + { + return params.decodeBurnParams(); + } + + function decodeMintParams(bytes calldata params) + external + pure + returns (PositionConfig calldata config, uint256 liquidity, 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(); + } +} 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/position-managers/Execute.t.sol b/test/position-managers/Execute.t.sol index b745ec3a..4d7c5e15 100644 --- a/test/position-managers/Execute.t.sol +++ b/test/position-managers/Execute.t.sol @@ -11,6 +11,8 @@ 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"; @@ -41,7 +43,10 @@ contract ExecuteTest is Test, PosmTestSetup, LiquidityFuzzers { deployFreshManagerAndRouters(); deployMintAndApprove2Currencies(); - (key, poolId) = initPool(currency0, currency1, IHooks(address(0)), 3000, SQRT_PRICE_1_1, ZERO_BYTES); + // 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); @@ -61,8 +66,8 @@ contract ExecuteTest is Test, PosmTestSetup, LiquidityFuzzers { 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); - uint256 tokenId = lpm.nextTokenId() - 1; increaseLiquidity(tokenId, config, liquidityToAdd, ZERO_BYTES); @@ -81,8 +86,8 @@ contract ExecuteTest is Test, PosmTestSetup, LiquidityFuzzers { 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); - uint256 tokenId = lpm.nextTokenId() - 1; Plan memory planner = Planner.init(); @@ -122,7 +127,72 @@ contract ExecuteTest is Test, PosmTestSetup, LiquidityFuzzers { } // rebalance: burn and mint - function test_execute_rebalance() public {} + 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, ZERO_BYTES)); + planner.add(Actions.MINT_POSITION, abi.encode(newConfig, newLiquidity, 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 diff --git a/test/position-managers/FeeCollection.t.sol b/test/position-managers/FeeCollection.t.sol index fa32bf44..54ad44ad 100644 --- a/test/position-managers/FeeCollection.t.sol +++ b/test/position-managers/FeeCollection.t.sol @@ -55,6 +55,23 @@ contract FeeCollectionTest is Test, PosmTestSetup, LiquidityFuzzers { 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; @@ -77,6 +94,7 @@ contract FeeCollectionTest is Test, PosmTestSetup, LiquidityFuzzers { 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()))); @@ -96,13 +114,15 @@ contract FeeCollectionTest is Test, PosmTestSetup, LiquidityFuzzers { PositionConfig memory config = PositionConfig({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); - vm.prank(alice); + vm.startPrank(alice); + uint256 tokenIdAlice = lpm.nextTokenId(); mint(config, uint256(params.liquidityDelta), alice, ZERO_BYTES); - uint256 tokenIdAlice = lpm.nextTokenId() - 1; + vm.stopPrank(); - vm.prank(bob); + vm.startPrank(bob); + uint256 tokenIdBob = lpm.nextTokenId(); mint(config, liquidityDeltaBob, bob, ZERO_BYTES); - uint256 tokenIdBob = lpm.nextTokenId() - 1; + vm.stopPrank(); // confirm the positions are same range // (, int24 tickLowerAlice, int24 tickUpperAlice) = lpm.tokenRange(tokenIdAlice); @@ -147,6 +167,110 @@ contract FeeCollectionTest is Test, PosmTestSetup, LiquidityFuzzers { 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 { @@ -225,8 +349,6 @@ contract FeeCollectionTest is Test, PosmTestSetup, LiquidityFuzzers { } } - function test_collect_donate() public {} - function test_collect_donate_sameRange() public {} // 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 index 2263757e..ef76e3cc 100644 --- a/test/position-managers/IncreaseLiquidity.t.sol +++ b/test/position-managers/IncreaseLiquidity.t.sol @@ -13,8 +13,8 @@ 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 {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; import {Fuzzers} from "@uniswap/v4-core/src/test/Fuzzers.sol"; +import {Position} from "@uniswap/v4-core/src/libraries/Position.sol"; import {IERC20} from "forge-std/interfaces/IERC20.sol"; @@ -32,6 +32,7 @@ contract IncreaseLiquidityTest is Test, PosmTestSetup, Fuzzers { using PoolIdLibrary for PoolKey; using Planner for Plan; using FeeMath for IPositionManager; + using StateLibrary for IPoolManager; PoolId poolId; address alice = makeAddr("ALICE"); @@ -78,13 +79,15 @@ contract IncreaseLiquidityTest is Test, PosmTestSetup, Fuzzers { uint256 liquidityBob = 1_000e18; // alice provides liquidity - vm.prank(alice); + vm.startPrank(alice); + uint256 tokenIdAlice = lpm.nextTokenId(); mint(config, liquidityAlice, alice, ZERO_BYTES); - uint256 tokenIdAlice = lpm.nextTokenId() - 1; + vm.stopPrank(); // bob provides liquidity - vm.prank(bob); + vm.startPrank(bob); mint(config, liquidityBob, bob, ZERO_BYTES); + vm.stopPrank(); // swap to create fees uint256 swapAmount = 0.001e18; @@ -132,13 +135,15 @@ contract IncreaseLiquidityTest is Test, PosmTestSetup, Fuzzers { uint256 liquidityBob = 1_000e18; // alice provides liquidity - vm.prank(alice); + vm.startPrank(alice); + uint256 tokenIdAlice = lpm.nextTokenId(); mint(config, liquidityAlice, alice, ZERO_BYTES); - uint256 tokenIdAlice = lpm.nextTokenId() - 1; + vm.stopPrank(); // bob provides liquidity - vm.prank(bob); + vm.startPrank(bob); mint(config, liquidityBob, bob, ZERO_BYTES); + vm.stopPrank(); // donate to create fees uint256 amountDonate = 0.2e18; @@ -175,12 +180,13 @@ contract IncreaseLiquidityTest is Test, PosmTestSetup, Fuzzers { uint256 liquidityAlice = 3_000e18; // alice provides liquidity - vm.prank(alice); + vm.startPrank(alice); + uint256 tokenIdAlice = lpm.nextTokenId(); mint(config, liquidityAlice, alice, ZERO_BYTES); - uint256 tokenIdAlice = lpm.nextTokenId() - 1; + vm.stopPrank(); bytes32 positionId = - keccak256(abi.encodePacked(address(lpm), config.tickLower, config.tickUpper, bytes32(tokenIdAlice))); + 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 @@ -203,24 +209,26 @@ contract IncreaseLiquidityTest is Test, PosmTestSetup, Fuzzers { uint256 totalLiquidity = liquidityAlice + liquidityBob; // alice provides liquidity - vm.prank(alice); + vm.startPrank(alice); + uint256 tokenIdAlice = lpm.nextTokenId(); mint(config, liquidityAlice, alice, ZERO_BYTES); - uint256 tokenIdAlice = lpm.nextTokenId() - 1; + vm.stopPrank(); // bob provides liquidity vm.prank(bob); + uint256 tokenIdBob = lpm.nextTokenId(); mint(config, liquidityBob, bob, ZERO_BYTES); - uint256 tokenIdBob = lpm.nextTokenId() - 1; + 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); - { + // 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, @@ -231,7 +239,6 @@ contract IncreaseLiquidityTest is Test, PosmTestSetup, Fuzzers { ); uint256 balance0BeforeAlice = currency0.balanceOf(alice); uint256 balance1BeforeAlice = currency1.balanceOf(alice); - vm.startPrank(alice); increaseLiquidity(tokenIdAlice, config, liquidityDelta, ZERO_BYTES); vm.stopPrank(); @@ -246,6 +253,14 @@ contract IncreaseLiquidityTest is Test, PosmTestSetup, Fuzzers { 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 + ); } { @@ -266,6 +281,19 @@ contract IncreaseLiquidityTest is Test, PosmTestSetup, Fuzzers { 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 + ); } } @@ -277,14 +305,16 @@ contract IncreaseLiquidityTest is Test, PosmTestSetup, Fuzzers { uint256 totalLiquidity = liquidityAlice + liquidityBob; // alice provides liquidity - vm.prank(alice); + vm.startPrank(alice); + uint256 tokenIdAlice = lpm.nextTokenId(); mint(config, liquidityAlice, alice, ZERO_BYTES); - uint256 tokenIdAlice = lpm.nextTokenId() - 1; + vm.stopPrank(); // bob provides liquidity - vm.prank(bob); + vm.startPrank(bob); + uint256 tokenIdBob = lpm.nextTokenId(); mint(config, liquidityBob, bob, ZERO_BYTES); - uint256 tokenIdBob = lpm.nextTokenId() - 1; + vm.stopPrank(); // swap to create fees uint256 swapAmount = 0.001e18; @@ -338,4 +368,94 @@ contract IncreaseLiquidityTest is Test, PosmTestSetup, Fuzzers { ); } } + + function test_mint_settleWithBalance() public { + uint256 liquidityAlice = 3_000e18; + + Plan memory planner = Planner.init(); + planner.add(Actions.MINT_POSITION, abi.encode(config, liquidityAlice, 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, 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); + } } diff --git a/test/position-managers/NativeToken.t.sol b/test/position-managers/NativeToken.t.sol index 5c7a4592..f8d23c54 100644 --- a/test/position-managers/NativeToken.t.sol +++ b/test/position-managers/NativeToken.t.sol @@ -109,7 +109,15 @@ contract PositionManagerTest is Test, PosmTestSetup, LiquidityFuzzers { uint256 balance1Before = currency1.balanceOfSelf(); uint256 tokenId = lpm.nextTokenId(); - bytes memory calls = getMintEncoded(config, liquidityToAdd, address(this), ZERO_BYTES); + + Plan memory planner = Planner.init(); + planner.add(Actions.MINT_POSITION, abi.encode(config, liquidityToAdd, 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, @@ -301,7 +309,14 @@ contract PositionManagerTest is Test, PosmTestSetup, LiquidityFuzzers { uint128(liquidityToAdd) ); - bytes memory calls = getIncreaseEncoded(tokenId, config, liquidityToAdd, ZERO_BYTES); // double the liquidity + Plan memory planner = Planner.init(); + planner.add(Actions.INCREASE_LIQUIDITY, abi.encode(tokenId, config, liquidityToAdd, 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(); diff --git a/test/position-managers/PositionManager.gas.t.sol b/test/position-managers/PositionManager.gas.t.sol index a0c7244c..a6d9a1da 100644 --- a/test/position-managers/PositionManager.gas.t.sol +++ b/test/position-managers/PositionManager.gas.t.sol @@ -123,8 +123,8 @@ contract PosMGasTest is Test, PosmTestSetup, GasSnapshot { } function test_gas_increaseLiquidity_erc20() public { + uint256 tokenId = lpm.nextTokenId(); mint(config, 10_000 ether, address(this), ZERO_BYTES); - uint256 tokenId = lpm.nextTokenId() - 1; Plan memory planner = Planner.init().add(Actions.INCREASE_LIQUIDITY, abi.encode(tokenId, config, 10_000 ether, ZERO_BYTES)); @@ -142,13 +142,15 @@ contract PosMGasTest is Test, PosmTestSetup, GasSnapshot { uint256 liquidityBob = 1_000e18; // alice provides liquidity - vm.prank(alice); + vm.startPrank(alice); + uint256 tokenIdAlice = lpm.nextTokenId(); mint(config, liquidityAlice, alice, ZERO_BYTES); - uint256 tokenIdAlice = lpm.nextTokenId() - 1; + vm.stopPrank(); // bob provides liquidity - vm.prank(bob); + vm.startPrank(bob); mint(config, liquidityBob, bob, ZERO_BYTES); + vm.stopPrank(); // donate to create fees uint256 amountDonate = 0.2e18; @@ -183,13 +185,15 @@ contract PosMGasTest is Test, PosmTestSetup, GasSnapshot { uint256 liquidityBob = 1_000e18; // alice provides liquidity - vm.prank(alice); + vm.startPrank(alice); + uint256 tokenIdAlice = lpm.nextTokenId(); mint(config, liquidityAlice, alice, ZERO_BYTES); - uint256 tokenIdAlice = lpm.nextTokenId() - 1; + vm.stopPrank(); // bob provides liquidity - vm.prank(bob); + vm.startPrank(bob); mint(config, liquidityBob, bob, ZERO_BYTES); + vm.stopPrank(); // donate to create fees uint256 amountDonate = 20e18; @@ -218,8 +222,8 @@ contract PosMGasTest is Test, PosmTestSetup, GasSnapshot { } function test_gas_decreaseLiquidity() public { + uint256 tokenId = lpm.nextTokenId(); mint(config, 10_000 ether, address(this), ZERO_BYTES); - uint256 tokenId = lpm.nextTokenId() - 1; Plan memory planner = Planner.init().add(Actions.DECREASE_LIQUIDITY, abi.encode(tokenId, config, 10_000 ether, ZERO_BYTES)); @@ -253,8 +257,8 @@ contract PosMGasTest is Test, PosmTestSetup, GasSnapshot { } function test_gas_collect() public { + uint256 tokenId = lpm.nextTokenId(); mint(config, 10_000 ether, address(this), ZERO_BYTES); - uint256 tokenId = lpm.nextTokenId() - 1; // donate to create fee revenue donateRouter.donate(config.poolKey, 0.2e18, 0.2e18, ZERO_BYTES); @@ -285,8 +289,8 @@ contract PosMGasTest is Test, PosmTestSetup, GasSnapshot { mint(config, 10_000 ether, address(this), ZERO_BYTES); vm.stopPrank(); + uint256 tokenId = lpm.nextTokenId(); mint(config, 10_000 ether, address(this), ZERO_BYTES); - uint256 tokenId = lpm.nextTokenId() - 1; Plan memory planner = Planner.init().add(Actions.DECREASE_LIQUIDITY, abi.encode(tokenId, config, 10_000 ether, ZERO_BYTES)); @@ -302,8 +306,8 @@ contract PosMGasTest is Test, PosmTestSetup, GasSnapshot { mint(config, 10_000 ether, address(this), ZERO_BYTES); vm.stopPrank(); + uint256 tokenId = lpm.nextTokenId(); mint(config, 10_000 ether, address(this), ZERO_BYTES); - uint256 tokenId = lpm.nextTokenId() - 1; // donate to create fee revenue donateRouter.donate(config.poolKey, 0.2e18, 0.2e18, ZERO_BYTES); @@ -376,7 +380,13 @@ contract PosMGasTest is Test, PosmTestSetup, GasSnapshot { function test_gas_mint_native_excess() public { uint256 liquidityToAdd = 10_000 ether; - bytes memory calls = getMintEncoded(configNative, liquidityToAdd, address(this), ZERO_BYTES); + + Plan memory planner = Planner.init(); + planner.add(Actions.MINT_POSITION, abi.encode(configNative, liquidityToAdd, 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, @@ -466,4 +476,24 @@ contract PosMGasTest is Test, PosmTestSetup, GasSnapshot { 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, 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.t.sol b/test/position-managers/PositionManager.t.sol index 4309d870..513e9710 100644 --- a/test/position-managers/PositionManager.t.sol +++ b/test/position-managers/PositionManager.t.sol @@ -13,6 +13,7 @@ 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"; @@ -51,6 +52,9 @@ contract PositionManagerTest is Test, PosmTestSetup, LiquidityFuzzers { // Requires currency0 and currency1 to be set in base Deployers contract. deployAndApprovePosm(manager); + + seedBalance(alice); + approvePosmFor(alice); } function test_modifyLiquidities_reverts_mismatchedLengths() public { @@ -286,6 +290,39 @@ contract PositionManagerTest is Test, PosmTestSetup, LiquidityFuzzers { 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()))); + } + + 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 = @@ -293,6 +330,17 @@ contract PositionManagerTest is Test, PosmTestSetup, LiquidityFuzzers { (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_fuzz_decreaseLiquidity_assertCollectedBalance( @@ -329,6 +377,146 @@ contract PositionManagerTest is Test, PosmTestSetup, LiquidityFuzzers { 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))}); @@ -341,10 +529,19 @@ contract PositionManagerTest is Test, PosmTestSetup, LiquidityFuzzers { assertEq(lpFee, key.fee); } - function test_initialize_fuzz() public {} - function test_mintTransferBurn() public {} - function test_mintTransferCollect() public {} - function test_mintTransferIncrease() public {} - function test_mintTransferDecrease() public {} + function test_fuzz_initialize(uint160 sqrtPrice, uint24 fee) public { + sqrtPrice = uint160(bound(sqrtPrice, TickMath.MIN_SQRT_PRICE, TickMath.MAX_SQRT_PRICE)); + 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/shared/HookSavesDelta.sol b/test/shared/HookSavesDelta.sol index e7cc833f..8ff86ac1 100644 --- a/test/shared/HookSavesDelta.sol +++ b/test/shared/HookSavesDelta.sol @@ -40,4 +40,8 @@ contract HookSavesDelta is BaseTestHooks { function numberDeltasReturned() external view returns (uint256) { return deltas.length; } + + function clearDeltas() external { + delete deltas; + } } diff --git a/test/shared/PosmTestSetup.sol b/test/shared/PosmTestSetup.sol index 1b1a3dd1..798a0879 100644 --- a/test/shared/PosmTestSetup.sol +++ b/test/shared/PosmTestSetup.sol @@ -68,4 +68,11 @@ contract PosmTestSetup is Test, Deployers, DeployPermit2, LiquidityOperations { 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/fuzz/LiquidityFuzzers.sol b/test/shared/fuzz/LiquidityFuzzers.sol index fd73c1e2..025d3c9f 100644 --- a/test/shared/fuzz/LiquidityFuzzers.sol +++ b/test/shared/fuzz/LiquidityFuzzers.sol @@ -31,10 +31,10 @@ contract LiquidityFuzzers is Fuzzers { Actions.MINT_POSITION, abi.encode(config, uint256(params.liquidityDelta), recipient, hookData) ); + uint256 tokenId = lpm.nextTokenId(); bytes memory calls = planner.finalizeModifyLiquidity(config.poolKey); lpm.modifyLiquidities(calls, block.timestamp + 1); - uint256 tokenId = lpm.nextTokenId() - 1; return (tokenId, params); } }