diff --git a/.forge-snapshots/PositionManager_decrease_take_take.snap b/.forge-snapshots/PositionManager_decrease_take_take.snap new file mode 100644 index 00000000..1e5f58d8 --- /dev/null +++ b/.forge-snapshots/PositionManager_decrease_take_take.snap @@ -0,0 +1 @@ +116877 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_increaseLiquidity_erc20.snap b/.forge-snapshots/PositionManager_increaseLiquidity_erc20.snap deleted file mode 100644 index 076b23da..00000000 --- a/.forge-snapshots/PositionManager_increaseLiquidity_erc20.snap +++ /dev/null @@ -1 +0,0 @@ -152144 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint.snap b/.forge-snapshots/PositionManager_mint.snap deleted file mode 100644 index 964f844e..00000000 --- a/.forge-snapshots/PositionManager_mint.snap +++ /dev/null @@ -1 +0,0 @@ -372007 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_nativeWithSweep.snap b/.forge-snapshots/PositionManager_mint_nativeWithSweep.snap deleted file mode 100644 index d691d43c..00000000 --- a/.forge-snapshots/PositionManager_mint_nativeWithSweep.snap +++ /dev/null @@ -1 +0,0 @@ -345190 \ No newline at end of file diff --git a/src/PositionManager.sol b/src/PositionManager.sol index 88873384..131d4526 100644 --- a/src/PositionManager.sol +++ b/src/PositionManager.sol @@ -186,6 +186,9 @@ contract PositionManager is } else if (action == Actions.SWEEP) { (Currency currency, address to) = params.decodeCurrencyAndAddress(); _sweep(currency, _mapRecipient(to)); + } else if (action == Actions.TAKE) { + (Currency currency, address recipient, uint256 amount) = params.decodeCurrencyAddressAndUint256(); + _take(currency, _mapRecipient(recipient), _mapTakeAmount(amount, currency)); } else { revert UnsupportedAction(action); } diff --git a/src/base/DeltaResolver.sol b/src/base/DeltaResolver.sol index 75bc5649..9521cd82 100644 --- a/src/base/DeltaResolver.sol +++ b/src/base/DeltaResolver.sol @@ -79,6 +79,14 @@ abstract contract DeltaResolver is ImmutableState { return amount; } + /// @notice Calculates the amount for a take action + function _mapTakeAmount(uint256 amount, Currency currency) internal view returns (uint256) { + if (amount == Constants.OPEN_DELTA) { + return _getFullCredit(currency).toUint128(); + } + return amount; + } + /// @notice Calculates the amount for a swap action function _mapInputAmount(uint128 amount, Currency currency) internal view returns (uint128) { if (amount == Constants.CONTRACT_BALANCE) { diff --git a/test/libraries/CalldataDecoder.t.sol b/test/libraries/CalldataDecoder.t.sol index 9ce41fb0..29398697 100644 --- a/test/libraries/CalldataDecoder.t.sol +++ b/test/libraries/CalldataDecoder.t.sol @@ -172,6 +172,18 @@ contract CalldataDecoderTest is Test { assertEq(_address, __address); } + function test_fuzz_decodeCurrencyAddressAndUint256(Currency _currency, address _addr, uint256 _amount) + public + view + { + bytes memory params = abi.encode(_currency, _addr, _amount); + (Currency currency, address addr, uint256 amount) = decoder.decodeCurrencyAddressAndUint256(params); + + assertEq(Currency.unwrap(currency), Currency.unwrap(_currency)); + assertEq(addr, _addr); + assertEq(amount, _amount); + } + function test_fuzz_decodeCurrencyAndUint256(Currency _currency, uint256 _amount) public view { bytes memory params = abi.encode(_currency, _amount); (Currency currency, uint256 amount) = decoder.decodeCurrencyAndUint256(params); diff --git a/test/mocks/MockCalldataDecoder.sol b/test/mocks/MockCalldataDecoder.sol index b63654e9..695a526b 100644 --- a/test/mocks/MockCalldataDecoder.sol +++ b/test/mocks/MockCalldataDecoder.sol @@ -113,4 +113,12 @@ contract MockCalldataDecoder { function decodeCurrencyAndUint256(bytes calldata params) external pure returns (Currency currency, uint256 _uint) { return params.decodeCurrencyAndUint256(); } + + function decodeCurrencyAddressAndUint256(bytes calldata params) + external + pure + returns (Currency currency, address addr, uint256 amount) + { + return params.decodeCurrencyAddressAndUint256(); + } } diff --git a/test/position-managers/PositionManager.gas.t.sol b/test/position-managers/PositionManager.gas.t.sol index c092f2fe..299d4697 100644 --- a/test/position-managers/PositionManager.gas.t.sol +++ b/test/position-managers/PositionManager.gas.t.sol @@ -807,4 +807,20 @@ contract PosMGasTest is Test, PosmTestSetup, GasSnapshot { lpm.modifyLiquidities(calls, _deadline); snapLastCall("PositionManager_mint_settleWithBalance_sweep"); } + + // Does not encode a take pair + function test_gas_decrease_take_take() public { + uint256 tokenId = lpm.nextTokenId(); + mint(config, 1e18, Constants.MSG_SENDER, ZERO_BYTES); + + Plan memory plan = Planner.init(); + plan.add( + Actions.DECREASE_LIQUIDITY, + abi.encode(tokenId, config, 1e18, MIN_SLIPPAGE_DECREASE, MIN_SLIPPAGE_DECREASE, ZERO_BYTES) + ); + bytes memory calls = plan.finalizeModifyLiquidityWithTake(config.poolKey, Constants.MSG_SENDER); + + lpm.modifyLiquidities(calls, _deadline); + snapLastCall("PositionManager_decrease_take_take"); + } } diff --git a/test/position-managers/PositionManager.t.sol b/test/position-managers/PositionManager.t.sol index 7afae777..4a47e7e6 100644 --- a/test/position-managers/PositionManager.t.sol +++ b/test/position-managers/PositionManager.t.sol @@ -878,5 +878,76 @@ contract PositionManagerTest is Test, PosmTestSetup, LiquidityFuzzers { assertEq(lpFee, fee); } + // tests a decrease and take in both currencies + // does not use take pair, so its less optimal + function test_decrease_take() public { + PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); + uint256 tokenId = lpm.nextTokenId(); + mint(config, 1e18, Constants.MSG_SENDER, ZERO_BYTES); + + hook.clearDeltas(); + + uint256 balanceBefore0 = currency0.balanceOfSelf(); + uint256 balanceBefore1 = currency1.balanceOfSelf(); + + Plan memory plan = Planner.init(); + plan.add( + Actions.DECREASE_LIQUIDITY, + abi.encode(tokenId, config, 1e18, MIN_SLIPPAGE_DECREASE, MIN_SLIPPAGE_DECREASE, ZERO_BYTES) + ); + bytes memory calls = plan.finalizeModifyLiquidityWithTake(config.poolKey, Constants.MSG_SENDER); + + lpm.modifyLiquidities(calls, _deadline); + BalanceDelta delta = getLastDelta(); + + assertEq(currency0.balanceOfSelf(), balanceBefore0 + uint256(int256(delta.amount0()))); + assertEq(currency1.balanceOfSelf(), balanceBefore1 + uint256(int256(delta.amount1()))); + } + + // decrease full range position + // mint new one sided position in currency1 + // expect to TAKE currency0 and SETTLE currency1 + function test_decrease_increaseCurrency1_take_settle() public { + PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); + uint256 tokenId = lpm.nextTokenId(); + mint(config, 1e18, Constants.MSG_SENDER, ZERO_BYTES); + + hook.clearDeltas(); + + uint256 balanceBefore0 = currency0.balanceOfSelf(); + uint256 balanceBefore1 = currency1.balanceOfSelf(); + + uint256 tokenIdMint = lpm.nextTokenId(); + + // one-sided liq in currency1 + PositionConfig memory configMint = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 0}); + + Plan memory plan = Planner.init(); + plan.add( + Actions.DECREASE_LIQUIDITY, + abi.encode(tokenId, config, 1e18, MIN_SLIPPAGE_DECREASE, MIN_SLIPPAGE_DECREASE, ZERO_BYTES) + ); + plan.add( + Actions.MINT_POSITION, + abi.encode(configMint, 1e18, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, Constants.MSG_SENDER, ZERO_BYTES) + ); + plan.add(Actions.TAKE, abi.encode(key.currency0, Constants.MSG_SENDER, Constants.OPEN_DELTA)); + plan.add(Actions.SETTLE, abi.encode(key.currency1, Constants.OPEN_DELTA, true)); + bytes memory calls = plan.finalizeModifyLiquidityWithTake(config.poolKey, Constants.MSG_SENDER); + + lpm.modifyLiquidities(calls, _deadline); + BalanceDelta deltaDecrease = hook.deltas(0); + BalanceDelta deltaMint = hook.deltas(1); + + assertEq(deltaMint.amount0(), 0); // there is no currency0 in the new position + assertEq(currency0.balanceOfSelf(), balanceBefore0 + uint256(int256(deltaDecrease.amount0()))); + assertEq( + currency1.balanceOfSelf(), balanceBefore1 - uint256(-int256(deltaDecrease.amount1() + deltaMint.amount1())) + ); + assertEq(lpm.ownerOf(tokenIdMint), address(this)); + assertLt(currency1.balanceOfSelf(), balanceBefore1); // currency1 was owed + assertLt(uint256(int256(deltaDecrease.amount1())), uint256(int256(-deltaMint.amount1()))); // amount1 in the second position was greater than amount1 in the first position + } + function test_mint_slippageRevert() public {} } diff --git a/test/shared/Planner.sol b/test/shared/Planner.sol index 8a0ffd8a..ac5a81aa 100644 --- a/test/shared/Planner.sol +++ b/test/shared/Planner.sol @@ -6,6 +6,7 @@ import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {IPositionManager} from "../../src/interfaces/IPositionManager.sol"; import {Actions} from "../../src/libraries/Actions.sol"; import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; +import {Constants} from "../../src/libraries/Constants.sol"; struct Plan { bytes actions; @@ -37,6 +38,16 @@ library Planner { return plan; } + function finalizeModifyLiquidityWithTake(Plan memory plan, PoolKey memory poolKey, address takeRecipient) + internal + pure + returns (bytes memory) + { + plan.add(Actions.TAKE, abi.encode(poolKey.currency0, takeRecipient, Constants.OPEN_DELTA)); + plan.add(Actions.TAKE, abi.encode(poolKey.currency1, takeRecipient, Constants.OPEN_DELTA)); + return plan.encode(); + } + function finalizeModifyLiquidityWithClose(Plan memory plan, PoolKey memory poolKey) internal pure