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

PosM: owner-level nonce for permit #139

Closed
wants to merge 17 commits into from
Closed
Show file tree
Hide file tree
Changes from 12 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
2 changes: 1 addition & 1 deletion .forge-snapshots/autocompound_exactUnclaimedFees.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
262456
295633
Original file line number Diff line number Diff line change
@@ -1 +1 @@
194829
227992
2 changes: 1 addition & 1 deletion .forge-snapshots/autocompound_excessFeesCredit.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
282995
316172
2 changes: 1 addition & 1 deletion .forge-snapshots/decreaseLiquidity_erc20.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
180479
210674
2 changes: 1 addition & 1 deletion .forge-snapshots/decreaseLiquidity_erc6909.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
180491
210686
2 changes: 1 addition & 1 deletion .forge-snapshots/increaseLiquidity_erc20.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
175213
196449
2 changes: 1 addition & 1 deletion .forge-snapshots/increaseLiquidity_erc6909.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
150802
196461
2 changes: 1 addition & 1 deletion .forge-snapshots/mintWithLiquidity.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
472424
510048
1 change: 1 addition & 0 deletions .forge-snapshots/permit.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
74708
1 change: 1 addition & 0 deletions .forge-snapshots/permit_secondPosition.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
57608
1 change: 1 addition & 0 deletions .forge-snapshots/permit_twice.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
40496
170 changes: 71 additions & 99 deletions contracts/NonfungiblePositionManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol";
import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol";
import {TransientStateLibrary} from "@uniswap/v4-core/src/libraries/TransientStateLibrary.sol";
import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol";
import {TransientLiquidityDelta} from "./libraries/TransientLiquidityDelta.sol";

import "forge-std/console2.sol";

contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidityManagement, ERC721Permit {
using CurrencyLibrary for Currency;
Expand All @@ -27,35 +30,56 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit
using StateLibrary for IPoolManager;
using TransientStateLibrary for IPoolManager;
using SafeCast for uint256;
using TransientLiquidityDelta for Currency;

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

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

// TODO: TSTORE these jawns
address internal msgSender;
bool internal unlockedByThis;

// TODO: Context is inherited through ERC721 and will be not useful to use _msgSender() which will be address(this) with our current mutlicall.
function _msgSenderInternal() internal override returns (address) {
return msgSender;
}

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

function unlockAndExecute(bytes[] memory data) public returns (bytes memory) {
return manager.unlock(abi.encode(data));
function unlockAndExecute(bytes[] memory data, Currency[] memory currencies) public returns (int128[] memory) {
msgSender = msg.sender;
unlockedByThis = true;
return abi.decode(manager.unlock(abi.encode(data, currencies)), (int128[]));
}

function _unlockCallback(bytes calldata payload) internal override returns (bytes memory) {
bytes[] memory data = abi.decode(payload, (bytes[]));
(bytes[] memory data, Currency[] memory currencies) = abi.decode(payload, (bytes[], Currency[]));

bool success;
bytes memory returnData;

for (uint256 i; i < data.length; i++) {
// TODO: bubble up the return
(success, returnData) = address(this).call(data[i]);
(success,) = address(this).call(data[i]);
if (!success) revert("EXECUTE_FAILED");
}
// zeroOut();

return returnData;
// close the deltas
int128[] memory returnData = new int128[](currencies.length);
for (uint256 i; i < currencies.length; i++) {
returnData[i] = currencies[i].close(manager, msgSender, false); // TODO: support claims
currencies[i].close(manager, address(this), true); // position manager always takes 6909
}

// Should just be returning the netted amount that was settled on behalf of the caller (msgSender)
// TODO: any recipient deltas settled earlier.
// @comment sauce: i dont think we can return recipient deltas since we cant parse the payload
return abi.encode(returnData);
}

// NOTE: more gas efficient as LiquidityAmounts is used offchain
Expand All @@ -64,28 +88,15 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit
LiquidityRange calldata range,
uint256 liquidity,
uint256 deadline,
address recipient,
address owner,
bytes calldata hookData
) public payable returns (BalanceDelta delta) {
// TODO: optimization, read/write manager.isUnlocked to avoid repeated external calls for batched execution
if (manager.isUnlocked()) {
BalanceDelta thisDelta;
(delta, thisDelta) = _increaseLiquidity(recipient, range, liquidity, hookData);

// TODO: should be triggered by zeroOut in _execute...
_closeCallerDeltas(delta, range.poolKey.currency0, range.poolKey.currency1, recipient, false);
_closeThisDeltas(thisDelta, range.poolKey.currency0, range.poolKey.currency1);

// mint receipt token
uint256 tokenId;
_mint(recipient, (tokenId = nextTokenId++));
tokenPositions[tokenId] = TokenPosition({owner: recipient, range: range});
} else {
bytes[] memory data = new bytes[](1);
data[0] = abi.encodeWithSelector(this.mint.selector, range, liquidity, deadline, recipient, hookData);
bytes memory result = unlockAndExecute(data);
delta = abi.decode(result, (BalanceDelta));
}
) external payable onlyIfUnlocked {
_increaseLiquidity(owner, range, liquidity, hookData);

// mint receipt token
uint256 tokenId;
_mint(owner, (tokenId = nextTokenId++));
tokenPositions[tokenId] = TokenPosition({owner: owner, range: range, operator: address(0x0)});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you mint, maybe you should be allowed to specify a default operator? maybe thats wack though lol

}

// NOTE: more expensive since LiquidityAmounts is used onchain
Expand All @@ -111,92 +122,43 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit
function increaseLiquidity(uint256 tokenId, uint256 liquidity, bytes calldata hookData, bool claims)
external
isAuthorizedForToken(tokenId)
returns (BalanceDelta delta)
onlyIfUnlocked
{
TokenPosition memory tokenPos = tokenPositions[tokenId];

if (manager.isUnlocked()) {
BalanceDelta thisDelta;
(delta, thisDelta) = _increaseLiquidity(tokenPos.owner, tokenPos.range, liquidity, hookData);

// TODO: should be triggered by zeroOut in _execute...
_closeCallerDeltas(
delta, tokenPos.range.poolKey.currency0, tokenPos.range.poolKey.currency1, tokenPos.owner, claims
);
_closeThisDeltas(thisDelta, tokenPos.range.poolKey.currency0, tokenPos.range.poolKey.currency1);
} else {
bytes[] memory data = new bytes[](1);
data[0] = abi.encodeWithSelector(this.increaseLiquidity.selector, tokenId, liquidity, hookData, claims);
bytes memory result = unlockAndExecute(data);
delta = abi.decode(result, (BalanceDelta));
}
_increaseLiquidity(tokenPos.owner, tokenPos.range, liquidity, hookData);
}

function decreaseLiquidity(uint256 tokenId, uint256 liquidity, bytes calldata hookData, bool claims)
public
external
isAuthorizedForToken(tokenId)
returns (BalanceDelta delta)
onlyIfUnlocked
{
TokenPosition memory tokenPos = tokenPositions[tokenId];

if (manager.isUnlocked()) {
BalanceDelta thisDelta;
(delta, thisDelta) = _decreaseLiquidity(tokenPos.owner, tokenPos.range, liquidity, hookData);
_closeCallerDeltas(
delta, tokenPos.range.poolKey.currency0, tokenPos.range.poolKey.currency1, tokenPos.owner, claims
);
_closeThisDeltas(thisDelta, tokenPos.range.poolKey.currency0, tokenPos.range.poolKey.currency1);
} else {
bytes[] memory data = new bytes[](1);
data[0] = abi.encodeWithSelector(this.decreaseLiquidity.selector, tokenId, liquidity, hookData, claims);
bytes memory result = unlockAndExecute(data);
delta = abi.decode(result, (BalanceDelta));
}
_decreaseLiquidity(tokenPos.owner, tokenPos.range, liquidity, hookData);
}

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];
// TODO return type?
function burn(uint256 tokenId) public isAuthorizedForToken(tokenId) returns (BalanceDelta delta) {
// TODO: Burn currently requires a decrease and collect call before the token can be deleted. Possible to combine.
// We do not need to enforce the pool manager to be unlocked bc this function is purely clearing storage for the minted tokenId.
TokenPosition memory tokenPos = tokenPositions[tokenId];
// Checks that the full position's liquidity has been removed and all tokens have been collected from tokensOwed.
_validateBurn(tokenPos.owner, tokenPos.range);
delete tokenPositions[tokenId];

// burn the token
// 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)
external
onlyIfUnlocked
{
TokenPosition memory tokenPos = tokenPositions[tokenId];
if (manager.isUnlocked()) {
BalanceDelta thisDelta;
(delta, thisDelta) = _collect(tokenPos.owner, tokenPos.range, hookData);
_closeCallerDeltas(
delta, tokenPos.range.poolKey.currency0, tokenPos.range.poolKey.currency1, tokenPos.owner, claims
);
_closeThisDeltas(thisDelta, tokenPos.range.poolKey.currency0, tokenPos.range.poolKey.currency1);
} else {
bytes[] memory data = new bytes[](1);
data[0] = abi.encodeWithSelector(this.collect.selector, tokenId, recipient, hookData, claims);
bytes memory result = unlockAndExecute(data);
delta = abi.decode(result, (BalanceDelta));
}

_collect(recipient, tokenPos.owner, tokenPos.range, hookData);
}

function feesOwed(uint256 tokenId) external view returns (uint256 token0Owed, uint256 token1Owed) {
Expand All @@ -208,23 +170,33 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit
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});
tokenPositions[tokenId] = TokenPosition({owner: to, range: tokenPosition.range, operator: address(0x0)});
}

function _getAndIncrementNonce(uint256 tokenId) internal override returns (uint256) {
TokenPosition memory tokenPosition = tokenPositions[tokenId];
return uint256(positions[tokenPosition.owner][tokenPosition.range.toId()].nonce++);
// override ERC721 approval by setting operator
function _approve(address spender, uint256 tokenId) internal override {
tokenPositions[tokenId].operator = spender;
}

function getApproved(uint256 tokenId) public view override returns (address) {
require(_exists(tokenId), "ERC721: approved query for nonexistent token");

return tokenPositions[tokenId].operator;
}

modifier isAuthorizedForToken(uint256 tokenId) {
require(msg.sender == address(this) || _isApprovedOrOwner(msg.sender, tokenId), "Not approved");
_;
}

modifier onlyIfUnlocked() {
if (!unlockedByThis) revert MustBeUnlockedByThisContract();
_;
}
}
Loading
Loading