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

Position Manager w/ UR-invokable delta resolving #124

Closed
wants to merge 59 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
6c63e1f
initial thoughts lock and batch
snreynolds Dec 5, 2023
ad39d19
update safecallback with constructor
snreynolds Dec 5, 2023
64fc40a
simple batch under lock
snreynolds Dec 6, 2023
a707c20
oops
snreynolds Dec 6, 2023
1d0e566
misc version bump; will conflict but can resolve later
saucepoint Feb 23, 2024
4f8bbd2
defining types and different levels of abstractions
saucepoint Feb 23, 2024
c4c9dcd
merge in main; resolve conflicts
saucepoint Mar 1, 2024
187fc3d
merge in main?
saucepoint Mar 1, 2024
ee27f4f
wip
saucepoint Mar 2, 2024
30ac060
merge in main
saucepoint Mar 2, 2024
d1c5897
misc fixes with main:latest
saucepoint Mar 2, 2024
7a134a5
basic mint
saucepoint Mar 3, 2024
7bd2996
begin moving tests to fuzz
saucepoint Mar 4, 2024
307f4bb
test for slippage
saucepoint Mar 5, 2024
109caf4
burning
saucepoint Mar 6, 2024
1bf080f
decrease liquidity
saucepoint Mar 6, 2024
40f042c
mint transfer burn, liquidityOf accounting
saucepoint Mar 7, 2024
6b1c7cb
wip
saucepoint Mar 8, 2024
fa511d0
refactor to use CurrencySettleTake
saucepoint Mar 12, 2024
a0e0a44
basic fee collection
saucepoint Mar 12, 2024
0d936d4
wip
saucepoint Mar 16, 2024
4be3c2a
misc fix
saucepoint Mar 19, 2024
7fa4c54
fee collection for independent same-range parties
saucepoint Mar 19, 2024
aae9697
LiquidityPosition -> LiquidityRange
saucepoint Mar 19, 2024
5dec534
erc20 fee collection
saucepoint Mar 19, 2024
1196c6a
decrease liquidity with fee collection
saucepoint Mar 25, 2024
3d317e8
wip test decrease liquidity on same range
saucepoint Mar 26, 2024
31a70cb
reworked fuzzers; more testing on fee claims for liquidity decreasing
saucepoint Mar 26, 2024
f844687
merge in main; update flipped deltas; update lockAcquired signature
saucepoint Mar 27, 2024
666faf8
forge fmt
saucepoint Mar 27, 2024
714913e
Merge remote-tracking branch 'parent/lock-and-batch-call' into positi…
saucepoint Mar 27, 2024
ddf5771
Merge branch 'position-manager' of github.com:saucepoint/v4-periphery…
saucepoint Mar 27, 2024
3c56d48
test fixes for flipped deltas
saucepoint Mar 27, 2024
f4275cc
wip
saucepoint Apr 3, 2024
245cc3e
test coverage for increase liquidity cases
saucepoint Apr 5, 2024
f971b3d
preliminary gas benchmarks
saucepoint Apr 5, 2024
0f9936e
merge in main; fix breaking changes
saucepoint Jun 3, 2024
41619f9
merge in changes
saucepoint Jun 3, 2024
0165be5
Position manager refactor (#2)
saucepoint Jun 7, 2024
d86a8c2
fix conflicts
saucepoint Jun 9, 2024
52b304e
cleanup: TODOs and imports
saucepoint Jun 12, 2024
af67661
Position manager Consolidate (#3)
saucepoint Jun 12, 2024
48f38c4
use currency settler syntax
saucepoint Jun 12, 2024
c8ce67b
use v4-core's gas snapshot
saucepoint Jun 13, 2024
da91136
use snapLastCall and isolate for posm benchmarks
saucepoint Jun 13, 2024
18600bd
Update contracts/libraries/CurrencySettleTake.sol
saucepoint Jun 14, 2024
f52adcf
use v4-core's solmate its more recent
saucepoint Jun 14, 2024
07cc628
use v4-core's openzeppelin-contracts
saucepoint Jun 17, 2024
240c8e1
add ERC721Permit
saucepoint Jun 17, 2024
1cb1948
feedback: memory hookData
saucepoint Jun 17, 2024
227683b
initial refactor. stack too deep
saucepoint Jun 19, 2024
a19636f
passing tests
saucepoint Jun 19, 2024
ea56ec2
merge in main
saucepoint Jun 19, 2024
fc04651
gutted LockAndBatchCall
saucepoint Jun 19, 2024
e1d55f8
cleanup diff
saucepoint Jun 19, 2024
b73a240
renaming vanilla functions
saucepoint Jun 19, 2024
2227265
sanitize
saucepoint Jun 20, 2024
0cff6ef
change add liq accounting (#126)
snreynolds Jun 26, 2024
0d6ab0b
update decrease (#133)
snreynolds Jun 28, 2024
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
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 @@
1015181
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
1 change: 1 addition & 0 deletions .forge-snapshots/autocompound_exactUnclaimedFees.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
258575
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
190948
1 change: 1 addition & 0 deletions .forge-snapshots/autocompound_excessFeesCredit.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
279114
1 change: 1 addition & 0 deletions .forge-snapshots/decreaseLiquidity_erc20.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
177014
1 change: 1 addition & 0 deletions .forge-snapshots/decreaseLiquidity_erc6909.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
177026
1 change: 1 addition & 0 deletions .forge-snapshots/increaseLiquidity_erc20.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
171339
1 change: 1 addition & 0 deletions .forge-snapshots/increaseLiquidity_erc6909.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
146921
1 change: 1 addition & 0 deletions .forge-snapshots/mint.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
422785
1 change: 1 addition & 0 deletions .forge-snapshots/mintWithLiquidity.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
466628
12 changes: 0 additions & 12 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,15 +1,3 @@
[submodule "lib/forge-std"]
path = lib/forge-std
url = https://github.com/foundry-rs/forge-std
[submodule "lib/openzeppelin-contracts"]
path = lib/openzeppelin-contracts
url = https://github.com/OpenZeppelin/openzeppelin-contracts
[submodule "lib/forge-gas-snapshot"]
path = lib/forge-gas-snapshot
url = https://github.com/marktoda/forge-gas-snapshot
[submodule "lib/v4-core"]
path = lib/v4-core
url = https://github.com/Uniswap/v4-core
[submodule "lib/solmate"]
path = lib/solmate
url = https://github.com/transmissions11/solmate
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ contract CoolHook is BaseHook {
address,
IPoolManager.PoolKey calldata key,
IPoolManager.ModifyLiquidityParams calldata params
) external override poolManagerOnly returns (bytes4) {
) external override onlyByManager returns (bytes4) {
// hook logic
return BaseHook.beforeAddLiquidity.selector;
}
Expand Down
156 changes: 156 additions & 0 deletions contracts/NonfungiblePositionManager.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.24;

import {ERC721Permit} from "./base/ERC721Permit.sol";
import {INonfungiblePositionManager} from "./interfaces/INonfungiblePositionManager.sol";
import {BaseLiquidityManagement} from "./base/BaseLiquidityManagement.sol";

import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
import {PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol";
import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol";
import {CurrencySettleTake} from "./libraries/CurrencySettleTake.sol";
import {LiquidityRange, LiquidityRangeId, LiquidityRangeIdLibrary} from "./types/LiquidityRange.sol";
import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol";

import {LiquidityAmounts} from "./libraries/LiquidityAmounts.sol";
import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol";
import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol";
import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol";

contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidityManagement, ERC721Permit {
using CurrencyLibrary for Currency;
using CurrencySettleTake for Currency;
using PoolIdLibrary for PoolKey;
using LiquidityRangeIdLibrary for LiquidityRange;
using StateLibrary for IPoolManager;
using SafeCast for uint256;

/// @dev The ID of the next token that will be minted. Skips 0
uint256 private _nextId = 1;

// maps the ERC721 tokenId to the keys that uniquely identify a liquidity position (owner, range)
mapping(uint256 tokenId => TokenPosition position) public tokenPositions;

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

// NOTE: more gas efficient as LiquidityAmounts is used offchain
// TODO: deadline check
function mint(
LiquidityRange calldata range,
uint256 liquidity,
uint256 deadline,
address recipient,
bytes calldata hookData
) public payable returns (uint256 tokenId, BalanceDelta delta) {
// delta = modifyLiquidity(range, liquidity.toInt256(), hookData, false);
delta = _lockAndIncreaseLiquidity(msg.sender, range, liquidity, hookData, false);

// mint receipt token
_mint(recipient, (tokenId = _nextId++));
tokenPositions[tokenId] = TokenPosition({owner: msg.sender, range: range});
}

// 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.poolKey.toId());
// (tokenId, delta) = mint(
// params.range,
// LiquidityAmounts.getLiquidityForAmounts(
// sqrtPriceX96,
// TickMath.getSqrtPriceAtTick(params.range.tickLower),
// TickMath.getSqrtPriceAtTick(params.range.tickUpper),
// params.amount0Desired,
// params.amount1Desired
// ),
// params.deadline,
// params.recipient,
// params.hookData
// );
// require(params.amount0Min <= uint256(uint128(delta.amount0())), "INSUFFICIENT_AMOUNT0");
// require(params.amount1Min <= uint256(uint128(delta.amount1())), "INSUFFICIENT_AMOUNT1");
// }

function increaseLiquidity(uint256 tokenId, uint256 liquidity, bytes calldata hookData, bool claims)
external
isAuthorizedForToken(tokenId)
returns (BalanceDelta delta)
{
TokenPosition memory tokenPos = tokenPositions[tokenId];
delta = _lockAndIncreaseLiquidity(tokenPos.owner, tokenPos.range, liquidity, hookData, claims);
}

function decreaseLiquidity(uint256 tokenId, uint256 liquidity, bytes calldata hookData, bool claims)
public
isAuthorizedForToken(tokenId)
returns (BalanceDelta delta, BalanceDelta thisDelta)
{
TokenPosition memory tokenPos = tokenPositions[tokenId];
(delta, thisDelta) = _lockAndDecreaseLiquidity(tokenPos.owner, tokenPos.range, liquidity, hookData, claims);
}

function burn(uint256 tokenId, address recipient, bytes calldata hookData, bool claims)
external
isAuthorizedForToken(tokenId)
returns (BalanceDelta delta)
{
// TODO: Burn currently decreases and collects. However its done under different locks.
// Replace once we have the execute multicall.
// remove liquidity
TokenPosition storage tokenPosition = tokenPositions[tokenId];
LiquidityRangeId rangeId = tokenPosition.range.toId();
Position storage position = positions[msg.sender][rangeId];
if (position.liquidity > 0) {
(delta,) = decreaseLiquidity(tokenId, position.liquidity, hookData, claims);
}

collect(tokenId, recipient, hookData, claims);
require(position.tokensOwed0 == 0 && position.tokensOwed1 == 0, "NOT_EMPTY");
delete positions[msg.sender][rangeId];
delete tokenPositions[tokenId];

// burn the token
_burn(tokenId);
}

// TODO: in v3, we can partially collect fees, but what was the usecase here?
function collect(uint256 tokenId, address recipient, bytes calldata hookData, bool claims)
public
returns (BalanceDelta delta)
{
TokenPosition memory tokenPos = tokenPositions[tokenId];
delta = _lockAndCollect(tokenPos.owner, tokenPos.range, hookData, claims);
}

function feesOwed(uint256 tokenId) external view returns (uint256 token0Owed, uint256 token1Owed) {
TokenPosition memory tokenPosition = tokenPositions[tokenId];
return feesOwed(tokenPosition.owner, tokenPosition.range);
}

function _beforeTokenTransfer(address from, address to, uint256 tokenId) internal override {
TokenPosition storage tokenPosition = tokenPositions[tokenId];
LiquidityRangeId rangeId = tokenPosition.range.toId();
Position storage position = positions[from][rangeId];
position.operator = address(0x0);

// transfer position data to destination
positions[to][rangeId] = position;
delete positions[from][rangeId];

// update token position
tokenPositions[tokenId] = TokenPosition({owner: to, range: tokenPosition.range});
}

function _getAndIncrementNonce(uint256 tokenId) internal override returns (uint256) {
TokenPosition memory tokenPosition = tokenPositions[tokenId];
return uint256(positions[tokenPosition.owner][tokenPosition.range.toId()].nonce++);
}

modifier isAuthorizedForToken(uint256 tokenId) {
require(_isApprovedOrOwner(msg.sender, tokenId), "Not approved");
_;
}
}
Loading
Loading