Skip to content

Commit

Permalink
additional liquidity tests (#129)
Browse files Browse the repository at this point in the history
* additional increase liquidity tests

* edge case of using cached fees for autocompound

* wip
  • Loading branch information
saucepoint authored Jun 25, 2024
1 parent 5ffc760 commit 4d3218e
Show file tree
Hide file tree
Showing 21 changed files with 240 additions and 20 deletions.
2 changes: 1 addition & 1 deletion .forge-snapshots/FullRangeAddInitialLiquidity.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
311181
354477
2 changes: 1 addition & 1 deletion .forge-snapshots/FullRangeAddLiquidity.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
122990
161786
2 changes: 1 addition & 1 deletion .forge-snapshots/FullRangeFirstSwap.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
80220
146400
2 changes: 1 addition & 1 deletion .forge-snapshots/FullRangeInitialize.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1016976
1039616
2 changes: 1 addition & 1 deletion .forge-snapshots/FullRangeRemoveLiquidity.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
110566
146394
2 changes: 1 addition & 1 deletion .forge-snapshots/FullRangeRemoveLiquidityAndRebalance.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
240044
281672
2 changes: 1 addition & 1 deletion .forge-snapshots/FullRangeSecondSwap.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
45930
116110
2 changes: 1 addition & 1 deletion .forge-snapshots/FullRangeSwap.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
79351
145819
2 changes: 1 addition & 1 deletion .forge-snapshots/OracleGrow10Slots.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
232960
254164
2 changes: 1 addition & 1 deletion .forge-snapshots/OracleGrow10SlotsCardinalityGreater.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
223649
249653
2 changes: 1 addition & 1 deletion .forge-snapshots/OracleGrow1Slot.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
32845
54049
2 changes: 1 addition & 1 deletion .forge-snapshots/OracleGrow1SlotCardinalityGreater.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
23545
49549
2 changes: 1 addition & 1 deletion .forge-snapshots/OracleInitialize.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
51310
72794
2 changes: 1 addition & 1 deletion .forge-snapshots/TWAMMSubmitOrder.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
122336
156828
2 changes: 1 addition & 1 deletion .forge-snapshots/decreaseLiquidity_erc20.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
119430
190044
2 changes: 1 addition & 1 deletion .forge-snapshots/decreaseLiquidity_erc6909.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
117181
168894
2 changes: 1 addition & 1 deletion .forge-snapshots/increaseLiquidity_erc20.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
60600
173212
2 changes: 1 addition & 1 deletion .forge-snapshots/increaseLiquidity_erc6909.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
64370
148794
2 changes: 1 addition & 1 deletion .forge-snapshots/mintWithLiquidity.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
444617
468501
32 changes: 32 additions & 0 deletions contracts/base/BaseLiquidityManagement.sol
Original file line number Diff line number Diff line change
Expand Up @@ -126,13 +126,45 @@ contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallback {
position.liquidity
);

console2.log(callerFeesAccrued.amount0());
console2.log(callerFeesAccrued.amount1());
console2.log("totalFees");
console2.log(totalFeesAccrued.amount0());
console2.log(totalFeesAccrued.amount1());

// Calculate the accurate tokens owed to the caller.
// If the totalFeesAccrued equals the callerFeesAccrued then the total owed to the caller is just the liquidityDelta.
// If the totalFeesAccrued is greater than the callerFeesAccrued, we must account for the difference.
(int128 callerDelta0, int128 callerDelta1) = totalFeesAccrued != callerFeesAccrued
? _calculateCallerDeltas(liquidityDelta, totalFeesAccrued, callerFeesAccrued)
: (liquidityDelta.amount0(), liquidityDelta.amount1());

// An edge case:
// assume alice and bob are on the same range
// and 20 token fee revenue is posted (i.e. swap revenue or donate)

// bob calls collects()
// liquidityDelta: 20
// totalFeesAccrued: 20
// callerFeesAccrued: 5
// (5 tokens sent to bob, and posm caches the 15 tokens for alice)

// assume another 20 token fee revenue is posted (a net new 5 tokens for bob and 15 new tokens for alice)
// alice now has 30 tokens of fee revenue, half of custodied by posm and the other half unclaimed in the PM

// when alice increases her liquidity, using exactly 30 tokens (autocompound):
// liquidityDelta: -10
// totalFeesAccrued: 20 (new fees: 5 for bob, 15 for alice)
// callerFeesAccrued: 30 (alice's fees, per feeGrowthInside)

// naively resolving deltas:
// posm: take 5 tokens (bob's fees), liquidityDelta is now: -15
// posm: pay PM the 15 tokens (alice's cached fees)
// alice: pay nothing, as its a pure and exact autocompound

// to solve:
// we need a way to discern cached fees and use them against liquidityDelta

// Update position storage, sanitizing the tokensOwed and callerDelta values first.
// if callerDelta > 0, then even after re-investing old fees, the caller still has some amount to collect that were not added into the position so they are accounted.
// if callerDelta <= 0, then tokensOwed0 and tokensOwed1 should be zero'd out as all fees were re-invested into a new position.
Expand Down
190 changes: 189 additions & 1 deletion test/position-managers/IncreaseLiquidity.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,67 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers {
token1Owed
);

uint256 balance0BeforeAlice = currency0.balanceOf(alice);
uint256 balance1BeforeAlice = currency1.balanceOf(alice);

vm.prank(alice);
lpm.increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false);

// TODO: assertions, currently increasing liquidity does not perfectly use the fees
// alice did not spend any tokens
assertEq(balance0BeforeAlice, currency0.balanceOf(alice));
assertEq(balance1BeforeAlice, currency1.balanceOf(alice));

// alice spent all of the fees, approximately
(token0Owed, token1Owed) = lpm.feesOwed(tokenIdAlice);
assertApproxEqAbs(token0Owed, 0, 20 wei);
assertApproxEqAbs(token1Owed, 0, 20 wei);
}

// uses donate to simulate fee revenue
function test_increaseLiquidity_withExactFees_donate() public {
// Alice and Bob provide liquidity on the range
// Alice uses her exact fees to increase liquidity (compounding)

uint256 liquidityAlice = 3_000e18;
uint256 liquidityBob = 1_000e18;

// alice provides liquidity
vm.prank(alice);
(uint256 tokenIdAlice,) = lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES);

// bob provides liquidity
vm.prank(bob);
lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES);

// donate to create fees
donateRouter.donate(key, 0.2e18, 0.2e18, ZERO_BYTES);

// alice uses her exact fees to increase liquidity
(uint256 token0Owed, uint256 token1Owed) = lpm.feesOwed(tokenIdAlice);

(uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.poolKey.toId());
uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts(
sqrtPriceX96,
TickMath.getSqrtPriceAtTick(range.tickLower),
TickMath.getSqrtPriceAtTick(range.tickUpper),
token0Owed,
token1Owed
);

uint256 balance0BeforeAlice = currency0.balanceOf(alice);
uint256 balance1BeforeAlice = currency1.balanceOf(alice);

vm.prank(alice);
lpm.increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false);

// alice did not spend any tokens
assertEq(balance0BeforeAlice, currency0.balanceOf(alice));
assertEq(balance1BeforeAlice, currency1.balanceOf(alice));

// alice spent all of the fees
(token0Owed, token1Owed) = lpm.feesOwed(tokenIdAlice);
assertEq(token0Owed, 0);
assertEq(token1Owed, 0);
}

function test_increaseLiquidity_withExcessFees() public {
Expand Down Expand Up @@ -254,4 +311,135 @@ contract IncreaseLiquidityTest is Test, Deployers, GasSnapshot, Fuzzers {
);
}
}

function test_increaseLiquidity_withExactFees_withExactCachedFees() public {
// Alice and Bob provide liquidity on the range
// Alice uses her fees to increase liquidity. Both unclaimed fees and cached fees are used to exactly increase the liquidity
uint256 liquidityAlice = 3_000e18;
uint256 liquidityBob = 1_000e18;
uint256 totalLiquidity = liquidityAlice + liquidityBob;

// alice provides liquidity
vm.prank(alice);
(uint256 tokenIdAlice,) = lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES);

// bob provides liquidity
vm.prank(bob);
(uint256 tokenIdBob,) = lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES);

// 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

(uint256 token0Owed, uint256 token1Owed) = lpm.feesOwed(tokenIdAlice);

// bob collects fees so some of alice's fees are now cached
vm.prank(bob);
lpm.collect(tokenIdBob, bob, ZERO_BYTES, false);

// swap to create more fees
swap(key, true, -int256(swapAmount), ZERO_BYTES);
swap(key, false, -int256(swapAmount), ZERO_BYTES); // move the price back

(uint256 newToken0Owed, uint256 newToken1Owed) = lpm.feesOwed(tokenIdAlice);
// alice's fees should be doubled
assertApproxEqAbs(newToken0Owed, token0Owed * 2, 2 wei);
assertApproxEqAbs(newToken1Owed, token1Owed * 2, 2 wei);

uint256 balance0AliceBefore = currency0.balanceOf(alice);
uint256 balance1AliceBefore = currency1.balanceOf(alice);

// alice will use ALL of her fees to increase liquidity
{
(uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.poolKey.toId());
uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts(
sqrtPriceX96,
TickMath.getSqrtPriceAtTick(range.tickLower),
TickMath.getSqrtPriceAtTick(range.tickUpper),
newToken0Owed,
newToken1Owed
);

vm.prank(alice);
lpm.increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false);
}

// alice did not spend any tokens, approximately
assertApproxEqAbs(balance0AliceBefore, currency0.balanceOf(alice), 0.00001 ether);
assertApproxEqAbs(balance1AliceBefore, currency1.balanceOf(alice), 0.00001 ether);

(token0Owed, token1Owed) = lpm.feesOwed(tokenIdAlice);
assertEq(token0Owed, 0);
assertEq(token1Owed, 0);
}

// uses donate to simulate fee revenue
function test_increaseLiquidity_withExactFees_withExactCachedFees_donate() public {
// Alice and Bob provide liquidity on the range
// Alice uses her fees to increase liquidity. Both unclaimed fees and cached fees are used to exactly increase the liquidity
uint256 liquidityAlice = 3_000e18;
uint256 liquidityBob = 1_000e18;
uint256 totalLiquidity = liquidityAlice + liquidityBob;

// alice provides liquidity
vm.prank(alice);
(uint256 tokenIdAlice,) = lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES);

// bob provides liquidity
vm.prank(bob);
(uint256 tokenIdBob,) = lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES);

// donate to create fees
donateRouter.donate(key, 20e18, 20e18, ZERO_BYTES);

(uint256 token0Owed, uint256 token1Owed) = lpm.feesOwed(tokenIdAlice);

// bob collects fees so some of alice's fees are now cached
vm.prank(bob);
lpm.collect(tokenIdBob, bob, ZERO_BYTES, false);

// donate to create more fees
donateRouter.donate(key, 20e18, 20e18, ZERO_BYTES);

(uint256 newToken0Owed, uint256 newToken1Owed) = lpm.feesOwed(tokenIdAlice);
// alice's fees should be doubled
assertApproxEqAbs(newToken0Owed, token0Owed * 2, 1 wei);
assertApproxEqAbs(newToken1Owed, token1Owed * 2, 1 wei);

uint256 balance0AliceBefore = currency0.balanceOf(alice);
uint256 balance1AliceBefore = currency1.balanceOf(alice);

// alice will use ALL of her fees to increase liquidity
{
(uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.poolKey.toId());
uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts(
sqrtPriceX96,
TickMath.getSqrtPriceAtTick(range.tickLower),
TickMath.getSqrtPriceAtTick(range.tickUpper),
newToken0Owed,
newToken1Owed
);

vm.prank(alice);
lpm.increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false);
}

// alice did not spend any tokens
assertEq(balance0AliceBefore, currency0.balanceOf(alice), "alice spent token0");
assertEq(balance1AliceBefore, currency1.balanceOf(alice), "alice spent token1");

// passes: but WRONG!!!
// assertEq(balance0AliceBefore - currency0.balanceOf(alice), 10e18);
// assertEq(balance1AliceBefore - currency1.balanceOf(alice), 10e18);

(token0Owed, token1Owed) = lpm.feesOwed(tokenIdAlice);
assertEq(token0Owed, 0);
assertEq(token1Owed, 0);

// bob still collects 5
(token0Owed, token1Owed) = lpm.feesOwed(tokenIdBob);
assertApproxEqAbs(token0Owed, 5e18, 1 wei);
assertApproxEqAbs(token1Owed, 5e18, 1 wei);
}
}

0 comments on commit 4d3218e

Please sign in to comment.