Skip to content

Commit bc17c22

Browse files
authored
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
1 parent 9d6dd49 commit bc17c22

11 files changed

+248
-71
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
258477
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
190850
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
279016
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
176386
1+
171241
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
151968
1+
146823
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
471675
1+
466530

contracts/base/BaseLiquidityManagement.sol

Lines changed: 47 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {FeeMath} from "../libraries/FeeMath.sol";
2323
import {LiquiditySaltLibrary} from "../libraries/LiquiditySaltLibrary.sol";
2424
import {IBaseLiquidityManagement} from "../interfaces/IBaseLiquidityManagement.sol";
2525
import {PositionLibrary} from "../libraries/Position.sol";
26+
import {BalanceDeltaExtensionLibrary} from "../libraries/BalanceDeltaExtensionLibrary.sol";
2627

2728
import "forge-std/console2.sol";
2829

@@ -38,6 +39,7 @@ contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallback {
3839
using SafeCast for uint256;
3940
using LiquiditySaltLibrary for IHooks;
4041
using PositionLibrary for IBaseLiquidityManagement.Position;
42+
using BalanceDeltaExtensionLibrary for BalanceDelta;
4143

4244
mapping(address owner => mapping(LiquidityRangeId rangeId => Position)) public positions;
4345

@@ -106,7 +108,7 @@ contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallback {
106108
LiquidityRange memory range,
107109
uint256 liquidityToAdd,
108110
bytes memory hookData
109-
) internal returns (BalanceDelta, BalanceDelta) {
111+
) internal returns (BalanceDelta callerDelta, BalanceDelta thisDelta) {
110112
// Note that the liquidityDelta includes totalFeesAccrued. The totalFeesAccrued is returned separately for accounting purposes.
111113
(BalanceDelta liquidityDelta, BalanceDelta totalFeesAccrued) =
112114
_modifyLiquidity(owner, range, liquidityToAdd.toInt256(), hookData);
@@ -126,66 +128,37 @@ contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallback {
126128
position.liquidity
127129
);
128130

129-
console2.log(callerFeesAccrued.amount0());
130-
console2.log(callerFeesAccrued.amount1());
131-
console2.log("totalFees");
132-
console2.log(totalFeesAccrued.amount0());
133-
console2.log(totalFeesAccrued.amount1());
131+
if (totalFeesAccrued == callerFeesAccrued) {
132+
// when totalFeesAccrued == callerFeesAccrued, the caller is not sharing the range
133+
// therefore, the caller is responsible for the entire liquidityDelta
134+
callerDelta = liquidityDelta;
135+
} else {
136+
// the delta for increasing liquidity assuming that totalFeesAccrued was not applied
137+
BalanceDelta principalDelta = liquidityDelta - totalFeesAccrued;
138+
139+
// outstanding deltas the caller is responsible for, after their fees are credited to the principal delta
140+
callerDelta = principalDelta + callerFeesAccrued;
134141

135-
// Calculate the accurate tokens owed to the caller.
136-
// If the totalFeesAccrued equals the callerFeesAccrued then the total owed to the caller is just the liquidityDelta.
137-
// If the totalFeesAccrued is greater than the callerFeesAccrued, we must account for the difference.
138-
// TODO: If totalFeesAccrued == callerFeesAccrued, I think we can just apply the entire delta onto the caller, even if this implicitly collects on behalf of another user in the same range.
139-
(int128 callerDelta0, int128 callerDelta1) = totalFeesAccrued != callerFeesAccrued
140-
? _calculateCallerDeltas(liquidityDelta, totalFeesAccrued, callerFeesAccrued)
141-
: (liquidityDelta.amount0(), liquidityDelta.amount1());
142+
// outstanding deltas this contract is responsible for, intuitively the contract is responsible for taking fees external to the caller's accrued fees
143+
thisDelta = totalFeesAccrued - callerFeesAccrued;
144+
}
142145

143146
// Update position storage, flushing the callerDelta value to tokensOwed first if necessary.
144147
// If callerDelta > 0, then even after investing callerFeesAccrued, the caller still has some amount to collect that were not added into the position so they are accounted to tokensOwed and removed from the final callerDelta returned.
145-
uint128 tokensOwed0 = 0;
146-
uint128 tokensOwed1 = 0;
147-
(tokensOwed0, callerDelta0) = callerDelta0 > 0 ? (uint128(callerDelta0), int128(0)) : (uint128(0), callerDelta0);
148-
(tokensOwed1, callerDelta1) = callerDelta1 > 0 ? (uint128(callerDelta1), int128(0)) : (uint128(0), callerDelta1);
148+
BalanceDelta tokensOwed;
149+
if (callerDelta.amount0() > 0) {
150+
(tokensOwed, callerDelta, thisDelta) =
151+
_moveCallerDeltaToTokensOwed(true, tokensOwed, callerDelta, thisDelta);
152+
}
153+
154+
if (callerDelta.amount1() > 0) {
155+
(tokensOwed, callerDelta, thisDelta) =
156+
_moveCallerDeltaToTokensOwed(false, tokensOwed, callerDelta, thisDelta);
157+
}
149158

150-
position.addTokensOwed(tokensOwed0, tokensOwed1);
159+
position.addTokensOwed(tokensOwed);
151160
position.addLiquidity(liquidityToAdd);
152161
position.updateFeeGrowthInside(feeGrowthInside0X128, feeGrowthInside1X128);
153-
154-
// The delta owed or credited by this contract.
155-
// TODO @sauce check that if callerDelta == 0 (zerod out from above), then this line just credits the posm to takes on behalf of the caller
156-
int128 thisDelta0 = liquidityDelta.amount0() - callerDelta0;
157-
int128 thisDelta1 = liquidityDelta.amount1() - callerDelta1;
158-
159-
return (toBalanceDelta(callerDelta0, callerDelta1), toBalanceDelta(thisDelta0, thisDelta1));
160-
}
161-
162-
// Returns the delta paid/credited by/to the caller.
163-
function _calculateCallerDeltas(
164-
BalanceDelta liquidityDelta,
165-
BalanceDelta totalFeesAccrued,
166-
BalanceDelta callerFeesAccrued
167-
) private pure returns (int128 callerDelta0, int128 callerDelta1) {
168-
(int128 liquidityDelta0, int128 liquidityDelta1) = (liquidityDelta.amount0(), liquidityDelta.amount1());
169-
(int128 totalFeesAccrued0, int128 totalFeesAccrued1) = (totalFeesAccrued.amount0(), totalFeesAccrued.amount1());
170-
(int128 callerFeesAccrued0, int128 callerFeesAccrued1) =
171-
(callerFeesAccrued.amount0(), callerFeesAccrued.amount1());
172-
173-
callerDelta0 = _calculateCallerDelta(liquidityDelta0, totalFeesAccrued0, callerFeesAccrued0);
174-
callerDelta1 = _calculateCallerDelta(liquidityDelta1, totalFeesAccrued1, callerFeesAccrued1);
175-
}
176-
177-
function _calculateCallerDelta(int128 liquidityDelta, int128 totalFeesAccrued, int128 callerFeesAccrued)
178-
private
179-
pure
180-
returns (int128 callerDelta)
181-
{
182-
unchecked {
183-
// The principle delta owed/debited to the caller before any LP fees are deducted.
184-
int128 principleDelta = liquidityDelta - totalFeesAccrued;
185-
// The new caller delta is this principle delta plus the callerFeesAccrued which consists of
186-
// the custodied fees by posm and unclaimed fees from the modifyLiq call.
187-
callerDelta = principleDelta + callerFeesAccrued;
188-
}
189162
}
190163

191164
function _increaseLiquidityAndZeroOut(
@@ -230,6 +203,26 @@ contract BaseLiquidityManagement is IBaseLiquidityManagement, SafeCallback {
230203
if (delta1 < 0) currency1.settle(manager, address(this), uint256(int256(-delta1)), true);
231204
}
232205

206+
function _moveCallerDeltaToTokensOwed(
207+
bool useAmount0,
208+
BalanceDelta tokensOwed,
209+
BalanceDelta callerDelta,
210+
BalanceDelta thisDelta
211+
) private returns (BalanceDelta, BalanceDelta, BalanceDelta) {
212+
// credit the excess tokens to the position's tokensOwed
213+
tokensOwed =
214+
useAmount0 ? tokensOwed.setAmount0(callerDelta.amount0()) : tokensOwed.setAmount1(callerDelta.amount1());
215+
216+
// this contract is responsible for custodying the excess tokens
217+
thisDelta =
218+
useAmount0 ? thisDelta.addAmount0(callerDelta.amount0()) : thisDelta.addAmount1(callerDelta.amount1());
219+
220+
// the caller is not expected to collect the excess tokens
221+
callerDelta = useAmount0 ? callerDelta.setAmount0(0) : callerDelta.setAmount1(0);
222+
223+
return (tokensOwed, callerDelta, thisDelta);
224+
}
225+
233226
function _lockAndIncreaseLiquidity(
234227
address owner,
235228
LiquidityRange memory range,
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.20;
3+
4+
import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
5+
6+
library BalanceDeltaExtensionLibrary {
7+
function setAmount0(BalanceDelta a, int128 amount0) internal pure returns (BalanceDelta) {
8+
assembly {
9+
// set the upper 128 bits of a to amount0
10+
a := or(shl(128, amount0), and(sub(shl(128, 1), 1), a))
11+
}
12+
return a;
13+
}
14+
15+
function setAmount1(BalanceDelta a, int128 amount1) internal pure returns (BalanceDelta) {
16+
assembly {
17+
// set the lower 128 bits of a to amount1
18+
a := or(and(shl(128, sub(shl(128, 1), 1)), a), amount1)
19+
}
20+
return a;
21+
}
22+
23+
function addAmount0(BalanceDelta a, int128 amount0) internal pure returns (BalanceDelta) {
24+
assembly {
25+
let a0 := sar(128, a)
26+
let res0 := add(a0, amount0)
27+
a := or(shl(128, res0), and(sub(shl(128, 1), 1), a))
28+
}
29+
return a;
30+
}
31+
32+
function addAmount1(BalanceDelta a, int128 amount1) internal pure returns (BalanceDelta) {
33+
assembly {
34+
let a1 := signextend(15, a)
35+
let res1 := add(a1, amount1)
36+
a := or(and(shl(128, sub(shl(128, 1), 1)), a), res1)
37+
}
38+
return a;
39+
}
40+
41+
function addAndAssign(BalanceDelta a, BalanceDelta b) internal pure returns (BalanceDelta) {
42+
assembly {
43+
let a0 := sar(128, a)
44+
let a1 := signextend(15, a)
45+
let b0 := sar(128, b)
46+
let b1 := signextend(15, b)
47+
let res0 := add(a0, b0)
48+
let res1 := add(a1, b1)
49+
a := or(shl(128, res0), and(sub(shl(128, 1), 1), res1))
50+
}
51+
return a;
52+
}
53+
}

contracts/libraries/Position.sol

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,14 @@
22
pragma solidity >=0.8.20;
33

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

67
// Updates Position storage
78
library PositionLibrary {
89
// TODO ensure this is one sstore.
9-
function addTokensOwed(IBaseLiquidityManagement.Position storage position, uint128 tokensOwed0, uint128 tokensOwed1)
10-
internal
11-
{
12-
position.tokensOwed0 += tokensOwed0;
13-
position.tokensOwed1 += tokensOwed1;
10+
function addTokensOwed(IBaseLiquidityManagement.Position storage position, BalanceDelta tokensOwed) internal {
11+
position.tokensOwed0 += uint128(tokensOwed.amount0());
12+
position.tokensOwed1 += uint128(tokensOwed.amount1());
1413
}
1514

1615
function addLiquidity(IBaseLiquidityManagement.Position storage position, uint256 liquidity) internal {

test/position-managers/Gas.t.sol

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,20 @@ contract GasTest is Test, Deployers, GasSnapshot {
5656
IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max);
5757
IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max);
5858

59+
// Give tokens to Alice and Bob, with approvals
60+
IERC20(Currency.unwrap(currency0)).transfer(alice, STARTING_USER_BALANCE);
61+
IERC20(Currency.unwrap(currency1)).transfer(alice, STARTING_USER_BALANCE);
62+
IERC20(Currency.unwrap(currency0)).transfer(bob, STARTING_USER_BALANCE);
63+
IERC20(Currency.unwrap(currency1)).transfer(bob, STARTING_USER_BALANCE);
64+
vm.startPrank(alice);
65+
IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max);
66+
IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max);
67+
vm.stopPrank();
68+
vm.startPrank(bob);
69+
IERC20(Currency.unwrap(currency0)).approve(address(lpm), type(uint256).max);
70+
IERC20(Currency.unwrap(currency1)).approve(address(lpm), type(uint256).max);
71+
vm.stopPrank();
72+
5973
// mint some ERC6909 tokens
6074
claimsRouter.deposit(currency0, address(this), 100_000_000 ether);
6175
claimsRouter.deposit(currency1, address(this), 100_000_000 ether);
@@ -102,6 +116,119 @@ contract GasTest is Test, Deployers, GasSnapshot {
102116
snapLastCall("increaseLiquidity_erc6909");
103117
}
104118

119+
function test_gas_autocompound_exactUnclaimedFees() public {
120+
// Alice and Bob provide liquidity on the range
121+
// Alice uses her exact fees to increase liquidity (compounding)
122+
123+
uint256 liquidityAlice = 3_000e18;
124+
uint256 liquidityBob = 1_000e18;
125+
126+
// alice provides liquidity
127+
vm.prank(alice);
128+
(uint256 tokenIdAlice,) = lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES);
129+
130+
// bob provides liquidity
131+
vm.prank(bob);
132+
lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES);
133+
134+
// donate to create fees
135+
donateRouter.donate(key, 0.2e18, 0.2e18, ZERO_BYTES);
136+
137+
// alice uses her exact fees to increase liquidity
138+
(uint256 token0Owed, uint256 token1Owed) = lpm.feesOwed(tokenIdAlice);
139+
140+
(uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.poolKey.toId());
141+
uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts(
142+
sqrtPriceX96,
143+
TickMath.getSqrtPriceAtTick(range.tickLower),
144+
TickMath.getSqrtPriceAtTick(range.tickUpper),
145+
token0Owed,
146+
token1Owed
147+
);
148+
149+
vm.prank(alice);
150+
lpm.increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false);
151+
snapLastCall("autocompound_exactUnclaimedFees");
152+
}
153+
154+
function test_gas_autocompound_exactUnclaimedFees_exactCustodiedFees() public {
155+
// Alice and Bob provide liquidity on the range
156+
// Alice uses her fees to increase liquidity. Both unclaimed fees and cached fees are used to exactly increase the liquidity
157+
uint256 liquidityAlice = 3_000e18;
158+
uint256 liquidityBob = 1_000e18;
159+
160+
// alice provides liquidity
161+
vm.prank(alice);
162+
(uint256 tokenIdAlice,) = lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES);
163+
164+
// bob provides liquidity
165+
vm.prank(bob);
166+
(uint256 tokenIdBob,) = lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES);
167+
168+
// donate to create fees
169+
donateRouter.donate(key, 20e18, 20e18, ZERO_BYTES);
170+
171+
// bob collects fees so some of alice's fees are now cached
172+
vm.prank(bob);
173+
lpm.collect(tokenIdBob, bob, ZERO_BYTES, false);
174+
175+
// donate to create more fees
176+
donateRouter.donate(key, 20e18, 20e18, ZERO_BYTES);
177+
178+
(uint256 newToken0Owed, uint256 newToken1Owed) = lpm.feesOwed(tokenIdAlice);
179+
180+
// alice will use ALL of her fees to increase liquidity
181+
{
182+
(uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.poolKey.toId());
183+
uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts(
184+
sqrtPriceX96,
185+
TickMath.getSqrtPriceAtTick(range.tickLower),
186+
TickMath.getSqrtPriceAtTick(range.tickUpper),
187+
newToken0Owed,
188+
newToken1Owed
189+
);
190+
191+
vm.prank(alice);
192+
lpm.increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false);
193+
snapLastCall("autocompound_exactUnclaimedFees_exactCustodiedFees");
194+
}
195+
}
196+
197+
// autocompounding but the excess fees are credited to tokensOwed
198+
function test_gas_autocompound_excessFeesCredit() public {
199+
// Alice and Bob provide liquidity on the range
200+
// Alice uses her fees to increase liquidity. Excess fees are accounted to alice
201+
uint256 liquidityAlice = 3_000e18;
202+
uint256 liquidityBob = 1_000e18;
203+
204+
// alice provides liquidity
205+
vm.prank(alice);
206+
(uint256 tokenIdAlice,) = lpm.mint(range, liquidityAlice, block.timestamp + 1, alice, ZERO_BYTES);
207+
208+
// bob provides liquidity
209+
vm.prank(bob);
210+
(uint256 tokenIdBob,) = lpm.mint(range, liquidityBob, block.timestamp + 1, bob, ZERO_BYTES);
211+
212+
// donate to create fees
213+
donateRouter.donate(key, 20e18, 20e18, ZERO_BYTES);
214+
215+
// alice will use half of her fees to increase liquidity
216+
(uint256 token0Owed, uint256 token1Owed) = lpm.feesOwed(tokenIdAlice);
217+
218+
(uint160 sqrtPriceX96,,,) = StateLibrary.getSlot0(manager, range.poolKey.toId());
219+
uint256 liquidityDelta = LiquidityAmounts.getLiquidityForAmounts(
220+
sqrtPriceX96,
221+
TickMath.getSqrtPriceAtTick(range.tickLower),
222+
TickMath.getSqrtPriceAtTick(range.tickUpper),
223+
token0Owed / 2,
224+
token1Owed / 2
225+
);
226+
227+
vm.prank(alice);
228+
lpm.increaseLiquidity(tokenIdAlice, liquidityDelta, ZERO_BYTES, false);
229+
snapLastCall("autocompound_excessFeesCredit");
230+
}
231+
105232
function test_gas_decreaseLiquidity_erc20() public {
106233
(uint256 tokenId,) = lpm.mint(range, 10_000 ether, block.timestamp + 1, address(this), ZERO_BYTES);
107234

0 commit comments

Comments
 (0)