Skip to content

Commit

Permalink
clear or take
Browse files Browse the repository at this point in the history
  • Loading branch information
saucepoint committed Aug 2, 2024
1 parent 1bbeef5 commit da06567
Show file tree
Hide file tree
Showing 20 changed files with 90 additions and 57 deletions.
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_burn_nonEmpty.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
129852
129834
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_burn_nonEmpty_native.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
122773
122756
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_collect.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
149984
149962
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_collect_native.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
141136
141114
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_collect_sameRange.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
149984
149962
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_decreaseLiquidity.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
115527
115505
Original file line number Diff line number Diff line change
@@ -1 +1 @@
108384
108366
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_decrease_burnEmpty.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
133885
133868
Original file line number Diff line number Diff line change
@@ -1 +1 @@
126624
126607
Original file line number Diff line number Diff line change
@@ -1 +1 @@
128243
128221
Original file line number Diff line number Diff line change
@@ -1 +1 @@
170759
170737
Original file line number Diff line number Diff line change
@@ -1 +1 @@
140581
140655
20 changes: 14 additions & 6 deletions src/PositionManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ contract PositionManager is
using StateLibrary for IPoolManager;
using TransientStateLibrary for IPoolManager;
using SafeCast for uint256;
using SafeCast for int256;
using CalldataDecoder for bytes;
using SlippageCheckLibrary for BalanceDelta;

Expand Down Expand Up @@ -111,9 +112,9 @@ contract PositionManager is
} else if (action == Actions.CLOSE_CURRENCY) {
Currency currency = params.decodeCurrency();
_close(currency);
} else if (action == Actions.CLEAR) {
} else if (action == Actions.CLEAR_OR_TAKE) {
(Currency currency, uint256 amountMax) = params.decodeCurrencyAndUint256();
_clear(currency, amountMax);
_clearOrTake(currency, amountMax);
} else if (action == Actions.BURN_POSITION) {
// Will automatically decrease liquidity to 0 if the position is not already empty.
(
Expand Down Expand Up @@ -208,11 +209,18 @@ contract PositionManager is
}

/// @dev integrators may elect to forfeit positive deltas with clear
/// provides a safety check that amount-to-clear is less than a user-provided maximum
function _clear(Currency currency, uint256 amountMax) internal {
/// if the forfeit amount exceeds the user-specified max, the amount is taken instead
function _clearOrTake(Currency currency, uint256 amountMax) internal {
int256 currencyDelta = poolManager.currencyDelta(address(this), currency);
if (uint256(currencyDelta) > amountMax) revert ClearExceedsMaxAmount(currency, currencyDelta, amountMax);
poolManager.clear(currency, uint256(currencyDelta));
if (currencyDelta < 0) revert CannotClearNegativeDelta(currency);
uint256 delta = uint256(currencyDelta); // safe since we know its positive

// forfeit the delta if its less than or equal to the user-specified limit
if (delta <= amountMax) {
poolManager.clear(currency, delta);
} else {
_take(currency, _msgSender(), delta);
}
}

/// @dev uses this addresses balance to settle a negative delta
Expand Down
2 changes: 1 addition & 1 deletion src/interfaces/IPositionManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ interface IPositionManager {
error NotApproved(address caller);
error DeadlinePassed();
error IncorrectPositionConfigForTokenId(uint256 tokenId);
error ClearExceedsMaxAmount(Currency currency, int256 amount, uint256 maxAmount);
error CannotClearNegativeDelta(Currency currency);

/// @notice Maps the ERC721 tokenId to a configId, which is a keccak256 hash of the position's pool key, and range (tickLower, tickUpper)
/// Enforces that a minted ERC721 token is tied to one range on one pool.
Expand Down
2 changes: 1 addition & 1 deletion src/libraries/Actions.sol
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ library Actions {

uint256 constant CLOSE_CURRENCY = 0x16;
uint256 constant CLOSE_PAIR = 0x17;
uint256 constant CLEAR = 0x18;
uint256 constant CLEAR_OR_TAKE = 0x18;
uint256 constant SWEEP = 0x19;

// minting/burning 6909s to close deltas
Expand Down
2 changes: 1 addition & 1 deletion test/BaseActionsRouter.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ contract BaseActionsRouterTest is Test, Deployers, GasSnapshot {
function test_clear_suceeds() public {
Plan memory plan = Planner.init();
for (uint256 i = 0; i < 10; i++) {
plan.add(Actions.CLEAR, "");
plan.add(Actions.CLEAR_OR_TAKE, "");
}

assertEq(router.clearCount(), 0);
Expand Down
2 changes: 1 addition & 1 deletion test/mocks/MockBaseActionsRouter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ contract MockBaseActionsRouter is BaseActionsRouter, ReentrancyLock {
} else {
if (action == Actions.SETTLE) _settle(params);
else if (action == Actions.TAKE) _take(params);
else if (action == Actions.CLEAR) _clear(params);
else if (action == Actions.CLEAR_OR_TAKE) _clear(params);
else if (action == Actions.MINT_6909) _mint6909(params);
else if (action == Actions.BURN_6909) _burn6909(params);
else revert UnsupportedAction(action);
Expand Down
47 changes: 27 additions & 20 deletions test/position-managers/IncreaseLiquidity.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -176,8 +176,8 @@ contract IncreaseLiquidityTest is Test, PosmTestSetup, Fuzzers {
Actions.INCREASE_LIQUIDITY,
abi.encode(tokenIdAlice, config, liquidityDelta, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, ZERO_BYTES)
);
planner.add(Actions.CLEAR, abi.encode(config.poolKey.currency0, 18 wei)); // alice is willing to forfeit 18 wei
planner.add(Actions.CLEAR, abi.encode(config.poolKey.currency1, 18 wei));
planner.add(Actions.CLEAR_OR_TAKE, abi.encode(config.poolKey.currency0, 18 wei)); // alice is willing to forfeit 18 wei
planner.add(Actions.CLEAR_OR_TAKE, abi.encode(config.poolKey.currency1, 18 wei));
bytes memory calls = planner.encode();

vm.prank(alice);
Expand Down Expand Up @@ -279,8 +279,8 @@ contract IncreaseLiquidityTest is Test, PosmTestSetup, Fuzzers {
Actions.INCREASE_LIQUIDITY,
abi.encode(tokenIdAlice, config, liquidityDelta, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, ZERO_BYTES)
);
planner.add(Actions.CLEAR, abi.encode(config.poolKey.currency0, 1 wei)); // alice is willing to forfeit 1 wei
planner.add(Actions.CLEAR, abi.encode(config.poolKey.currency1, 1 wei));
planner.add(Actions.CLEAR_OR_TAKE, abi.encode(config.poolKey.currency0, 1 wei)); // alice is willing to forfeit 1 wei
planner.add(Actions.CLEAR_OR_TAKE, abi.encode(config.poolKey.currency1, 1 wei));
bytes memory calls = planner.encode();

vm.prank(alice);
Expand Down Expand Up @@ -653,7 +653,8 @@ contract IncreaseLiquidityTest is Test, PosmTestSetup, Fuzzers {
assertEq(currency1.balanceOf(address(this)), balanceBefore1 - amount1);
}

function test_increaseLiquidity_clearExceeds_revert() public {
/// @dev if clearing exceeds the max amount, the amount is taken instead
function test_increaseLiquidity_clearExceedsThenTake() public {
uint256 tokenId = lpm.nextTokenId();
mint(config, 1000e18, address(this), ZERO_BYTES);

Expand All @@ -663,6 +664,7 @@ contract IncreaseLiquidityTest is Test, PosmTestSetup, Fuzzers {

// calculate the amount of liquidity to add, using half of the proceeds
uint256 amountToReinvest = amountToDonate / 2;
uint256 amountToReclaim = amountToDonate / 2; // expect to reclaim the other half of the fee revenue
uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts(
SQRT_PRICE_1_1,
TickMath.getSqrtPriceAtTick(config.tickLower),
Expand All @@ -671,28 +673,33 @@ contract IncreaseLiquidityTest is Test, PosmTestSetup, Fuzzers {
amountToReinvest
);

// set the max-forfeit to less than the amount we expect to claim
uint256 maxClear = amountToReclaim - 2 wei;

Plan memory planner = Planner.init();
planner.add(
Actions.INCREASE_LIQUIDITY,
abi.encode(tokenId, config, liquidityDelta, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, ZERO_BYTES)
);
planner.add(Actions.CLEAR, abi.encode(config.poolKey.currency0, amountToReinvest - 2 wei));
planner.add(Actions.CLEAR, abi.encode(config.poolKey.currency1, amountToReinvest - 2 wei));
planner.add(Actions.CLEAR_OR_TAKE, abi.encode(config.poolKey.currency0, maxClear));
planner.add(Actions.CLEAR_OR_TAKE, abi.encode(config.poolKey.currency1, maxClear));
bytes memory calls = planner.encode();

// revert since we're forfeiting beyond the max tolerance
vm.expectRevert(
abi.encodeWithSelector(
IPositionManager.ClearExceedsMaxAmount.selector,
config.poolKey.currency0,
int256(amountToReinvest - 1 wei), // imprecision, PM expects us to collect half of the fees (minus 1 wei)
uint256(amountToReinvest - 2 wei) // the maximum amount we were willing to forfeit
)
);
uint256 balance0Before = currency0.balanceOf(address(this));
uint256 balance1Before = currency1.balanceOf(address(this));

// expect to take the excess, as it exceeds the amount to clear
lpm.modifyLiquidities(calls, _deadline);
BalanceDelta delta = getLastDelta();

assertEq(uint128(delta.amount0()), amountToReclaim - 1 wei); // imprecision
assertEq(uint128(delta.amount1()), amountToReclaim - 1 wei);

assertEq(currency0.balanceOf(address(this)), balance0Before + amountToReclaim - 1 wei);
assertEq(currency1.balanceOf(address(this)), balance1Before + amountToReclaim - 1 wei);
}

/// @dev clearing a negative delta reverts in core with SafeCastOverflow
/// @dev clearing a negative delta reverts
function test_increaseLiquidity_clearNegative_revert() public {
uint256 tokenId = lpm.nextTokenId();
mint(config, 1000e18, address(this), ZERO_BYTES);
Expand All @@ -703,12 +710,12 @@ contract IncreaseLiquidityTest is Test, PosmTestSetup, Fuzzers {
Actions.INCREASE_LIQUIDITY,
abi.encode(tokenId, config, 100e18, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, ZERO_BYTES)
);
planner.add(Actions.CLEAR, abi.encode(config.poolKey.currency0, type(uint256).max));
planner.add(Actions.CLEAR, abi.encode(config.poolKey.currency1, type(uint256).max));
planner.add(Actions.CLEAR_OR_TAKE, abi.encode(config.poolKey.currency0, type(uint256).max));
planner.add(Actions.CLEAR_OR_TAKE, abi.encode(config.poolKey.currency1, type(uint256).max));
bytes memory calls = planner.encode();

// revert since we're forfeiting beyond the max tolerance
vm.expectRevert(SafeCast.SafeCastOverflow.selector);
vm.expectRevert(abi.encodeWithSelector(IPositionManager.CannotClearNegativeDelta.selector, currency0));
lpm.modifyLiquidities(calls, _deadline);
}
}
4 changes: 2 additions & 2 deletions test/position-managers/PositionManager.gas.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -230,8 +230,8 @@ contract PosMGasTest is Test, PosmTestSetup, GasSnapshot {
Actions.INCREASE_LIQUIDITY,
abi.encode(tokenIdAlice, config, liquidityDelta, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, ZERO_BYTES)
);
planner.add(Actions.CLEAR, abi.encode(config.poolKey.currency0, halfTokensOwedAlice + 1 wei));
planner.add(Actions.CLEAR, abi.encode(config.poolKey.currency1, halfTokensOwedAlice + 1 wei));
planner.add(Actions.CLEAR_OR_TAKE, abi.encode(config.poolKey.currency0, halfTokensOwedAlice + 1 wei));
planner.add(Actions.CLEAR_OR_TAKE, abi.encode(config.poolKey.currency1, halfTokensOwedAlice + 1 wei));
bytes memory calls = planner.encode();

vm.prank(alice);
Expand Down
44 changes: 31 additions & 13 deletions test/position-managers/PositionManager.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ contract PositionManagerTest is Test, PosmTestSetup, LiquidityFuzzers {
assertEq(currency1.balanceOf(alice), balance1BeforeAlice);
}

/// @dev test that clear does not work on minting
/// @dev clear cannot be used on mint (negative delta)
function test_fuzz_mint_clear_revert(IPoolManager.ModifyLiquidityParams memory seedParams) public {
IPoolManager.ModifyLiquidityParams memory params = createFuzzyLiquidityParams(key, seedParams, SQRT_PRICE_1_1);
uint256 liquidityToAdd =
Expand All @@ -196,11 +196,19 @@ contract PositionManagerTest is Test, PosmTestSetup, LiquidityFuzzers {
Actions.MINT_POSITION,
abi.encode(config, liquidityToAdd, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, address(this), ZERO_BYTES)
);
planner.add(Actions.CLEAR, abi.encode(key.currency0, type(uint256).max));
planner.add(Actions.CLEAR, abi.encode(key.currency1, type(uint256).max));
planner.add(Actions.CLEAR_OR_TAKE, abi.encode(key.currency0, type(uint256).max));
planner.add(Actions.CLEAR_OR_TAKE, abi.encode(key.currency1, type(uint256).max));
bytes memory calls = planner.encode();

vm.expectRevert(SafeCast.SafeCastOverflow.selector);
Currency negativeDeltaCurrency = currency0;
// because we're fuzzing the range, single-sided mint with currency1 means currency0Delta = 0 and currency1Delta < 0
if (config.tickUpper <= 0) {
negativeDeltaCurrency = currency1;
}

vm.expectRevert(
abi.encodeWithSelector(IPositionManager.CannotClearNegativeDelta.selector, negativeDeltaCurrency)
);
lpm.modifyLiquidities(calls, _deadline);
}

Expand Down Expand Up @@ -469,8 +477,8 @@ contract PositionManagerTest is Test, PosmTestSetup, LiquidityFuzzers {
tokenId, config, decreaseLiquidityDelta, MIN_SLIPPAGE_DECREASE, MIN_SLIPPAGE_DECREASE, ZERO_BYTES
)
);
planner.add(Actions.CLEAR, abi.encode(key.currency0, type(uint256).max));
planner.add(Actions.CLEAR, abi.encode(key.currency1, type(uint256).max));
planner.add(Actions.CLEAR_OR_TAKE, abi.encode(key.currency0, type(uint256).max));
planner.add(Actions.CLEAR_OR_TAKE, abi.encode(key.currency1, type(uint256).max));
bytes memory calls = planner.encode();

lpm.modifyLiquidities(calls, _deadline);
Expand All @@ -485,8 +493,10 @@ contract PositionManagerTest is Test, PosmTestSetup, LiquidityFuzzers {
assertEq(currency1.balanceOfSelf(), balance1Before);
}

/// @dev Clearing on decrease reverts if it exceeds user threshold
function test_fuzz_decreaseLiquidity_clearRevert(IPoolManager.ModifyLiquidityParams memory params) public {
/// @dev Clearing on decrease will take tokens if the amount exceeds the clear limit
function test_fuzz_decreaseLiquidity_clearExceedsThenTake(IPoolManager.ModifyLiquidityParams memory params)
public
{
// use fuzzer for tick range
params = createFuzzyLiquidityParams(key, params, SQRT_PRICE_1_1);
vm.assume(params.tickLower < 0 && 0 < params.tickUpper); // require two-sided liquidity
Expand All @@ -511,14 +521,22 @@ contract PositionManagerTest is Test, PosmTestSetup, LiquidityFuzzers {
Actions.DECREASE_LIQUIDITY,
abi.encode(tokenId, config, liquidityToRemove, MIN_SLIPPAGE_DECREASE, MIN_SLIPPAGE_DECREASE, ZERO_BYTES)
);
planner.add(Actions.CLEAR, abi.encode(key.currency0, amount0 - 1 wei));
planner.add(Actions.CLEAR, abi.encode(key.currency1, amount1 - 1 wei));
planner.add(Actions.CLEAR_OR_TAKE, abi.encode(key.currency0, amount0 - 1 wei));
planner.add(Actions.CLEAR_OR_TAKE, abi.encode(key.currency1, amount1 - 1 wei));
bytes memory calls = planner.encode();

vm.expectRevert(
abi.encodeWithSelector(IPositionManager.ClearExceedsMaxAmount.selector, currency0, amount0, amount0 - 1 wei)
);
uint256 balance0Before = currency0.balanceOfSelf();
uint256 balance1Before = currency1.balanceOfSelf();

// expect to take the tokens
lpm.modifyLiquidities(calls, _deadline);
BalanceDelta delta = getLastDelta();

// clear failed, so we should have the tokens
assertEq(uint128(delta.amount0()), amount0);
assertEq(uint128(delta.amount1()), amount1);
assertEq(currency0.balanceOfSelf(), balance0Before + amount0);
assertEq(currency1.balanceOfSelf(), balance1Before + amount1);
}

function test_decreaseLiquidity_collectFees(
Expand Down

0 comments on commit da06567

Please sign in to comment.