From e5829f6bbd656444bce937a27f4d714b6de82d75 Mon Sep 17 00:00:00 2001 From: Sara Reynolds <30504811+snreynolds@users.noreply.github.com> Date: Fri, 22 Nov 2024 16:09:02 +0000 Subject: [PATCH] add from amount support (#384) * add from amount support * update gas snaps * add calldata decoder fuzz tests * use open delta * mint from deltas * increase test * use SafeCast * add fuzz test, optimize clear * spearbit comments * fix FOT fuzz test * add license --------- Co-authored-by: marktoda Co-authored-by: Alice Henshaw Co-authored-by: Alice <34962750+hensha256@users.noreply.github.com> --- .../PositionManager_burn_empty.snap | 2 +- .../PositionManager_burn_empty_native.snap | 2 +- ...anager_burn_nonEmpty_native_withClose.snap | 2 +- ...ger_burn_nonEmpty_native_withTakePair.snap | 2 +- ...sitionManager_burn_nonEmpty_withClose.snap | 2 +- ...ionManager_burn_nonEmpty_withTakePair.snap | 2 +- .../PositionManager_collect_native.snap | 2 +- .../PositionManager_collect_sameRange.snap | 2 +- .../PositionManager_collect_withClose.snap | 2 +- .../PositionManager_collect_withTakePair.snap | 2 +- ...itionManager_decreaseLiquidity_native.snap | 2 +- ...onManager_decreaseLiquidity_withClose.snap | 2 +- ...anager_decreaseLiquidity_withTakePair.snap | 2 +- .../PositionManager_decrease_burnEmpty.snap | 2 +- ...tionManager_decrease_burnEmpty_native.snap | 2 +- ...nager_decrease_sameRange_allLiquidity.snap | 2 +- .../PositionManager_decrease_take_take.snap | 2 +- ...ger_increase_autocompound_clearExcess.snap | 2 +- .../PositionManager_mint_native.snap | 2 +- ...anager_mint_nativeWithSweep_withClose.snap | 2 +- ...r_mint_nativeWithSweep_withSettlePair.snap | 2 +- .../PositionManager_mint_onSameTickLower.snap | 2 +- .../PositionManager_mint_onSameTickUpper.snap | 2 +- .../PositionManager_mint_sameRange.snap | 2 +- ...nManager_mint_settleWithBalance_sweep.snap | 2 +- ...anager_mint_warmedPool_differentRange.snap | 2 +- .../PositionManager_mint_withClose.snap | 2 +- .../PositionManager_mint_withSettlePair.snap | 2 +- ...tionManager_multicall_initialize_mint.snap | 2 +- src/PositionManager.sol | 71 ++++- src/libraries/Actions.sol | 42 +-- src/libraries/CalldataDecoder.sol | 41 +++ src/libraries/LiquidityAmounts.sol | 75 +++++ test/libraries/CalldataDecoder.t.sol | 40 ++- test/mocks/MockCalldataDecoder.sol | 43 +++ test/mocks/MockFeeOnTransfer.sol | 43 +++ .../PositionManager.modifyLiquidities.t.sol | 272 +++++++++++++++++- test/shared/PosmTestSetup.sol | 13 +- 38 files changed, 644 insertions(+), 54 deletions(-) create mode 100644 src/libraries/LiquidityAmounts.sol create mode 100644 test/mocks/MockFeeOnTransfer.sol diff --git a/.forge-snapshots/PositionManager_burn_empty.snap b/.forge-snapshots/PositionManager_burn_empty.snap index 16a0217a7..2479c9837 100644 --- a/.forge-snapshots/PositionManager_burn_empty.snap +++ b/.forge-snapshots/PositionManager_burn_empty.snap @@ -1 +1 @@ -50479 \ No newline at end of file +50540 \ 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 16a0217a7..2479c9837 100644 --- a/.forge-snapshots/PositionManager_burn_empty_native.snap +++ b/.forge-snapshots/PositionManager_burn_empty_native.snap @@ -1 +1 @@ -50479 \ No newline at end of file +50540 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_burn_nonEmpty_native_withClose.snap b/.forge-snapshots/PositionManager_burn_nonEmpty_native_withClose.snap index 7b256dc71..6cc21bc29 100644 --- a/.forge-snapshots/PositionManager_burn_nonEmpty_native_withClose.snap +++ b/.forge-snapshots/PositionManager_burn_nonEmpty_native_withClose.snap @@ -1 +1 @@ -125652 \ No newline at end of file +125712 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_burn_nonEmpty_native_withTakePair.snap b/.forge-snapshots/PositionManager_burn_nonEmpty_native_withTakePair.snap index 7c676e270..152039dde 100644 --- a/.forge-snapshots/PositionManager_burn_nonEmpty_native_withTakePair.snap +++ b/.forge-snapshots/PositionManager_burn_nonEmpty_native_withTakePair.snap @@ -1 +1 @@ -125134 \ No newline at end of file +125195 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_burn_nonEmpty_withClose.snap b/.forge-snapshots/PositionManager_burn_nonEmpty_withClose.snap index 4a700a1c7..9f4b916f8 100644 --- a/.forge-snapshots/PositionManager_burn_nonEmpty_withClose.snap +++ b/.forge-snapshots/PositionManager_burn_nonEmpty_withClose.snap @@ -1 +1 @@ -132512 \ No newline at end of file +132572 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_burn_nonEmpty_withTakePair.snap b/.forge-snapshots/PositionManager_burn_nonEmpty_withTakePair.snap index b74086572..84dd6ef48 100644 --- a/.forge-snapshots/PositionManager_burn_nonEmpty_withTakePair.snap +++ b/.forge-snapshots/PositionManager_burn_nonEmpty_withTakePair.snap @@ -1 +1 @@ -131994 \ No newline at end of file +132055 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_collect_native.snap b/.forge-snapshots/PositionManager_collect_native.snap index de5f92d75..5b1ce3e4f 100644 --- a/.forge-snapshots/PositionManager_collect_native.snap +++ b/.forge-snapshots/PositionManager_collect_native.snap @@ -1 +1 @@ -146379 \ No newline at end of file +146402 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_collect_sameRange.snap b/.forge-snapshots/PositionManager_collect_sameRange.snap index 2f8946517..4cdcba5fe 100644 --- a/.forge-snapshots/PositionManager_collect_sameRange.snap +++ b/.forge-snapshots/PositionManager_collect_sameRange.snap @@ -1 +1 @@ -154954 \ No newline at end of file +154977 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_collect_withClose.snap b/.forge-snapshots/PositionManager_collect_withClose.snap index 2f8946517..4cdcba5fe 100644 --- a/.forge-snapshots/PositionManager_collect_withClose.snap +++ b/.forge-snapshots/PositionManager_collect_withClose.snap @@ -1 +1 @@ -154954 \ No newline at end of file +154977 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_collect_withTakePair.snap b/.forge-snapshots/PositionManager_collect_withTakePair.snap index 8b55bcf18..338401963 100644 --- a/.forge-snapshots/PositionManager_collect_withTakePair.snap +++ b/.forge-snapshots/PositionManager_collect_withTakePair.snap @@ -1 +1 @@ -154319 \ No newline at end of file +154342 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_decreaseLiquidity_native.snap b/.forge-snapshots/PositionManager_decreaseLiquidity_native.snap index 4f47d5cdd..eddfbeb8a 100644 --- a/.forge-snapshots/PositionManager_decreaseLiquidity_native.snap +++ b/.forge-snapshots/PositionManager_decreaseLiquidity_native.snap @@ -1 +1 @@ -112048 \ No newline at end of file +112067 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_decreaseLiquidity_withClose.snap b/.forge-snapshots/PositionManager_decreaseLiquidity_withClose.snap index 5320c7d05..cb0cab2e8 100644 --- a/.forge-snapshots/PositionManager_decreaseLiquidity_withClose.snap +++ b/.forge-snapshots/PositionManager_decreaseLiquidity_withClose.snap @@ -1 +1 @@ -119835 \ No newline at end of file +119858 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_decreaseLiquidity_withTakePair.snap b/.forge-snapshots/PositionManager_decreaseLiquidity_withTakePair.snap index 690234de4..5b8a8ab82 100644 --- a/.forge-snapshots/PositionManager_decreaseLiquidity_withTakePair.snap +++ b/.forge-snapshots/PositionManager_decreaseLiquidity_withTakePair.snap @@ -1 +1 @@ -119200 \ No newline at end of file +119223 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_decrease_burnEmpty.snap b/.forge-snapshots/PositionManager_decrease_burnEmpty.snap index 1bd57c304..c2e430727 100644 --- a/.forge-snapshots/PositionManager_decrease_burnEmpty.snap +++ b/.forge-snapshots/PositionManager_decrease_burnEmpty.snap @@ -1 +1 @@ -135308 \ No newline at end of file +135388 \ 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 d1d857dcb..46828a493 100644 --- a/.forge-snapshots/PositionManager_decrease_burnEmpty_native.snap +++ b/.forge-snapshots/PositionManager_decrease_burnEmpty_native.snap @@ -1 +1 @@ -128448 \ No newline at end of file +128528 \ 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 99eac1b69..0d940fbfa 100644 --- a/.forge-snapshots/PositionManager_decrease_sameRange_allLiquidity.snap +++ b/.forge-snapshots/PositionManager_decrease_sameRange_allLiquidity.snap @@ -1 +1 @@ -132522 \ No newline at end of file +132545 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_decrease_take_take.snap b/.forge-snapshots/PositionManager_decrease_take_take.snap index 07b447cf2..bf7f2b047 100644 --- a/.forge-snapshots/PositionManager_decrease_take_take.snap +++ b/.forge-snapshots/PositionManager_decrease_take_take.snap @@ -1 +1 @@ -120455 \ No newline at end of file +120478 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_increase_autocompound_clearExcess.snap b/.forge-snapshots/PositionManager_increase_autocompound_clearExcess.snap index 8a5e17d06..85389eb86 100644 --- a/.forge-snapshots/PositionManager_increase_autocompound_clearExcess.snap +++ b/.forge-snapshots/PositionManager_increase_autocompound_clearExcess.snap @@ -1 +1 @@ -148160 \ No newline at end of file +148210 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_native.snap b/.forge-snapshots/PositionManager_mint_native.snap index 86b73c4d1..428620b58 100644 --- a/.forge-snapshots/PositionManager_mint_native.snap +++ b/.forge-snapshots/PositionManager_mint_native.snap @@ -1 +1 @@ -365994 \ No newline at end of file +366017 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_nativeWithSweep_withClose.snap b/.forge-snapshots/PositionManager_mint_nativeWithSweep_withClose.snap index bb299253b..6669c3da7 100644 --- a/.forge-snapshots/PositionManager_mint_nativeWithSweep_withClose.snap +++ b/.forge-snapshots/PositionManager_mint_nativeWithSweep_withClose.snap @@ -1 +1 @@ -374512 \ No newline at end of file +374535 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_nativeWithSweep_withSettlePair.snap b/.forge-snapshots/PositionManager_mint_nativeWithSweep_withSettlePair.snap index f3ab1f4e6..d361ccef6 100644 --- a/.forge-snapshots/PositionManager_mint_nativeWithSweep_withSettlePair.snap +++ b/.forge-snapshots/PositionManager_mint_nativeWithSweep_withSettlePair.snap @@ -1 +1 @@ -373748 \ No newline at end of file +373771 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_onSameTickLower.snap b/.forge-snapshots/PositionManager_mint_onSameTickLower.snap index 0aff1998a..b10c99fe9 100644 --- a/.forge-snapshots/PositionManager_mint_onSameTickLower.snap +++ b/.forge-snapshots/PositionManager_mint_onSameTickLower.snap @@ -1 +1 @@ -317417 \ No newline at end of file +317440 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_onSameTickUpper.snap b/.forge-snapshots/PositionManager_mint_onSameTickUpper.snap index 6a2602583..a10679c13 100644 --- a/.forge-snapshots/PositionManager_mint_onSameTickUpper.snap +++ b/.forge-snapshots/PositionManager_mint_onSameTickUpper.snap @@ -1 +1 @@ -318087 \ No newline at end of file +318110 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_sameRange.snap b/.forge-snapshots/PositionManager_mint_sameRange.snap index 8c4f5a353..c1e9eea41 100644 --- a/.forge-snapshots/PositionManager_mint_sameRange.snap +++ b/.forge-snapshots/PositionManager_mint_sameRange.snap @@ -1 +1 @@ -243656 \ No newline at end of file +243679 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_settleWithBalance_sweep.snap b/.forge-snapshots/PositionManager_mint_settleWithBalance_sweep.snap index 2d831268b..1854045d0 100644 --- a/.forge-snapshots/PositionManager_mint_settleWithBalance_sweep.snap +++ b/.forge-snapshots/PositionManager_mint_settleWithBalance_sweep.snap @@ -1 +1 @@ -418864 \ No newline at end of file +418887 \ 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 d4011d6ca..db4e21f2d 100644 --- a/.forge-snapshots/PositionManager_mint_warmedPool_differentRange.snap +++ b/.forge-snapshots/PositionManager_mint_warmedPool_differentRange.snap @@ -1 +1 @@ -323448 \ No newline at end of file +323471 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_withClose.snap b/.forge-snapshots/PositionManager_mint_withClose.snap index 33db8cb92..c8af8e307 100644 --- a/.forge-snapshots/PositionManager_mint_withClose.snap +++ b/.forge-snapshots/PositionManager_mint_withClose.snap @@ -1 +1 @@ -419970 \ No newline at end of file +419993 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_withSettlePair.snap b/.forge-snapshots/PositionManager_mint_withSettlePair.snap index 4605a1b60..7d6b55033 100644 --- a/.forge-snapshots/PositionManager_mint_withSettlePair.snap +++ b/.forge-snapshots/PositionManager_mint_withSettlePair.snap @@ -1 +1 @@ -419040 \ No newline at end of file +419063 \ 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 79b9364c8..5c67ca185 100644 --- a/.forge-snapshots/PositionManager_multicall_initialize_mint.snap +++ b/.forge-snapshots/PositionManager_multicall_initialize_mint.snap @@ -1 +1 @@ -455814 \ No newline at end of file +455837 \ No newline at end of file diff --git a/src/PositionManager.sol b/src/PositionManager.sol index 73253dbf2..58efd722e 100644 --- a/src/PositionManager.sol +++ b/src/PositionManager.sol @@ -11,8 +11,9 @@ import {Position} from "@uniswap/v4-core/src/libraries/Position.sol"; import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; import {TransientStateLibrary} from "@uniswap/v4-core/src/libraries/TransientStateLibrary.sol"; import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol"; -import {IPositionDescriptor} from "./interfaces/IPositionDescriptor.sol"; +import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; +import {IPositionDescriptor} from "./interfaces/IPositionDescriptor.sol"; import {ERC721Permit_v4} from "./base/ERC721Permit_v4.sol"; import {ReentrancyLock} from "./base/ReentrancyLock.sol"; import {IPositionManager} from "./interfaces/IPositionManager.sol"; @@ -26,6 +27,7 @@ import {CalldataDecoder} from "./libraries/CalldataDecoder.sol"; import {Permit2Forwarder} from "./base/Permit2Forwarder.sol"; import {SlippageCheck} from "./libraries/SlippageCheck.sol"; import {PositionInfo, PositionInfoLibrary} from "./libraries/PositionInfoLibrary.sol"; +import {LiquidityAmounts} from "./libraries/LiquidityAmounts.sol"; import {NativeWrapper} from "./base/NativeWrapper.sol"; import {IWETH9} from "./interfaces/external/IWETH9.sol"; @@ -199,6 +201,11 @@ contract PositionManager is params.decodeModifyLiquidityParams(); _increase(tokenId, liquidity, amount0Max, amount1Max, hookData); return; + } else if (action == Actions.INCREASE_LIQUIDITY_FROM_DELTAS) { + (uint256 tokenId, uint128 amount0Max, uint128 amount1Max, bytes calldata hookData) = + params.decodeIncreaseLiquidityFromDeltasParams(); + _increaseFromDeltas(tokenId, amount0Max, amount1Max, hookData); + return; } else if (action == Actions.DECREASE_LIQUIDITY) { (uint256 tokenId, uint256 liquidity, uint128 amount0Min, uint128 amount1Min, bytes calldata hookData) = params.decodeModifyLiquidityParams(); @@ -217,6 +224,18 @@ contract PositionManager is ) = params.decodeMintParams(); _mint(poolKey, tickLower, tickUpper, liquidity, amount0Max, amount1Max, _mapRecipient(owner), hookData); return; + } else if (action == Actions.MINT_POSITION_FROM_DELTAS) { + ( + PoolKey calldata poolKey, + int24 tickLower, + int24 tickUpper, + uint128 amount0Max, + uint128 amount1Max, + address owner, + bytes calldata hookData + ) = params.decodeMintFromDeltasParams(); + _mintFromDeltas(poolKey, tickLower, tickUpper, amount0Max, amount1Max, _mapRecipient(owner), hookData); + return; } else if (action == Actions.BURN_POSITION) { // Will automatically decrease liquidity to 0 if the position is not already empty. (uint256 tokenId, uint128 amount0Min, uint128 amount1Min, bytes calldata hookData) = @@ -283,6 +302,31 @@ contract PositionManager is (liquidityDelta - feesAccrued).validateMaxIn(amount0Max, amount1Max); } + /// @dev The liquidity delta is derived from open deltas in the pool manager. + function _increaseFromDeltas(uint256 tokenId, uint128 amount0Max, uint128 amount1Max, bytes calldata hookData) + internal + onlyIfApproved(msgSender(), tokenId) + { + (PoolKey memory poolKey, PositionInfo info) = getPoolAndPositionInfo(tokenId); + + (uint160 sqrtPriceX96,,,) = poolManager.getSlot0(poolKey.toId()); + + // Use the credit on the pool manager as the amounts for the mint. + uint256 liquidity = LiquidityAmounts.getLiquidityForAmounts( + sqrtPriceX96, + TickMath.getSqrtPriceAtTick(info.tickLower()), + TickMath.getSqrtPriceAtTick(info.tickUpper()), + _getFullCredit(poolKey.currency0), + _getFullCredit(poolKey.currency1) + ); + + // Note: The tokenId is used as the salt for this position, so every minted position has unique storage in the pool manager. + (BalanceDelta liquidityDelta, BalanceDelta feesAccrued) = + _modifyLiquidity(info, poolKey, liquidity.toInt256(), bytes32(tokenId), hookData); + // Slippage checks should be done on the principal liquidityDelta which is the liquidityDelta - feesAccrued + (liquidityDelta - feesAccrued).validateMaxIn(amount0Max, amount1Max); + } + /// @dev Calling decrease with 0 liquidity will credit the caller with any underlying fees of the position function _decrease( uint256 tokenId, @@ -335,6 +379,29 @@ contract PositionManager is liquidityDelta.validateMaxIn(amount0Max, amount1Max); } + function _mintFromDeltas( + PoolKey calldata poolKey, + int24 tickLower, + int24 tickUpper, + uint128 amount0Max, + uint128 amount1Max, + address owner, + bytes calldata hookData + ) internal { + (uint160 sqrtPriceX96,,,) = poolManager.getSlot0(poolKey.toId()); + + // Use the credit on the pool manager as the amounts for the mint. + uint256 liquidity = LiquidityAmounts.getLiquidityForAmounts( + sqrtPriceX96, + TickMath.getSqrtPriceAtTick(tickLower), + TickMath.getSqrtPriceAtTick(tickUpper), + _getFullCredit(poolKey.currency0), + _getFullCredit(poolKey.currency1) + ); + + _mint(poolKey, tickLower, tickUpper, liquidity, amount0Max, amount1Max, owner, hookData); + } + /// @dev this is overloaded with ERC721Permit_v4._burn function _burn(uint256 tokenId, uint128 amount0Min, uint128 amount1Min, bytes calldata hookData) internal @@ -389,8 +456,10 @@ contract PositionManager is /// @dev integrators may elect to forfeit positive deltas with clear /// if the forfeit amount exceeds the user-specified max, the amount is taken instead + /// if there is no credit, no call is made. function _clearOrTake(Currency currency, uint256 amountMax) internal { uint256 delta = _getFullCredit(currency); + if (delta == 0) return; // forfeit the delta if its less than or equal to the user-specified limit if (delta <= amountMax) { diff --git a/src/libraries/Actions.sol b/src/libraries/Actions.sol index 883eaeecb..207a8c7be 100644 --- a/src/libraries/Actions.sol +++ b/src/libraries/Actions.sol @@ -10,32 +10,36 @@ library Actions { uint256 constant DECREASE_LIQUIDITY = 0x01; uint256 constant MINT_POSITION = 0x02; uint256 constant BURN_POSITION = 0x03; + uint256 constant INCREASE_LIQUIDITY_FROM_DELTAS = 0x04; + uint256 constant MINT_POSITION_FROM_DELTAS = 0x05; + // swapping - uint256 constant SWAP_EXACT_IN_SINGLE = 0x04; - uint256 constant SWAP_EXACT_IN = 0x05; - uint256 constant SWAP_EXACT_OUT_SINGLE = 0x06; - uint256 constant SWAP_EXACT_OUT = 0x07; + uint256 constant SWAP_EXACT_IN_SINGLE = 0x06; + uint256 constant SWAP_EXACT_IN = 0x07; + uint256 constant SWAP_EXACT_OUT_SINGLE = 0x08; + uint256 constant SWAP_EXACT_OUT = 0x09; // donate - uint256 constant DONATE = 0x08; + uint256 constant DONATE = 0x0a; // closing deltas on the pool manager // settling - uint256 constant SETTLE = 0x09; - uint256 constant SETTLE_ALL = 0x10; - uint256 constant SETTLE_PAIR = 0x11; + uint256 constant SETTLE = 0x0b; + uint256 constant SETTLE_ALL = 0x0c; + uint256 constant SETTLE_PAIR = 0x0d; // taking - uint256 constant TAKE = 0x12; - uint256 constant TAKE_ALL = 0x13; - uint256 constant TAKE_PORTION = 0x14; - uint256 constant TAKE_PAIR = 0x15; + uint256 constant TAKE = 0x0e; + uint256 constant TAKE_ALL = 0x0f; + uint256 constant TAKE_PORTION = 0x10; + uint256 constant TAKE_PAIR = 0x11; + + uint256 constant CLOSE_CURRENCY = 0x12; + uint256 constant CLEAR_OR_TAKE = 0x13; + uint256 constant SWEEP = 0x14; - uint256 constant CLOSE_CURRENCY = 0x17; - uint256 constant CLEAR_OR_TAKE = 0x18; - uint256 constant SWEEP = 0x19; - uint256 constant WRAP = 0x20; - uint256 constant UNWRAP = 0x21; + uint256 constant WRAP = 0x15; + uint256 constant UNWRAP = 0x16; // minting/burning 6909s to close deltas - uint256 constant MINT_6909 = 0x22; - uint256 constant BURN_6909 = 0x23; + uint256 constant MINT_6909 = 0x17; + uint256 constant BURN_6909 = 0x18; } diff --git a/src/libraries/CalldataDecoder.sol b/src/libraries/CalldataDecoder.sol index a14f3a34e..00eeeb37c 100644 --- a/src/libraries/CalldataDecoder.sol +++ b/src/libraries/CalldataDecoder.sol @@ -86,6 +86,21 @@ library CalldataDecoder { hookData = params.toBytes(4); } + /// @dev equivalent to: abi.decode(params, (uint256, uint128, uint128, bytes)) in calldata + function decodeIncreaseLiquidityFromDeltasParams(bytes calldata params) + internal + pure + returns (uint256 tokenId, uint128 amount0Max, uint128 amount1Max, bytes calldata hookData) + { + assembly ("memory-safe") { + tokenId := calldataload(params.offset) + amount0Max := calldataload(add(params.offset, 0x20)) + amount1Max := calldataload(add(params.offset, 0x40)) + } + + hookData = params.toBytes(3); + } + /// @dev equivalent to: abi.decode(params, (PoolKey, int24, int24, uint256, uint128, uint128, address, bytes)) in calldata function decodeMintParams(bytes calldata params) internal @@ -113,6 +128,32 @@ library CalldataDecoder { hookData = params.toBytes(11); } + /// @dev equivalent to: abi.decode(params, (PoolKey, int24, int24, uint128, uint128, address, bytes)) in calldata + function decodeMintFromDeltasParams(bytes calldata params) + internal + pure + returns ( + PoolKey calldata poolKey, + int24 tickLower, + int24 tickUpper, + uint128 amount0Max, + uint128 amount1Max, + address owner, + bytes calldata hookData + ) + { + assembly ("memory-safe") { + poolKey := params.offset + tickLower := calldataload(add(params.offset, 0xa0)) + tickUpper := calldataload(add(params.offset, 0xc0)) + amount0Max := calldataload(add(params.offset, 0xe0)) + amount1Max := calldataload(add(params.offset, 0x100)) + owner := calldataload(add(params.offset, 0x120)) + } + + hookData = params.toBytes(10); + } + /// @dev equivalent to: abi.decode(params, (uint256, uint128, uint128, bytes)) in calldata function decodeBurnParams(bytes calldata params) internal diff --git a/src/libraries/LiquidityAmounts.sol b/src/libraries/LiquidityAmounts.sol new file mode 100644 index 000000000..d6d2dd96d --- /dev/null +++ b/src/libraries/LiquidityAmounts.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {FullMath} from "@uniswap/v4-core/src/libraries/FullMath.sol"; +import {FixedPoint96} from "@uniswap/v4-core/src/libraries/FixedPoint96.sol"; +import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; + +/// @notice Provides functions for computing liquidity amounts from token amounts and prices +library LiquidityAmounts { + using SafeCast for uint256; + + /// @notice Computes the amount of liquidity received for a given amount of token0 and price range + /// @dev Calculates amount0 * (sqrt(upper) * sqrt(lower)) / (sqrt(upper) - sqrt(lower)) + /// @param sqrtPriceAX96 A sqrt price representing the first tick boundary + /// @param sqrtPriceBX96 A sqrt price representing the second tick boundary + /// @param amount0 The amount0 being sent in + /// @return liquidity The amount of returned liquidity + function getLiquidityForAmount0(uint160 sqrtPriceAX96, uint160 sqrtPriceBX96, uint256 amount0) + internal + pure + returns (uint128 liquidity) + { + unchecked { + if (sqrtPriceAX96 > sqrtPriceBX96) (sqrtPriceAX96, sqrtPriceBX96) = (sqrtPriceBX96, sqrtPriceAX96); + uint256 intermediate = FullMath.mulDiv(sqrtPriceAX96, sqrtPriceBX96, FixedPoint96.Q96); + return FullMath.mulDiv(amount0, intermediate, sqrtPriceBX96 - sqrtPriceAX96).toUint128(); + } + } + + /// @notice Computes the amount of liquidity received for a given amount of token1 and price range + /// @dev Calculates amount1 / (sqrt(upper) - sqrt(lower)). + /// @param sqrtPriceAX96 A sqrt price representing the first tick boundary + /// @param sqrtPriceBX96 A sqrt price representing the second tick boundary + /// @param amount1 The amount1 being sent in + /// @return liquidity The amount of returned liquidity + function getLiquidityForAmount1(uint160 sqrtPriceAX96, uint160 sqrtPriceBX96, uint256 amount1) + internal + pure + returns (uint128 liquidity) + { + unchecked { + if (sqrtPriceAX96 > sqrtPriceBX96) (sqrtPriceAX96, sqrtPriceBX96) = (sqrtPriceBX96, sqrtPriceAX96); + return FullMath.mulDiv(amount1, FixedPoint96.Q96, sqrtPriceBX96 - sqrtPriceAX96).toUint128(); + } + } + + /// @notice Computes the maximum amount of liquidity received for a given amount of token0, token1, the current + /// pool prices and the prices at the tick boundaries + /// @param sqrtPriceX96 A sqrt price representing the current pool prices + /// @param sqrtPriceAX96 A sqrt price representing the first tick boundary + /// @param sqrtPriceBX96 A sqrt price representing the second tick boundary + /// @param amount0 The amount of token0 being sent in + /// @param amount1 The amount of token1 being sent in + /// @return liquidity The maximum amount of liquidity received + function getLiquidityForAmounts( + uint160 sqrtPriceX96, + uint160 sqrtPriceAX96, + uint160 sqrtPriceBX96, + uint256 amount0, + uint256 amount1 + ) internal pure returns (uint128 liquidity) { + if (sqrtPriceAX96 > sqrtPriceBX96) (sqrtPriceAX96, sqrtPriceBX96) = (sqrtPriceBX96, sqrtPriceAX96); + + if (sqrtPriceX96 <= sqrtPriceAX96) { + liquidity = getLiquidityForAmount0(sqrtPriceAX96, sqrtPriceBX96, amount0); + } else if (sqrtPriceX96 < sqrtPriceBX96) { + uint128 liquidity0 = getLiquidityForAmount0(sqrtPriceX96, sqrtPriceBX96, amount0); + uint128 liquidity1 = getLiquidityForAmount1(sqrtPriceAX96, sqrtPriceX96, amount1); + + liquidity = liquidity0 < liquidity1 ? liquidity0 : liquidity1; + } else { + liquidity = getLiquidityForAmount1(sqrtPriceAX96, sqrtPriceBX96, amount1); + } + } +} diff --git a/test/libraries/CalldataDecoder.t.sol b/test/libraries/CalldataDecoder.t.sol index de1006678..f772c514e 100644 --- a/test/libraries/CalldataDecoder.t.sol +++ b/test/libraries/CalldataDecoder.t.sol @@ -83,6 +83,28 @@ contract CalldataDecoderTest is Test { assertEq(mintParams.tickUpper, _config.tickUpper); } + function test_fuzz_decodeMintFromDeltasParams( + PositionConfig calldata _config, + uint128 _amount0Max, + uint128 _amount1Max, + address _owner, + bytes calldata _hookData + ) public view { + bytes memory params = abi.encode( + _config.poolKey, _config.tickLower, _config.tickUpper, _amount0Max, _amount1Max, _owner, _hookData + ); + + (MockCalldataDecoder.MintFromDeltasParams memory mintParams) = decoder.decodeMintFromDeltasParams(params); + + _assertEq(mintParams.poolKey, _config.poolKey); + assertEq(mintParams.tickLower, _config.tickLower); + assertEq(mintParams.tickUpper, _config.tickUpper); + assertEq(mintParams.amount0Max, _amount0Max); + assertEq(mintParams.amount1Max, _amount1Max); + assertEq(mintParams.owner, _owner); + assertEq(mintParams.hookData, _hookData); + } + function test_fuzz_decodeSwapExactInParams(IV4Router.ExactInputParams calldata _swapParams) public view { bytes memory params = abi.encode(_swapParams); IV4Router.ExactInputParams memory swapParams = decoder.decodeSwapExactInParams(params); @@ -232,7 +254,23 @@ contract CalldataDecoderTest is Test { assertEq(amount, _amount); } - function test_fuzz_decodeUint256(uint256 _amount) public { + function test_fuzz_decodeIncreaseLiquidityFromAmountsParams( + uint256 _tokenId, + uint128 _amount0Max, + uint128 _amount1Max, + bytes calldata _hookData + ) public view { + bytes memory params = abi.encode(_tokenId, _amount0Max, _amount1Max, _hookData); + + (uint256 tokenId, uint128 amount0Max, uint128 amount1Max, bytes memory hookData) = + decoder.decodeIncreaseLiquidityFromDeltasParams(params); + assertEq(_tokenId, tokenId); + assertEq(_amount0Max, amount0Max); + assertEq(_amount1Max, amount1Max); + assertEq(_hookData, hookData); + } + + function test_fuzz_decodeUint256(uint256 _amount) public view { bytes memory params = abi.encode(_amount); uint256 amount = decoder.decodeUint256(params); diff --git a/test/mocks/MockCalldataDecoder.sol b/test/mocks/MockCalldataDecoder.sol index c61db4afa..c4bd803ee 100644 --- a/test/mocks/MockCalldataDecoder.sol +++ b/test/mocks/MockCalldataDecoder.sol @@ -22,6 +22,16 @@ contract MockCalldataDecoder { bytes hookData; } + struct MintFromDeltasParams { + PoolKey poolKey; + int24 tickLower; + int24 tickUpper; + uint128 amount0Max; + uint128 amount1Max; + address owner; + bytes hookData; + } + function decodeActionsRouterParams(bytes calldata params) external pure @@ -137,6 +147,39 @@ contract MockCalldataDecoder { return params.decodeCurrencyAddressAndUint256(); } + function decodeIncreaseLiquidityFromDeltasParams(bytes calldata params) + external + pure + returns (uint256 tokenId, uint128 amount0Max, uint128 amount1Max, bytes calldata hookData) + { + return params.decodeIncreaseLiquidityFromDeltasParams(); + } + + function decodeMintFromDeltasParams(bytes calldata params) + external + pure + returns (MintFromDeltasParams memory mintParams) + { + ( + PoolKey memory poolKey, + int24 tickLower, + int24 tickUpper, + uint128 amount0Max, + uint128 amount1Max, + address owner, + bytes memory hookData + ) = params.decodeMintFromDeltasParams(); + return MintFromDeltasParams({ + poolKey: poolKey, + tickLower: tickLower, + tickUpper: tickUpper, + amount0Max: amount0Max, + amount1Max: amount1Max, + owner: owner, + hookData: hookData + }); + } + function decodeUint256(bytes calldata params) external pure returns (uint256) { return params.decodeUint256(); } diff --git a/test/mocks/MockFeeOnTransfer.sol b/test/mocks/MockFeeOnTransfer.sol new file mode 100644 index 000000000..1150a777d --- /dev/null +++ b/test/mocks/MockFeeOnTransfer.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.20; + +import {MockERC20} from "solmate/src/test/utils/mocks/MockERC20.sol"; +import {IPositionManager} from "../../src/interfaces/IPositionManager.sol"; +import {BipsLibrary} from "../../src/libraries/BipsLibrary.sol"; + +contract MockFOT is MockERC20 { + using BipsLibrary for uint256; + + IPositionManager immutable posm; + + uint256 public bips; + + constructor(IPositionManager _posm) MockERC20("FOT Token", "FOT", 18) { + posm = _posm; + } + + function setFee(uint256 amountInBips) public { + bips = amountInBips; + } + + function transferFrom(address from, address to, uint256 amount) public override returns (bool) { + uint256 allowed = allowance[from][msg.sender]; + + if (allowed != type(uint256).max) allowance[from][msg.sender] = allowed - amount; + + balanceOf[from] -= amount; + + // bips% fee on the recipient + uint256 amountAfterFee = amount - amount.calculatePortion(bips); + + // Cannot overflow because the sum of all user + // balances can't exceed the max uint256 value. + unchecked { + balanceOf[to] += amountAfterFee; + } + + emit Transfer(from, to, amount); + + return true; + } +} diff --git a/test/position-managers/PositionManager.modifyLiquidities.t.sol b/test/position-managers/PositionManager.modifyLiquidities.t.sol index 4e14a733c..664758616 100644 --- a/test/position-managers/PositionManager.modifyLiquidities.t.sol +++ b/test/position-managers/PositionManager.modifyLiquidities.t.sol @@ -18,6 +18,9 @@ import {Position} from "@uniswap/v4-core/src/libraries/Position.sol"; import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; import {LiquidityAmounts} from "@uniswap/v4-core/test/utils/LiquidityAmounts.sol"; import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; +import {Pool} from "@uniswap/v4-core/src/libraries/Pool.sol"; + +import {MockERC20} from "solmate/src/test/utils/mocks/MockERC20.sol"; import {IPositionManager} from "../../src/interfaces/IPositionManager.sol"; import {IMulticall_v4} from "../../src/interfaces/IMulticall_v4.sol"; @@ -25,6 +28,7 @@ import {ReentrancyLock} from "../../src/base/ReentrancyLock.sol"; import {Actions} from "../../src/libraries/Actions.sol"; import {PositionManager} from "../../src/PositionManager.sol"; import {PositionConfig} from "../shared/PositionConfig.sol"; +import {BipsLibrary} from "../../src/libraries/BipsLibrary.sol"; import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; import {Planner, Plan} from "../shared/Planner.sol"; @@ -32,20 +36,27 @@ import {PosmTestSetup} from "../shared/PosmTestSetup.sol"; import {ActionConstants} from "../../src/libraries/ActionConstants.sol"; import {Planner, Plan} from "../shared/Planner.sol"; import {DeltaResolver} from "../../src/base/DeltaResolver.sol"; +import {MockFOT} from "../mocks/MockFeeOnTransfer.sol"; contract PositionManagerModifyLiquiditiesTest is Test, PosmTestSetup, LiquidityFuzzers { using StateLibrary for IPoolManager; using PoolIdLibrary for PoolKey; using Planner for Plan; + using BipsLibrary for uint256; PoolId poolId; address alice; uint256 alicePK; address bob; + PoolKey fotKey; + PositionConfig config; PositionConfig wethConfig; PositionConfig nativeConfig; + PositionConfig fotConfig; + + MockERC20 fotToken; function setUp() public { (alice, alicePK) = makeAddrAndKey("ALICE"); @@ -66,7 +77,7 @@ contract PositionManagerModifyLiquiditiesTest is Test, PosmTestSetup, LiquidityF seedBalance(address(hookModifyLiquidities)); (key, poolId) = initPool(currency0, currency1, IHooks(hookModifyLiquidities), 3000, SQRT_PRICE_1_1); - initWethPool(currency1, IHooks(address(0)), 3000, SQRT_PRICE_1_1); + wethKey = initPoolUnsorted(Currency.wrap(address(_WETH9)), currency1, IHooks(address(0)), 3000, SQRT_PRICE_1_1); seedWeth(address(this)); approvePosmCurrency(Currency.wrap(address(_WETH9))); @@ -83,6 +94,11 @@ contract PositionManagerModifyLiquiditiesTest is Test, PosmTestSetup, LiquidityF nativeConfig = PositionConfig({poolKey: nativeKey, tickLower: -120, tickUpper: 120}); vm.deal(address(this), 1000 ether); + + fotToken = new MockFOT(lpm); + approvePosmCurrency(Currency.wrap(address(fotToken))); + seedToken(fotToken, address(this)); + fotKey = initPoolUnsorted(Currency.wrap(address(fotToken)), currency1, IHooks(address(0)), 3000, SQRT_PRICE_1_1); } /// @dev minting liquidity without approval is allowable @@ -694,4 +710,258 @@ contract PositionManagerModifyLiquiditiesTest is Test, PosmTestSetup, LiquidityF vm.expectRevert(DeltaResolver.InsufficientBalance.selector); lpm.modifyLiquidities(actions, _deadline); } + + function test_mintFromDeltas_fot() public { + // Use a 1% fee. + MockFOT(address(fotToken)).setFee(100); + uint256 tokenId = lpm.nextTokenId(); + + uint256 fotBalanceBefore = Currency.wrap(address(fotToken)).balanceOf(address(this)); + + uint256 amountAfterTransfer = 990e18; + uint256 amountToSendFot = 1000e18; + + (uint256 amount0, uint256 amount1) = fotKey.currency0 == Currency.wrap(address(fotToken)) + ? (amountToSendFot, amountAfterTransfer) + : (amountAfterTransfer, amountToSendFot); + + // Calculcate the expected liquidity from the amounts after the transfer. They are the same for both currencies. + uint256 expectedLiquidity = LiquidityAmounts.getLiquidityForAmounts( + SQRT_PRICE_1_1, + TickMath.getSqrtPriceAtTick(LIQUIDITY_PARAMS.tickLower), + TickMath.getSqrtPriceAtTick(LIQUIDITY_PARAMS.tickUpper), + amountAfterTransfer, + amountAfterTransfer + ); + + Plan memory planner = Planner.init(); + planner.add(Actions.SETTLE, abi.encode(fotKey.currency0, amount0, true)); + planner.add(Actions.SETTLE, abi.encode(fotKey.currency1, amount1, true)); + planner.add( + Actions.MINT_POSITION_FROM_DELTAS, + abi.encode( + fotKey, + LIQUIDITY_PARAMS.tickLower, + LIQUIDITY_PARAMS.tickUpper, + MAX_SLIPPAGE_INCREASE, + MAX_SLIPPAGE_INCREASE, + ActionConstants.MSG_SENDER, + ZERO_BYTES + ) + ); + + bytes memory plan = planner.encode(); + + lpm.modifyLiquidities(plan, _deadline); + + uint256 fotBalanceAfter = Currency.wrap(address(fotToken)).balanceOf(address(this)); + + assertEq(lpm.ownerOf(tokenId), address(this)); + assertEq(lpm.getPositionLiquidity(tokenId), expectedLiquidity); + assertEq(fotBalanceBefore - fotBalanceAfter, 1000e18); + } + + function test_increaseFromDeltas() public { + uint128 initialLiquidity = 1000e18; + uint256 tokenId = lpm.nextTokenId(); + fotConfig = PositionConfig({poolKey: fotKey, tickLower: -120, tickUpper: 120}); + + mint(fotConfig, initialLiquidity, address(this), ZERO_BYTES); + + assertEq(lpm.ownerOf(tokenId), address(this)); + assertEq(lpm.getPositionLiquidity(tokenId), initialLiquidity); + + Plan memory planner = Planner.init(); + planner.add(Actions.SETTLE, abi.encode(fotKey.currency0, 10e18, true)); + planner.add(Actions.SETTLE, abi.encode(fotKey.currency1, 10e18, true)); + planner.add( + Actions.INCREASE_LIQUIDITY_FROM_DELTAS, + abi.encode(tokenId, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, ZERO_BYTES) + ); + + bytes memory actions = planner.encode(); + + lpm.modifyLiquidities(actions, _deadline); + + uint128 newLiquidity = LiquidityAmounts.getLiquidityForAmounts( + SQRT_PRICE_1_1, + TickMath.getSqrtPriceAtTick(fotConfig.tickLower), + TickMath.getSqrtPriceAtTick(fotConfig.tickUpper), + 10e18, + 10e18 + ); + + assertEq(lpm.getPositionLiquidity(tokenId), initialLiquidity + newLiquidity); + } + + function test_increaseFromDeltas_fot() public { + uint128 initialLiquidity = 1000e18; + uint256 tokenId = lpm.nextTokenId(); + fotConfig = PositionConfig({poolKey: fotKey, tickLower: -120, tickUpper: 120}); + + mint(fotConfig, initialLiquidity, address(this), ZERO_BYTES); + + assertEq(lpm.ownerOf(tokenId), address(this)); + assertEq(lpm.getPositionLiquidity(tokenId), initialLiquidity); + + // Use a 1% fee. + MockFOT(address(fotToken)).setFee(100); + + // Set the fee on transfer amount 1% higher. + (uint256 amount0, uint256 amount1) = + fotKey.currency0 == Currency.wrap(address(fotToken)) ? (100e18, 99e18) : (99e19, 100e18); + + Plan memory planner = Planner.init(); + planner.add(Actions.SETTLE, abi.encode(fotKey.currency0, amount0, true)); + planner.add(Actions.SETTLE, abi.encode(fotKey.currency1, amount1, true)); + planner.add( + Actions.INCREASE_LIQUIDITY_FROM_DELTAS, + abi.encode(tokenId, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, ZERO_BYTES) + ); + + bytes memory actions = planner.encode(); + + lpm.modifyLiquidities(actions, _deadline); + + (uint256 amount0AfterTransfer, uint256 amount1AfterTransfer) = + fotKey.currency0 == Currency.wrap(address(fotToken)) ? (99e18, 100e18) : (100e18, 99e19); + + uint128 newLiquidity = LiquidityAmounts.getLiquidityForAmounts( + SQRT_PRICE_1_1, + TickMath.getSqrtPriceAtTick(fotConfig.tickLower), + TickMath.getSqrtPriceAtTick(fotConfig.tickUpper), + amount0AfterTransfer, + amount1AfterTransfer + ); + + assertEq(lpm.getPositionLiquidity(tokenId), initialLiquidity + newLiquidity); + } + + function test_fuzz_mintFromDeltas_burn_fot( + uint256 bips, + uint256 amount0, + uint256 amount1, + int24 tickLower, + int24 tickUpper + ) public { + bips = bound(bips, 1, 10_000); + MockFOT(address(fotToken)).setFee(bips); + + tickLower = int24( + bound( + tickLower, + fotKey.tickSpacing * (TickMath.MIN_TICK / fotKey.tickSpacing), + fotKey.tickSpacing * (TickMath.MAX_TICK / fotKey.tickSpacing) + ) + ); + tickUpper = int24( + bound( + tickUpper, + fotKey.tickSpacing * (TickMath.MIN_TICK / fotKey.tickSpacing), + fotKey.tickSpacing * (TickMath.MAX_TICK / fotKey.tickSpacing) + ) + ); + + tickLower = fotKey.tickSpacing * (tickLower / fotKey.tickSpacing); + tickUpper = fotKey.tickSpacing * (tickUpper / fotKey.tickSpacing); + vm.assume(tickUpper > tickLower); + + (uint160 sqrtPriceX96,,,) = manager.getSlot0(fotKey.toId()); + uint128 maxLiquidityPerTick = Pool.tickSpacingToMaxLiquidityPerTick(fotKey.tickSpacing); + + (uint256 maxAmount0, uint256 maxAmount1) = LiquidityAmounts.getAmountsForLiquidity( + sqrtPriceX96, + TickMath.getSqrtPriceAtTick(tickLower), + TickMath.getSqrtPriceAtTick(tickUpper), + maxLiquidityPerTick + ); + + maxAmount0 = maxAmount0 == 0 ? 1 : maxAmount0 > STARTING_USER_BALANCE ? STARTING_USER_BALANCE : maxAmount0; + maxAmount1 = maxAmount1 == 0 ? 1 : maxAmount1 > STARTING_USER_BALANCE ? STARTING_USER_BALANCE : maxAmount1; + amount0 = bound(amount0, 1, maxAmount0); + amount1 = bound(amount1, 1, maxAmount1); + + uint256 tokenId = lpm.nextTokenId(); + + uint256 balance0 = fotKey.currency0.balanceOf(address(this)); + uint256 balance1 = fotKey.currency1.balanceOf(address(this)); + uint256 balance0PM = fotKey.currency0.balanceOf(address(manager)); + uint256 balance1PM = fotKey.currency1.balanceOf(address(manager)); + + Plan memory planner = Planner.init(); + planner.add(Actions.SETTLE, abi.encode(fotKey.currency0, amount0, true)); + planner.add(Actions.SETTLE, abi.encode(fotKey.currency1, amount1, true)); + planner.add( + Actions.MINT_POSITION_FROM_DELTAS, + abi.encode( + fotKey, + tickLower, + tickUpper, + MAX_SLIPPAGE_INCREASE, + MAX_SLIPPAGE_INCREASE, + ActionConstants.MSG_SENDER, + ZERO_BYTES + ) + ); + // take the excess of each currency + planner.add(Actions.TAKE_PAIR, abi.encode(fotKey.currency0, fotKey.currency1, ActionConstants.MSG_SENDER)); + + bytes memory actions = planner.encode(); + + bool currency0IsFOT = fotKey.currency0 == Currency.wrap(address(fotToken)); + bool positionIsEntirelyInOtherToken = currency0IsFOT + ? tickUpper <= TickMath.getTickAtSqrtPrice(sqrtPriceX96) + : tickLower > TickMath.getTickAtSqrtPrice(sqrtPriceX96); + + if (bips == 10000 && !positionIsEntirelyInOtherToken) { + vm.expectRevert(Position.CannotUpdateEmptyPosition.selector); + lpm.modifyLiquidities(actions, _deadline); + } else { + // MINT FROM DELTAS. + lpm.modifyLiquidities(actions, _deadline); + + uint256 balance0After = fotKey.currency0.balanceOf(address(this)); + uint256 balance1After = fotKey.currency1.balanceOf(address(this)); + uint256 balance0PMAfter = fotKey.currency0.balanceOf(address(manager)); + uint256 balance1PMAfter = fotKey.currency1.balanceOf(address(manager)); + + // Calculate the expected resulting balances used to create liquidity after the fee is applied. + uint256 amountInFOT = currency0IsFOT ? amount0 : amount1; + uint256 expectedFee = amountInFOT.calculatePortion(bips); + (uint256 expected0, uint256 expected1) = currency0IsFOT + ? (balance0 - balance0After - expectedFee, balance1 - balance1After) + : (balance0 - balance0After, balance1 - balance1After - expectedFee); + + assertEq(expected0, balance0PMAfter - balance0PM); + assertEq(expected1, balance1PMAfter - balance1PM); + + // the liquidity that was created is a diff of the balance change + uint128 expectedLiquidity = LiquidityAmounts.getLiquidityForAmounts( + sqrtPriceX96, + TickMath.getSqrtPriceAtTick(tickLower), + TickMath.getSqrtPriceAtTick(tickUpper), + expected0, + expected1 + ); + + assertEq(lpm.ownerOf(tokenId), address(this)); + assertEq(lpm.getPositionLiquidity(tokenId), expectedLiquidity); + + // BURN. + planner = Planner.init(); + // Note that the slippage does not include the fee from the transfer. + planner.add( + Actions.BURN_POSITION, + abi.encode(tokenId, expected0 == 0 ? 0 : expected0 - 1, expected1 == 0 ? 0 : expected1 - 1, ZERO_BYTES) + ); + + planner.add(Actions.TAKE_PAIR, abi.encode(fotKey.currency0, fotKey.currency1, ActionConstants.MSG_SENDER)); + + actions = planner.encode(); + + lpm.modifyLiquidities(actions, _deadline); + + assertEq(lpm.getPositionLiquidity(tokenId), 0); + } + } } diff --git a/test/shared/PosmTestSetup.sol b/test/shared/PosmTestSetup.sol index 22e09ed9f..bce86b97b 100644 --- a/test/shared/PosmTestSetup.sol +++ b/test/shared/PosmTestSetup.sol @@ -103,11 +103,18 @@ contract PosmTestSetup is Test, Deployers, DeployPermit2, LiquidityOperations { _WETH9.transfer(to, STARTING_USER_BALANCE); } - function initWethPool(Currency currencyB, IHooks hooks, uint24 fee, uint160 sqrtPriceX96) internal { + function seedToken(MockERC20 token, address to) internal { + token.mint(to, STARTING_USER_BALANCE); + } + + function initPoolUnsorted(Currency currencyA, Currency currencyB, IHooks hooks, uint24 fee, uint160 sqrtPriceX96) + internal + returns (PoolKey memory poolKey) + { (Currency _currency0, Currency _currency1) = - SortTokens.sort(MockERC20(address(_WETH9)), MockERC20(Currency.unwrap(currencyB))); + SortTokens.sort(MockERC20(Currency.unwrap(currencyA)), MockERC20(Currency.unwrap(currencyB))); - (wethKey,) = initPool(_currency0, _currency1, hooks, fee, sqrtPriceX96); + (poolKey,) = initPool(_currency0, _currency1, hooks, fee, sqrtPriceX96); } function permit(uint256 privateKey, uint256 tokenId, address operator, uint256 nonce) internal {