-
Notifications
You must be signed in to change notification settings - Fork 504
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
Add v4 NonfungiblePositionManager skeleton #76
Changes from all commits
fe9305e
0b01a0a
74ad1a2
30479c6
349ddb8
906fb65
0152e5c
e5fc9dd
54d55fd
a2200ac
b4f84ee
ad5c503
04a7a1d
f9d830e
0d87708
02a5cd2
d261d58
7f544b1
7cc8c66
c52f05a
05c9543
ca01507
4a7d82a
6e82a2e
3166045
ad34755
aad107a
87e0b2f
c64e82e
b72452b
5d8a888
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,201 @@ | ||
// SPDX-License-Identifier: UNLICENSED | ||
pragma solidity ^0.8.19; | ||
|
||
import {IERC721Metadata} from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; | ||
import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; | ||
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; | ||
import {PoolKey} from "@uniswap/v4-core/contracts/types/PoolKey.sol"; | ||
import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/contracts/types/PoolId.sol"; | ||
import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; | ||
import {Currency} from "@uniswap/v4-core/contracts/types/Currency.sol"; | ||
|
||
import {INonfungiblePositionManagerV4} from "./interfaces/INonfungiblePositionManagerV4.sol"; | ||
import {PeripheryValidation} from "./base/PeripheryValidation.sol"; | ||
import {PeripheryPayments} from "./base/PeripheryPayments.sol"; | ||
import {PeripheryImmutableState} from "./base/PeripheryImmutableState.sol"; | ||
import {SelfPermit} from "./base/SelfPermit.sol"; | ||
import {LiquidityManagement} from "./base/LiquidityManagement.sol"; | ||
import {Multicall} from "./base/Multicall.sol"; | ||
|
||
contract NonfungiblePositionManagerV4 is | ||
INonfungiblePositionManagerV4, | ||
ERC721, | ||
PeripheryImmutableState, | ||
PeripheryValidation, | ||
LiquidityManagement, | ||
SelfPermit, | ||
Multicall | ||
{ | ||
using PoolIdLibrary for PoolKey; | ||
|
||
error InvalidTokenID(); | ||
error NotApproved(); | ||
error NotCleared(); | ||
error NonexistentToken(); | ||
|
||
// details about the Uniswap position | ||
struct TokenPosition { | ||
// the nonce for permits | ||
uint96 nonce; | ||
// the address that is approved for spending this token | ||
address operator; | ||
// the hashed poolKey of the pool with which this token is connected | ||
PoolId poolId; | ||
// the tick range of the position | ||
int24 tickLower; | ||
int24 tickUpper; | ||
// the liquidity of the position | ||
uint128 liquidity; | ||
// the fee growth of the aggregate position as of the last action on the individual position | ||
uint256 feeGrowthInside0LastX128; | ||
uint256 feeGrowthInside1LastX128; | ||
// how many uncollected tokens are owed to the position, as of the last computation | ||
uint128 tokensOwed0; | ||
uint128 tokensOwed1; | ||
} | ||
|
||
/// @dev Pool keys by poolIds | ||
mapping(bytes32 => PoolKey) private _poolIdToPoolKey; | ||
|
||
/// @dev The token ID position data | ||
mapping(uint256 => TokenPosition) private _positions; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is there a reason for this to be private? im down for this to be public i can imagine other contracts or interfaces wanting to look up position info There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. thinking they probably want to use the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. oh yeah good point There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: Would just add to the natspec that they key is specifically the tokenId (tokenId) ie mapping from tokenId to position data |
||
|
||
/// @dev The ID of the next token that will be minted. Skips 0 | ||
uint176 private _nextId = 1; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we call this _nextTokenId? We have tokenIds and poolIds so just want to be clear when we have id var names |
||
|
||
/// @dev The address of the token descriptor contract, which handles generating token URIs for position tokens | ||
address private immutable _tokenDescriptor; | ||
|
||
constructor(IPoolManager _poolManager, address _tokenDescriptor_) | ||
PeripheryImmutableState(_poolManager) | ||
ERC721("Uniswap V4 Positions NFT-V1", "UNI-V4-POS") | ||
{ | ||
_tokenDescriptor = _tokenDescriptor_; | ||
} | ||
|
||
/// @inheritdoc INonfungiblePositionManagerV4 | ||
function positions(uint256 tokenId) | ||
external | ||
view | ||
override | ||
returns ( | ||
uint96 nonce, | ||
address operator, | ||
Currency currency0, | ||
Currency currency1, | ||
uint24 fee, | ||
int24 tickLower, | ||
int24 tickUpper, | ||
uint128 liquidity, | ||
uint256 feeGrowthInside0LastX128, | ||
uint256 feeGrowthInside1LastX128, | ||
uint128 tokensOwed0, | ||
uint128 tokensOwed1 | ||
) | ||
{ | ||
TokenPosition memory position = _positions[tokenId]; | ||
if (PoolId.unwrap(position.poolId) == 0) revert InvalidTokenID(); | ||
PoolKey memory poolKey = _poolIdToPoolKey[PoolId.unwrap(position.poolId)]; | ||
return ( | ||
position.nonce, | ||
position.operator, | ||
poolKey.currency0, | ||
poolKey.currency1, | ||
poolKey.fee, | ||
position.tickLower, | ||
position.tickUpper, | ||
position.liquidity, | ||
position.feeGrowthInside0LastX128, | ||
position.feeGrowthInside1LastX128, | ||
position.tokensOwed0, | ||
position.tokensOwed1 | ||
); | ||
} | ||
|
||
/// @inheritdoc INonfungiblePositionManagerV4 | ||
function mint(MintParams calldata params) | ||
external | ||
payable | ||
override | ||
checkDeadline(params.deadline) | ||
returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1) | ||
{ | ||
// TODO: implement this | ||
} | ||
|
||
/// @inheritdoc INonfungiblePositionManagerV4 | ||
function increaseLiquidity(IncreaseLiquidityParams calldata params) | ||
external | ||
payable | ||
override | ||
checkDeadline(params.deadline) | ||
returns (uint128 liquidity, uint256 amount0, uint256 amount1) | ||
{ | ||
// TODO: implement this | ||
} | ||
|
||
/// @inheritdoc INonfungiblePositionManagerV4 | ||
function decreaseLiquidity(DecreaseLiquidityParams calldata params) | ||
external | ||
payable | ||
override | ||
isAuthorizedForToken(params.tokenId) | ||
checkDeadline(params.deadline) | ||
returns (uint256 amount0, uint256 amount1) | ||
{ | ||
// TODO: implement this | ||
} | ||
|
||
/// @inheritdoc INonfungiblePositionManagerV4 | ||
function collect(CollectParams calldata params) | ||
external | ||
payable | ||
override | ||
isAuthorizedForToken(params.tokenId) | ||
returns (uint256 amount0, uint256 amount1) | ||
{ | ||
// TODO: implement this | ||
} | ||
|
||
/// @inheritdoc INonfungiblePositionManagerV4 | ||
function burn(uint256 tokenId) external payable override isAuthorizedForToken(tokenId) { | ||
TokenPosition storage position = _positions[tokenId]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the checks below will all be sloads. I think you can copy to memory, do the checks (mloads) below and then delete the storage var with delete _positions[tokenId]; |
||
if (position.liquidity != 0 || position.tokensOwed0 != 0 || position.tokensOwed1 != 0) revert NotCleared(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do we need to make sure theyve collected all their rewards before they can burn? or does this already check that? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ok ive dug into it and these checks should be enough for burn |
||
delete _positions[tokenId]; | ||
_burn(tokenId); | ||
} | ||
|
||
modifier isAuthorizedForToken(uint256 tokenId) { | ||
if (!_isApprovedOrOwner(msg.sender, tokenId)) revert NotApproved(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hm.. I think we are not using the most up to date erc721 contract from in openzeppelin? Also I prefer using solmate packages although I think for erc721 open zeppelin may have a few more helpers. @hensha256 thoughts here? |
||
_; | ||
} | ||
|
||
function tokenURI(uint256 tokenId) public view override(ERC721, IERC721Metadata) returns (string memory) { | ||
// TODO: implement this | ||
} | ||
|
||
/// @inheritdoc IERC721 | ||
function getApproved(uint256 tokenId) public view override(ERC721, IERC721) returns (address) { | ||
if (!_exists(tokenId)) revert NonexistentToken(); | ||
|
||
return _positions[tokenId].operator; | ||
} | ||
|
||
/// @dev Overrides _approve to use the operator in the position, which is packed with the position permit nonce | ||
function _approve(address to, uint256 tokenId) internal override(ERC721) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hmmm I'm kinda confused with the approval/auth scheme we are defining. It sounds like we are trying to override the traditional 721 spec but we are still using(not dis-allowing) the old 721 interfaces. and we are using the old traditional 721 checks like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also it seems like we want to allow permits... but where is all that sig verification? |
||
_positions[tokenId].operator = to; | ||
emit Approval(ownerOf(tokenId), to, tokenId); | ||
} | ||
|
||
function tokenByIndex(uint256 index) external view returns (uint256) { | ||
// TODO: implement this | ||
} | ||
|
||
function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256) { | ||
// TODO: implement this | ||
} | ||
|
||
function totalSupply() external view returns (uint256) { | ||
// TODO: implement this | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
// SPDX-License-Identifier: UNLICENSED | ||
pragma solidity ^0.8.19; | ||
|
||
import {PoolKey} from "@uniswap/v4-core/contracts/types/PoolKey.sol"; | ||
import {BalanceDelta} from "@uniswap/v4-core/contracts/types/BalanceDelta.sol"; | ||
import {ILockCallback} from "@uniswap/v4-core/contracts/interfaces/callback/ILockCallback.sol"; | ||
import {TickMath} from "@uniswap/v4-core/contracts/libraries/TickMath.sol"; | ||
import {BalanceDelta} from "@uniswap/v4-core/contracts/types/BalanceDelta.sol"; | ||
import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; | ||
import {Currency, CurrencyLibrary} from "@uniswap/v4-core/contracts/types/Currency.sol"; | ||
import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/contracts/types/PoolId.sol"; | ||
|
||
import {LiquidityAmounts} from "../libraries/LiquidityAmounts.sol"; | ||
import {PeripheryImmutableState} from "./PeripheryImmutableState.sol"; | ||
import {PeripheryPayments} from "./PeripheryPayments.sol"; | ||
|
||
/// @title Liquidity management functions | ||
/// @notice Internal functions for safely managing liquidity in Uniswap V4 | ||
abstract contract LiquidityManagement is ILockCallback, PeripheryImmutableState, PeripheryPayments { | ||
using CurrencyLibrary for Currency; | ||
using PoolIdLibrary for PoolKey; | ||
|
||
error PriceSlippage(); | ||
|
||
enum CallbackType {AddLiquidity} | ||
|
||
struct CallbackData { | ||
CallbackType callbackType; | ||
address sender; | ||
bytes params; | ||
} | ||
|
||
struct AddLiquidityParams { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It seems weird to have both Also, its possible that we actually can just have ModifyLiquidityParams that handle both increase and decrease.. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. And we should be consistent with the naming add/remove vs. increase/decrease. I prefer increase/decrease I think |
||
PoolKey poolKey; | ||
int24 tickLower; | ||
int24 tickUpper; | ||
uint256 amount0Desired; | ||
uint256 amount1Desired; | ||
uint256 amount0Min; | ||
uint256 amount1Min; | ||
bytes hookData; | ||
} | ||
|
||
/// @notice Add liquidity to an initialized pool | ||
function addLiquidity(AddLiquidityParams memory params) | ||
internal | ||
returns (uint128 liquidity, uint256 amount0, uint256 amount1) | ||
{ | ||
(liquidity, amount0, amount1) = abi.decode( | ||
poolManager.lock(abi.encode(CallbackData(CallbackType.AddLiquidity, msg.sender, abi.encode(params)))), | ||
(uint128, uint256, uint256) | ||
); | ||
} | ||
|
||
function addLiquidityCallback(AddLiquidityParams memory params) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. addLiquidityCallback is a strange name for an internal handler function because callbacks imply an external caller. Also for internal functions we can use the _ maybe |
||
internal | ||
returns (uint128 liquidity, BalanceDelta delta) | ||
{ | ||
(uint160 sqrtPriceX96,,,) = poolManager.getSlot0(params.poolKey.toId()); | ||
uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(params.tickLower); | ||
uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(params.tickUpper); | ||
liquidity = LiquidityAmounts.getLiquidityForAmounts( | ||
sqrtPriceX96, sqrtRatioAX96, sqrtRatioBX96, params.amount0Desired, params.amount1Desired | ||
); | ||
delta = poolManager.modifyPosition( | ||
params.poolKey, | ||
IPoolManager.ModifyPositionParams(params.tickLower, params.tickUpper, int256(int128(liquidity))), | ||
params.hookData | ||
); | ||
if ( | ||
uint256(int256(delta.amount0())) < params.amount0Min || uint256(int256(delta.amount1())) < params.amount1Min | ||
) revert PriceSlippage(); | ||
} | ||
|
||
function settleDeltas(address from, PoolKey memory poolKey, BalanceDelta delta) internal { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One high level thing here... I don't think we want to use When the user is withdrawing fully we will use |
||
if (delta.amount0() > 0) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. note that this won't work for pools that have custom accounting |
||
pay(poolKey.currency0, from, address(poolManager), uint256(int256(delta.amount0()))); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. how are we handling native? |
||
poolManager.settle(poolKey.currency0); | ||
} else if (delta.amount0() < 0) { | ||
poolManager.take(poolKey.currency0, address(this), uint128(-delta.amount0())); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i feel like we might want this to send directly to the recipient instead of to |
||
} | ||
|
||
if (delta.amount1() > 0) { | ||
pay(poolKey.currency0, from, address(poolManager), uint256(int256(delta.amount1()))); | ||
poolManager.settle(poolKey.currency1); | ||
} else if (delta.amount1() < 0) { | ||
poolManager.take(poolKey.currency1, address(this), uint128(-delta.amount1())); | ||
} | ||
} | ||
|
||
function lockAcquired(bytes calldata data) external override returns (bytes memory) { | ||
CallbackData memory callbackData = abi.decode(data, (CallbackData)); | ||
if (callbackData.callbackType == CallbackType.AddLiquidity) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm guessing this will be a big block of if statements? probably will then want to move the settle logic and the return statement outside the if statement since we will want to settle/return deltas in all of the cases |
||
AddLiquidityParams memory params = abi.decode(callbackData.params, (AddLiquidityParams)); | ||
(uint128 liquidity, BalanceDelta delta) = addLiquidityCallback(params); | ||
settleDeltas(callbackData.sender, params.poolKey, delta); | ||
return abi.encode(liquidity, delta.amount0(), delta.amount1()); | ||
} | ||
|
||
// TODO: handle decrease liquidity here | ||
return abi.encode(0); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
// SPDX-License-Identifier: GPL-2.0-or-later | ||
pragma solidity ^0.8.19; | ||
|
||
import {IMulticall} from "../interfaces/IMulticall.sol"; | ||
|
||
/// @title Multicall | ||
/// @notice Enables calling multiple methods in a single call to the contract | ||
abstract contract Multicall is IMulticall { | ||
/// @inheritdoc IMulticall | ||
function multicall(bytes[] calldata data) public payable override returns (bytes[] memory results) { | ||
results = new bytes[](data.length); | ||
for (uint256 i = 0; i < data.length; i++) { | ||
(bool success, bytes memory result) = address(this).delegatecall(data[i]); | ||
|
||
if (!success) { | ||
// handle custom errors | ||
if (result.length == 4) { | ||
assembly { | ||
revert(add(result, 0x20), mload(result)) | ||
} | ||
} | ||
Comment on lines
+17
to
+21
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. oops mark pointed out that custom errors can revert with params so this doesn't necessarily catch all custom errors 😅 , we could probably just revert with the reason again or we can look for a fix |
||
// Next 5 lines from https://ethereum.stackexchange.com/a/83577 | ||
if (result.length < 68) revert(); | ||
assembly { | ||
result := add(result, 0x04) | ||
} | ||
revert(abi.decode(result, (string))); | ||
} | ||
|
||
results[i] = result; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
// SPDX-License-Identifier: UNLICENSED | ||
pragma solidity ^0.8.19; | ||
|
||
import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; | ||
import {IPeripheryImmutableState} from "../interfaces/IPeripheryImmutableState.sol"; | ||
|
||
/// @title Immutable state | ||
/// @notice Immutable state used by periphery contracts | ||
abstract contract PeripheryImmutableState is IPeripheryImmutableState { | ||
IPoolManager public immutable override poolManager; | ||
|
||
constructor(IPoolManager _poolManager) { | ||
poolManager = _poolManager; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
// SPDX-License-Identifier: UNLICENSED | ||
pragma solidity ^0.8.19; | ||
|
||
import {ERC20} from "solmate/tokens/ERC20.sol"; | ||
import {Currency, CurrencyLibrary} from "@uniswap/v4-core/contracts/types/Currency.sol"; | ||
import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; | ||
import {IPeripheryPayments} from "../interfaces/IPeripheryPayments.sol"; | ||
|
||
abstract contract PeripheryPayments is IPeripheryPayments { | ||
using CurrencyLibrary for Currency; | ||
using SafeTransferLib for address; | ||
using SafeTransferLib for ERC20; | ||
|
||
error InsufficientToken(); | ||
error NativeTokenTransferFrom(); | ||
|
||
/// @inheritdoc IPeripheryPayments | ||
function sweepToken(Currency currency, uint256 amountMinimum, address recipient) public payable override { | ||
uint256 balanceCurrency = currency.balanceOfSelf(); | ||
if (balanceCurrency < amountMinimum) revert InsufficientToken(); | ||
|
||
if (balanceCurrency > 0) { | ||
currency.transfer(recipient, balanceCurrency); | ||
} | ||
} | ||
|
||
/// @param currency The currency to pay | ||
/// @param payer The entity that must pay | ||
/// @param recipient The entity that will receive payment | ||
/// @param value The amount to pay | ||
function pay(Currency currency, address payer, address recipient, uint256 value) internal { | ||
if (payer == address(this)) { | ||
// pay with tokens already in the contract (for the exact input multihop case) | ||
currency.transfer(recipient, value); | ||
} else { | ||
if (currency.isNative()) revert NativeTokenTransferFrom(); | ||
// pull payment | ||
ERC20(Currency.unwrap(currency)).safeTransferFrom(payer, recipient, value); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do we want to use Permit2 for this? or are we good with approve and transferFrom? |
||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hm this is kind of strange because you can have more than 1 operator with the 721 spec... so not sure why we save just one. did we do the same in v3? if so.. why?