Skip to content

Commit

Permalink
change add liq accounting (#126)
Browse files Browse the repository at this point in the history
* change add liq accounting

* remove rand comments

* fix exact fees

* use closeAllDeltas

* comments cleanup

* additional liquidity tests (#129)

* additional increase liquidity tests

* edge case of using cached fees for autocompound

* wip

* fix autocompound bug, use custodied and unclaimed fees in the autocompound

* fix tests and use BalanceDeltas (#130)

* fix some assertions

* use BalanceDeltas for arithmetic

* cleanest code in the game???

* additional cleaning

* typo lol

* autocompound gas benchmarks

* autocompound excess credit gas benchmark

* save 600 gas, cleaner code when moving caller delta to tokensOwed

---------

Co-authored-by: saucepoint <[email protected]>
  • Loading branch information
snreynolds and saucepoint authored Jun 26, 2024
1 parent 2227265 commit 0cff6ef
Show file tree
Hide file tree
Showing 21 changed files with 567 additions and 92 deletions.
1 change: 1 addition & 0 deletions .forge-snapshots/autocompound_exactUnclaimedFees.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
258477
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
190850
1 change: 1 addition & 0 deletions .forge-snapshots/autocompound_excessFeesCredit.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
279016
2 changes: 1 addition & 1 deletion .forge-snapshots/decreaseLiquidity_erc20.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
187556
190026
2 changes: 1 addition & 1 deletion .forge-snapshots/decreaseLiquidity_erc6909.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
166551
168894
2 changes: 1 addition & 1 deletion .forge-snapshots/increaseLiquidity_erc20.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
183251
171241
2 changes: 1 addition & 1 deletion .forge-snapshots/increaseLiquidity_erc6909.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
158833
146823
2 changes: 1 addition & 1 deletion .forge-snapshots/mintWithLiquidity.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
478540
466530
4 changes: 2 additions & 2 deletions contracts/NonfungiblePositionManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit

constructor(IPoolManager _manager)
BaseLiquidityManagement(_manager)
ERC721Permit("Uniswap V4 Positions NFT-V1", "UNI-V3-POS", "1")
ERC721Permit("Uniswap V4 Positions NFT-V1", "UNI-V4-POS", "1")
{}

// NOTE: more gas efficient as LiquidityAmounts is used offchain
Expand All @@ -56,7 +56,7 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit

// NOTE: more expensive since LiquidityAmounts is used onchain
// function mint(MintParams calldata params) external payable returns (uint256 tokenId, BalanceDelta delta) {
// (uint160 sqrtPriceX96,,,) = manager.getSlot0(params.range.key.toId());
// (uint160 sqrtPriceX96,,,) = manager.getSlot0(params.range.poolKey.toId());
// (tokenId, delta) = mint(
// params.range,
// LiquidityAmounts.getLiquidityForAmounts(
Expand Down
181 changes: 130 additions & 51 deletions contracts/base/BaseLiquidityManagement.sol

Large diffs are not rendered by default.

9 changes: 0 additions & 9 deletions contracts/interfaces/IBaseLiquidityManagement.sol
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,6 @@ interface IBaseLiquidityManagement {
COLLECT
}

/// @notice Zero-out outstanding deltas for the PoolManager
/// @dev To be called for batched operations where delta-zeroing happens once at the end of a sequence of operations
/// @param delta The amounts to zero out. Negatives are paid by the sender, positives are collected by the sender
/// @param currency0 The currency of the token0
/// @param currency1 The currency of the token1
/// @param user The user zero'ing the deltas. I.e. negative delta (debit) is paid by the user, positive delta (credit) is collected to the user
/// @param claims Whether deltas are zeroed with ERC-6909 claim tokens
function zeroOut(BalanceDelta delta, Currency currency0, Currency currency1, address user, bool claims) external;

/// @notice Fees owed for a given liquidity position. Includes materialized fees and uncollected fees.
/// @param owner The owner of the liquidity position
/// @param range The range of the liquidity position
Expand Down
53 changes: 53 additions & 0 deletions contracts/libraries/BalanceDeltaExtensionLibrary.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol";

library BalanceDeltaExtensionLibrary {
function setAmount0(BalanceDelta a, int128 amount0) internal pure returns (BalanceDelta) {
assembly {
// set the upper 128 bits of a to amount0
a := or(shl(128, amount0), and(sub(shl(128, 1), 1), a))
}
return a;
}

function setAmount1(BalanceDelta a, int128 amount1) internal pure returns (BalanceDelta) {
assembly {
// set the lower 128 bits of a to amount1
a := or(and(shl(128, sub(shl(128, 1), 1)), a), amount1)
}
return a;
}

function addAmount0(BalanceDelta a, int128 amount0) internal pure returns (BalanceDelta) {
assembly {
let a0 := sar(128, a)
let res0 := add(a0, amount0)
a := or(shl(128, res0), and(sub(shl(128, 1), 1), a))
}
return a;
}

function addAmount1(BalanceDelta a, int128 amount1) internal pure returns (BalanceDelta) {
assembly {
let a1 := signextend(15, a)
let res1 := add(a1, amount1)
a := or(and(shl(128, sub(shl(128, 1), 1)), a), res1)
}
return a;
}

function addAndAssign(BalanceDelta a, BalanceDelta b) internal pure returns (BalanceDelta) {
assembly {
let a0 := sar(128, a)
let a1 := signextend(15, a)
let b0 := sar(128, b)
let b1 := signextend(15, b)
let res0 := add(a0, b0)
let res1 := add(a1, b1)
a := or(shl(128, res0), and(sub(shl(128, 1), 1), res1))
}
return a;
}
}
4 changes: 2 additions & 2 deletions contracts/libraries/CurrencySenderLibrary.sol
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ library CurrencySenderLibrary {
if (useClaims) {
manager.transfer(recipient, currency.toId(), amount);
} else {
currency.settle(manager, address(this), amount, true);
currency.take(manager, recipient, amount, false);
// currency.settle(manager, address(this), amount, true); // sends in tokens into PM from this address
currency.take(manager, recipient, amount, false); // takes out tokens from PM to recipient
}
}
}
8 changes: 5 additions & 3 deletions contracts/libraries/FeeMath.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pragma solidity ^0.8.24;
import {FullMath} from "@uniswap/v4-core/src/libraries/FullMath.sol";
import {FixedPoint128} from "@uniswap/v4-core/src/libraries/FixedPoint128.sol";
import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol";
import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol";

library FeeMath {
using SafeCast for uint256;
Expand All @@ -14,9 +15,10 @@ library FeeMath {
uint256 feeGrowthInside0LastX128,
uint256 feeGrowthInside1LastX128,
uint256 liquidity
) internal pure returns (uint128 token0Owed, uint128 token1Owed) {
token0Owed = getFeeOwed(feeGrowthInside0X128, feeGrowthInside0LastX128, liquidity);
token1Owed = getFeeOwed(feeGrowthInside1X128, feeGrowthInside1LastX128, liquidity);
) internal pure returns (BalanceDelta feesOwed) {
uint128 token0Owed = getFeeOwed(feeGrowthInside0X128, feeGrowthInside0LastX128, liquidity);
uint128 token1Owed = getFeeOwed(feeGrowthInside1X128, feeGrowthInside1LastX128, liquidity);
feesOwed = toBalanceDelta(uint256(token0Owed).toInt128(), uint256(token1Owed).toInt128());
}

function getFeeOwed(uint256 feeGrowthInsideX128, uint256 feeGrowthInsideLastX128, uint256 liquidity)
Expand Down
30 changes: 30 additions & 0 deletions contracts/libraries/Position.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity >=0.8.20;

import {IBaseLiquidityManagement} from "../interfaces/IBaseLiquidityManagement.sol";
import {BalanceDelta} from "v4-core/types/BalanceDelta.sol";

// Updates Position storage
library PositionLibrary {
// TODO ensure this is one sstore.
function addTokensOwed(IBaseLiquidityManagement.Position storage position, BalanceDelta tokensOwed) internal {
position.tokensOwed0 += uint128(tokensOwed.amount0());
position.tokensOwed1 += uint128(tokensOwed.amount1());
}

function addLiquidity(IBaseLiquidityManagement.Position storage position, uint256 liquidity) internal {
unchecked {
position.liquidity += liquidity;
}
}

// TODO ensure this is one sstore.
function updateFeeGrowthInside(
IBaseLiquidityManagement.Position storage position,
uint256 feeGrowthInside0X128,
uint256 feeGrowthInside1X128
) internal {
position.feeGrowthInside0LastX128 = feeGrowthInside0X128;
position.feeGrowthInside1LastX128 = feeGrowthInside1X128;
}
}
2 changes: 1 addition & 1 deletion contracts/types/LiquidityRange.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ pragma solidity ^0.8.24;
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";

struct LiquidityRange {
PoolKey key;
PoolKey poolKey;
int24 tickLower;
int24 tickUpper;
}
Expand Down
8 changes: 4 additions & 4 deletions test/position-managers/FeeCollection.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers {
liquidityDeltaBob = bound(liquidityDeltaBob, 100e18, 100_000e18);

LiquidityRange memory range =
LiquidityRange({key: key, tickLower: params.tickLower, tickUpper: params.tickUpper});
LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper});
vm.prank(alice);
(tokenIdAlice,) = lpm.mint(range, uint256(params.liquidityDelta), block.timestamp + 1, alice, ZERO_BYTES);

Expand Down Expand Up @@ -167,7 +167,7 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers {
liquidityDeltaBob = bound(liquidityDeltaBob, 100e18, 100_000e18);

LiquidityRange memory range =
LiquidityRange({key: key, tickLower: params.tickLower, tickUpper: params.tickUpper});
LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper});
vm.prank(alice);
(tokenIdAlice,) = lpm.mint(range, uint256(params.liquidityDelta), block.timestamp + 1, alice, ZERO_BYTES);

Expand Down Expand Up @@ -229,7 +229,7 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers {
liquidityDeltaBob = bound(liquidityDeltaBob, 100e18, 100_000e18);

LiquidityRange memory range =
LiquidityRange({key: key, tickLower: params.tickLower, tickUpper: params.tickUpper});
LiquidityRange({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper});
vm.prank(alice);
(tokenIdAlice,) = lpm.mint(range, uint256(params.liquidityDelta), block.timestamp + 1, alice, ZERO_BYTES);

Expand Down Expand Up @@ -261,7 +261,7 @@ contract FeeCollectionTest is Test, Deployers, GasSnapshot, LiquidityFuzzers {
/// when alice decreases liquidity, she should only collect her fees
function test_decreaseLiquidity_sameRange_exact() public {
// alice and bob create liquidity on the same range [-120, 120]
LiquidityRange memory range = LiquidityRange({key: key, tickLower: -120, tickUpper: 120});
LiquidityRange memory range = LiquidityRange({poolKey: key, tickLower: -120, tickUpper: 120});

// alice provisions 3x the amount of liquidity as bob
uint256 liquidityAlice = 3000e18;
Expand Down
129 changes: 128 additions & 1 deletion test/position-managers/Gas.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,27 @@ contract GasTest is Test, Deployers, GasSnapshot {
IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max);
IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max);

// Give tokens to Alice and Bob, with approvals
IERC20(Currency.unwrap(currency0)).transfer(alice, STARTING_USER_BALANCE);
IERC20(Currency.unwrap(currency1)).transfer(alice, STARTING_USER_BALANCE);
IERC20(Currency.unwrap(currency0)).transfer(bob, STARTING_USER_BALANCE);
IERC20(Currency.unwrap(currency1)).transfer(bob, STARTING_USER_BALANCE);
vm.startPrank(alice);
IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max);
IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max);
vm.stopPrank();
vm.startPrank(bob);
IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max);
IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max);
vm.stopPrank();

// mint some ERC6909 tokens
claimsRouter.deposit(currency0, address(this), 100_000_000 ether);
claimsRouter.deposit(currency1, address(this), 100_000_000 ether);
manager.setOperator(address(lpm), true);

// define a reusable range
range = LiquidityRange({key: key, tickLower: -300, tickUpper: 300});
range = LiquidityRange({poolKey: key, tickLower: -300, tickUpper: 300});
}

// function test_gas_mint() public {
Expand Down Expand Up @@ -102,6 +116,119 @@ contract GasTest is Test, Deployers, GasSnapshot {
snapLastCall("increaseLiquidity_erc6909");
}

function test_gas_autocompound_exactUnclaimedFees() 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
);

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

function test_gas_autocompound_exactUnclaimedFees_exactCustodiedFees() 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;

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

// 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 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);
snapLastCall("autocompound_exactUnclaimedFees_exactCustodiedFees");
}
}

// autocompounding but the excess fees are credited to tokensOwed
function test_gas_autocompound_excessFeesCredit() public {
// Alice and Bob provide liquidity on the range
// Alice uses her fees to increase liquidity. Excess fees are accounted to alice
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);
(uint256 tokenIdBob,) = lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES);

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

// alice will use half of her 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 / 2,
token1Owed / 2
);

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

function test_gas_decreaseLiquidity_erc20() public {
(uint256 tokenId,) = lpm.mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES);

Expand Down
Loading

0 comments on commit 0cff6ef

Please sign in to comment.