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

change add liq accounting #126

Merged
merged 8 commits into from
Jun 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading