Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

posm slippage #225

Merged
merged 16 commits into from
Aug 2, 2024
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_burn_empty.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
47248
47639
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_burn_empty_native.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
47066
47456
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_burn_nonEmpty.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
130117
130604
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 @@
123039
123525
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_collect.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
151223
151829
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_collect_native.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
142375
142981
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_collect_sameRange.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
151223
151829
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_decreaseLiquidity.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
116766
117372
Original file line number Diff line number Diff line change
@@ -1 +1 @@
109375
109860
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_decrease_burnEmpty.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
135156
136042
Original file line number Diff line number Diff line change
@@ -1 +1 @@
127895
128781
Original file line number Diff line number Diff line change
@@ -1 +1 @@
129482
130088
Original file line number Diff line number Diff line change
@@ -1 +1 @@
152736
154000
Original file line number Diff line number Diff line change
@@ -1 +1 @@
134536
135800
Original file line number Diff line number Diff line change
@@ -1 +1 @@
135432
136451
Original file line number Diff line number Diff line change
@@ -1 +1 @@
171588
172607
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_mint.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
372837
373981
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_mint_native.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
337537
338681
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_mint_nativeWithSweep.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
346399
347557
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_mint_onSameTickLower.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
315519
316663
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_mint_onSameTickUpper.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
316161
317305
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_mint_sameRange.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
241743
242887
Original file line number Diff line number Diff line change
@@ -1 +1 @@
371451
372610
Original file line number Diff line number Diff line change
@@ -1 +1 @@
321537
322681
Original file line number Diff line number Diff line change
@@ -1 +1 @@
417173
418329
45 changes: 36 additions & 9 deletions src/PositionManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -100,31 +100,54 @@ contract PositionManager is
/// @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));
(
uint256 tokenId,
PositionConfig memory config,
uint256 liquidity,
uint128 amount0Max,
uint128 amount1Max,
bytes memory hookData
) = abi.decode(params, (uint256, PositionConfig, uint256, uint128, uint128, bytes));

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);
if (liquidityDelta.amount0() < 0 && amount0Max < uint128(-liquidityDelta.amount0())) revert SlippageExceeded();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

helper func! same code is happening twice

_checkSlippage(amount, amountMax);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a case where liquidityDelta < 0 ? I guess a weird hook?

Copy link
Collaborator Author

@saucepoint saucepoint Jul 31, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

liqDelta < 0 -- majority of cases, users paying tokens to increase liquidity. where we want to ensure slippage

0 < liqDelta -- cases where fee revenue spillover needs to be collected

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right i forgot about spillover case

if (liquidityDelta.amount1() < 0 && amount1Max < uint128(-liquidityDelta.amount1())) revert SlippageExceeded();
}

/// @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));
(
uint256 tokenId,
PositionConfig memory config,
uint256 liquidity,
uint128 amount0Min,
uint128 amount1Min,
bytes memory hookData
) = abi.decode(params, (uint256, PositionConfig, uint256, uint128, uint128, bytes));

if (!_isApprovedOrOwner(_msgSender(), tokenId)) revert NotApproved(_msgSender());
if (positionConfigs[tokenId] != config.toId()) revert IncorrectPositionConfigForTokenId(tokenId);

// Note: the tokenId is used as the salt.
BalanceDelta liquidityDelta = _modifyLiquidity(config, -(liquidity.toInt256()), bytes32(tokenId), hookData);
if (uint128(liquidityDelta.amount0()) < amount0Min || uint128(liquidityDelta.amount1()) < amount1Min) {
revert SlippageExceeded();
}
}

/// @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));
(
PositionConfig memory config,
uint256 liquidity,
uint128 amount0Max,
uint128 amount1Max,
address owner,
bytes memory hookData
) = abi.decode(params, (PositionConfig, uint256, uint128, uint128, address, bytes));

// mint receipt token
uint256 tokenId;
Expand All @@ -136,7 +159,8 @@ contract PositionManager is

// _beforeModify is not called here because the tokenId is newly minted
BalanceDelta liquidityDelta = _modifyLiquidity(config, liquidity.toInt256(), bytes32(tokenId), hookData);

if (amount0Max < uint128(-liquidityDelta.amount0())) revert SlippageExceeded();
if (amount1Max < uint128(-liquidityDelta.amount1())) revert SlippageExceeded();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this slippage check different from _increase? shouldn't mint and increase be the same?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_mint is guaranteed to be negative delta so the conditional is cleaner

_increase can be positive (spill over fee revenue to be collected), where a slippage check is irrelevant

positionConfigs[tokenId] = config.toId();
}

Expand Down Expand Up @@ -168,8 +192,8 @@ contract PositionManager is
/// @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));
(uint256 tokenId, PositionConfig memory config, uint128 amount0Min, uint128 amount1Min, bytes memory hookData) =
abi.decode(params, (uint256, PositionConfig, uint128, uint128, bytes));

if (!_isApprovedOrOwner(_msgSender(), tokenId)) revert NotApproved(_msgSender());
if (positionConfigs[tokenId] != config.toId()) revert IncorrectPositionConfigForTokenId(tokenId);
Expand All @@ -179,6 +203,9 @@ contract PositionManager is
// Can only call modify if there is non zero liquidity.
if (liquidity > 0) {
liquidityDelta = _modifyLiquidity(config, -(liquidity.toInt256()), bytes32(tokenId), hookData);
if (uint128(liquidityDelta.amount0()) < amount0Min || uint128(liquidityDelta.amount1()) < amount1Min) {
saucepoint marked this conversation as resolved.
Show resolved Hide resolved
revert SlippageExceeded();
}
}

delete positionConfigs[tokenId];
Expand Down
1 change: 1 addition & 0 deletions src/interfaces/IPositionManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";

interface IPositionManager {
error NotApproved(address caller);
error SlippageExceeded();
error DeadlinePassed();
error IncorrectPositionConfigForTokenId(uint256 tokenId);

Expand Down
34 changes: 28 additions & 6 deletions test/position-managers/Execute.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,14 @@ contract ExecuteTest is Test, PosmTestSetup, LiquidityFuzzers {

Plan memory planner = Planner.init();

planner.add(Actions.INCREASE_LIQUIDITY, abi.encode(tokenId, config, liquidityToAdd, ZERO_BYTES));
planner.add(Actions.INCREASE_LIQUIDITY, abi.encode(tokenId, config, liquidityToAdd2, ZERO_BYTES));
planner.add(
Actions.INCREASE_LIQUIDITY,
abi.encode(tokenId, config, liquidityToAdd, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, ZERO_BYTES)
);
planner.add(
Actions.INCREASE_LIQUIDITY,
abi.encode(tokenId, config, liquidityToAdd2, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, ZERO_BYTES)
);

bytes memory calls = planner.finalizeModifyLiquidity(config.poolKey);
lpm.modifyLiquidities(calls, _deadline);
Expand All @@ -113,8 +119,16 @@ contract ExecuteTest is Test, PosmTestSetup, LiquidityFuzzers {

Plan memory planner = Planner.init();

planner.add(Actions.MINT_POSITION, abi.encode(config, initialLiquidity, address(this), ZERO_BYTES));
planner.add(Actions.INCREASE_LIQUIDITY, abi.encode(tokenId, config, liquidityToAdd, ZERO_BYTES));
planner.add(
Actions.MINT_POSITION,
abi.encode(
config, initialLiquidity, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, address(this), ZERO_BYTES
)
);
planner.add(
Actions.INCREASE_LIQUIDITY,
abi.encode(tokenId, config, liquidityToAdd, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, ZERO_BYTES)
);

bytes memory calls = planner.finalizeModifyLiquidity(config.poolKey);
lpm.modifyLiquidities(calls, _deadline);
Expand Down Expand Up @@ -151,8 +165,16 @@ contract ExecuteTest is Test, PosmTestSetup, LiquidityFuzzers {
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));
planner.add(
Actions.BURN_POSITION,
abi.encode(
tokenId, config, uint128(-delta.amount0()) - 1 wei, uint128(-delta.amount1()) - 1 wei, ZERO_BYTES
)
);
planner.add(
Actions.MINT_POSITION,
abi.encode(newConfig, newLiquidity, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, address(this), ZERO_BYTES)
);
bytes memory calls = planner.finalizeModifyLiquidity(config.poolKey);

lpm.modifyLiquidities(calls, _deadline);
Expand Down
84 changes: 81 additions & 3 deletions test/position-managers/IncreaseLiquidity.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,9 @@ contract IncreaseLiquidityTest is Test, PosmTestSetup, Fuzzers {

// TODO: Can we make this easier to re-invest fees, so that you don't need to know the exact collect amount?
Plan memory planner = Planner.init();
planner.add(Actions.INCREASE_LIQUIDITY, abi.encode(tokenIdAlice, config, liquidityDelta, ZERO_BYTES));
planner.add(
Actions.INCREASE_LIQUIDITY, abi.encode(tokenIdAlice, config, liquidityDelta, 0 wei, 0 wei, ZERO_BYTES)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how come this is 0 and not MAX_SLIPPAGE_INCREASE?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we're ~perfectly autocompounding fees in this test so a strict slippage (no tokens paid by the caller) is an additional assurance

);
bytes memory calls = planner.finalizeModifyLiquidity(config.poolKey);
vm.startPrank(alice);
lpm.modifyLiquidities(calls, _deadline);
Expand Down Expand Up @@ -369,11 +371,84 @@ contract IncreaseLiquidityTest is Test, PosmTestSetup, Fuzzers {
}
}

function test_increaseLiquidity_slippage_revertAmount0() public {
// increasing liquidity with strict slippage parameters (amount0) will revert
uint256 tokenId = lpm.nextTokenId();
mint(config, 100e18, address(this), ZERO_BYTES);

// revert since amount0Max is too low
bytes memory calls = getIncreaseEncoded(tokenId, config, 100e18, 1 wei, type(uint128).max, ZERO_BYTES);
vm.expectRevert(IPositionManager.SlippageExceeded.selector);
lpm.modifyLiquidities(calls, _deadline);
}

function test_increaseLiquidity_slippage_revertAmount1() public {
// increasing liquidity with strict slippage parameters (amount1) will revert
uint256 tokenId = lpm.nextTokenId();
mint(config, 100e18, address(this), ZERO_BYTES);

// revert since amount1Max is too low
bytes memory calls = getIncreaseEncoded(tokenId, config, 100e18, type(uint128).max, 1 wei, ZERO_BYTES);
vm.expectRevert(IPositionManager.SlippageExceeded.selector);
lpm.modifyLiquidities(calls, _deadline);
}

function test_increaseLiquidity_slippage_exactDoesNotRevert() public {
// increasing liquidity with perfect slippage parameters does not revert
uint256 tokenId = lpm.nextTokenId();
mint(config, 100e18, address(this), ZERO_BYTES);

uint128 newLiquidity = 10e18;
(uint256 amount0, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity(
SQRT_PRICE_1_1,
TickMath.getSqrtPriceAtTick(config.tickLower),
TickMath.getSqrtPriceAtTick(config.tickUpper),
newLiquidity
);
assertEq(amount0, amount1); // symmetric liquidity addition
uint128 slippage = uint128(amount0) + 1;

bytes memory calls = getIncreaseEncoded(tokenId, config, newLiquidity, slippage, slippage, ZERO_BYTES);
lpm.modifyLiquidities(calls, _deadline);
BalanceDelta delta = getLastDelta();

// confirm that delta == slippage tolerance
assertEq(-delta.amount0(), int128(slippage));
assertEq(-delta.amount1(), int128(slippage));
}

/// price movement from swaps will cause slippage reverts
function test_increaseLiquidity_slippage_revert_swap() public {
// increasing liquidity with perfect slippage parameters does not revert
uint256 tokenId = lpm.nextTokenId();
mint(config, 100e18, address(this), ZERO_BYTES);

uint128 newLiquidity = 10e18;
(uint256 amount0, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity(
SQRT_PRICE_1_1,
TickMath.getSqrtPriceAtTick(config.tickLower),
TickMath.getSqrtPriceAtTick(config.tickUpper),
newLiquidity
);
assertEq(amount0, amount1); // symmetric liquidity addition
uint128 slippage = uint128(amount0) + 1;

// swap to create slippage
swap(key, true, -10e18, ZERO_BYTES);

bytes memory calls = getIncreaseEncoded(tokenId, config, newLiquidity, slippage, slippage, ZERO_BYTES);
vm.expectRevert(IPositionManager.SlippageExceeded.selector);
lpm.modifyLiquidities(calls, _deadline);
}

function test_mint_settleWithBalance() public {
uint256 liquidityAlice = 3_000e18;

Plan memory planner = Planner.init();
planner.add(Actions.MINT_POSITION, abi.encode(config, liquidityAlice, alice, ZERO_BYTES));
planner.add(
Actions.MINT_POSITION,
abi.encode(config, liquidityAlice, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, alice, ZERO_BYTES)
);
planner.add(Actions.SETTLE_WITH_BALANCE, abi.encode(currency0));
planner.add(Actions.SETTLE_WITH_BALANCE, abi.encode(currency1));
planner.add(Actions.SWEEP, abi.encode(currency0, address(this)));
Expand Down Expand Up @@ -422,7 +497,10 @@ contract IncreaseLiquidityTest is Test, PosmTestSetup, Fuzzers {

// 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.INCREASE_LIQUIDITY,
abi.encode(tokenIdAlice, config, liquidityAlice, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, ZERO_BYTES)
);
planner.add(Actions.SETTLE_WITH_BALANCE, abi.encode(currency0));
planner.add(Actions.SETTLE_WITH_BALANCE, abi.encode(currency1));
planner.add(Actions.SWEEP, abi.encode(currency0, address(this)));
Expand Down
Loading