-
Notifications
You must be signed in to change notification settings - Fork 505
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
Conversation
2834b62
to
38e78db
Compare
88119a9
to
a26a6e9
Compare
a26a6e9
to
05c9543
Compare
02a5cd2
to
324976e
Compare
ca4e05c
to
ad34755
Compare
import {PeripheryImmutableState} from "./PeripheryImmutableState.sol"; | ||
|
||
abstract contract LiquidityManagement is ILockCallback, ILiquidityManagement, PeripheryImmutableState { | ||
function mintEntry(MintParams memory params) |
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.
just an example of how LiquidityManagement
can be used with contracts/NonfungiblePositionManagerV4.sol
- mintEntry calls the lock, then lockAcquired
handles rest of the codepath within this file
if (result.length == 4) { | ||
assembly { | ||
revert(add(result, 0x20), mload(result)) | ||
} | ||
} |
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.
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
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 comment
The 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 comment
The reason will be displayed to describe this comment to others. Learn more.
thinking they probably want to use the positions
getter for getting position data, since it includes helpers for unwrapping poolId
- otherwise they're kind of exposed to our internal representation of how we store pools right? they'll need both _poolIdToPoolKey
and _positions
to get useful position data
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.
oh yeah good point
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.
nit: Would just add to the natspec that they key is specifically the tokenId (tokenId) ie mapping from tokenId to position data
/// @inheritdoc INonfungiblePositionManagerV4 | ||
function burn(uint256 tokenId) external payable override isAuthorizedForToken(tokenId) { | ||
TokenPosition storage position = _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 comment
The 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 comment
The 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
} 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 comment
The 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?
/// @title Periphery Payments | ||
/// @notice Functions to ease deposits and withdrawals of ETH | ||
interface IPeripheryPayments { | ||
// TODO: figure out if we still need unwrapWETH9 from v3? |
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.
hmmm good question. I think its more of an opinionated decision nowadays... I would lean towards not adding it? People can add liquidity in ETH and remove it in ETH, people can add liquidity in WETH and remove it in WETH. It feels opinionated now to allow the WETH people to remove it as ETH if we arent doing it the other way around?
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.
however we do like to be helpful if we think its a common usecase of course
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.
yeah agreed on not making it opinionated
pay(poolKey.currency0, from, address(poolManager), uint256(int256(delta.amount0()))); | ||
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 comment
The 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 address(this)
- so that we dont then have to send it onwards which is extra gas?
} | ||
|
||
/// @notice Creates a new position wrapped in a NFT | ||
/// @dev Call this when the pool does exist and is initialized. Note that if the pool is created but not initialized |
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.
what is a pool that is created but not initialized?
payable | ||
returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1); | ||
|
||
struct IncreaseLiquidityParams { |
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.
I prefer all structs to be defined at the top/grouped together. I think that is just a consistent styling choice I've noticed for most of our contracts
bytes params; | ||
} | ||
|
||
struct AddLiquidityParams { |
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.
It seems weird to have bothAddLiquidityParams
and IncreaseLiquidityParams
. Like hard to know why you use one and not the other.
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 comment
The 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
|
||
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 comment
The 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
); | ||
} | ||
|
||
function addLiquidityCallback(AddLiquidityParams memory params) |
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.
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 _addLiquidity
?
// the nonce for permits | ||
uint96 nonce; | ||
// the address that is approved for spending this token | ||
address operator; |
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?
} | ||
|
||
/// @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 comment
The 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 _isApprovedOrOwner
which doesnt look at the operator in the TokenPositions struct
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.
Also it seems like we want to allow permits... but where is all that sig verification?
} | ||
|
||
function settleDeltas(address from, PoolKey memory poolKey, BalanceDelta delta) internal { | ||
if (delta.amount0() > 0) { |
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.
note that this won't work for pools that have custom accounting
|
||
function settleDeltas(address from, PoolKey memory poolKey, BalanceDelta delta) internal { | ||
if (delta.amount0() > 0) { | ||
pay(poolKey.currency0, from, address(poolManager), uint256(int256(delta.amount0()))); |
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.
how are we handling native?
) 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 comment
The 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 take
and settle
. We will want to use mint
/ burn
.
When the user is withdrawing fully we will use take
/settle
though to give them the underlying erc20s. But when we are in the state where the position manager is still managing positions for a user we just want to hold claims to underlying imo (which will interface with the 6909 path)
Closing in favor of #141 |
Description of changes
This PR adds the base skeleton for
NonfungiblePositionManagerV4
So this is mostly copied from v3-periphery but I made a few changes:
PoolKeys
(instead of having a separate counter => address mapping)createAndInitializePoolIfNecessary
directly to the contract and func args take inPoolKey
instead of idstokenByIndex
,tokenOfOwnerByIndex
, andtotalSupply
- think these were added toERC721Enumerable
?TODO (next steps):
// TODO: implement this
)