Skip to content

Commit

Permalink
add ERC721Permit
Browse files Browse the repository at this point in the history
  • Loading branch information
saucepoint committed Jun 17, 2024
1 parent 07cc628 commit 240c8e1
Show file tree
Hide file tree
Showing 11 changed files with 135 additions and 13 deletions.
2 changes: 1 addition & 1 deletion .forge-snapshots/FullRangeInitialize.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1037821
1039616
2 changes: 1 addition & 1 deletion .forge-snapshots/decreaseLiquidity_erc20.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
187091
187220
2 changes: 1 addition & 1 deletion .forge-snapshots/decreaseLiquidity_erc6909.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
166084
166214
2 changes: 1 addition & 1 deletion .forge-snapshots/increaseLiquidity_erc20.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
187781
187943
2 changes: 1 addition & 1 deletion .forge-snapshots/increaseLiquidity_erc6909.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
163384
163546
2 changes: 1 addition & 1 deletion .forge-snapshots/mintWithLiquidity.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
485624
485501
20 changes: 14 additions & 6 deletions contracts/NonfungiblePositionManager.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.24;

import {ERC721} from "openzeppelin-contracts/contracts/token/ERC721/ERC721.sol";
import {ERC721Permit} from "./base/ERC721Permit.sol";
import {INonfungiblePositionManager} from "./interfaces/INonfungiblePositionManager.sol";
import {BaseLiquidityManagement} from "./base/BaseLiquidityManagement.sol";

Expand All @@ -18,7 +18,7 @@ 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, ERC721 {
contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidityManagement, ERC721Permit {
using CurrencyLibrary for Currency;
using CurrencySettleTake for Currency;
using PoolIdLibrary for PoolKey;
Expand All @@ -36,7 +36,10 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit

mapping(uint256 tokenId => TokenPosition position) public tokenPositions;

constructor(IPoolManager _poolManager) BaseLiquidityManagement(_poolManager) ERC721("Uniswap V4 LP", "LPT") {}
constructor(IPoolManager _poolManager)
BaseLiquidityManagement(_poolManager)
ERC721Permit("Uniswap V4 Positions NFT-V1", "UNI-V3-POS", "1")
{}

// NOTE: more gas efficient as LiquidityAmounts is used offchain
// TODO: deadline check
Expand Down Expand Up @@ -123,8 +126,8 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit
return feesOwed(tokenPosition.owner, tokenPosition.range);
}

function _afterTokenTransfer(address from, address to, uint256 firstTokenId, uint256 batchSize) internal override {
TokenPosition storage tokenPosition = tokenPositions[firstTokenId];
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);
Expand All @@ -134,7 +137,12 @@ contract NonfungiblePositionManager is INonfungiblePositionManager, BaseLiquidit
delete positions[from][rangeId];

// update token position
tokenPositions[firstTokenId] = TokenPosition({owner: to, range: tokenPosition.range});
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) {
Expand Down
76 changes: 76 additions & 0 deletions contracts/base/ERC721Permit.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.24;

import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {Address} from "@openzeppelin/contracts/utils/Address.sol";

import {ChainId} from "../libraries/ChainId.sol";
import {IERC721Permit} from "../interfaces/IERC721Permit.sol";
import {IERC1271} from "../interfaces/external/IERC1271.sol";

/// @title ERC721 with permit
/// @notice Nonfungible tokens that support an approve via signature, i.e. permit
abstract contract ERC721Permit is ERC721, IERC721Permit {
/// @dev Gets the current nonce for a token ID and then increments it, returning the original value
function _getAndIncrementNonce(uint256 tokenId) internal virtual returns (uint256);

/// @dev The hash of the name used in the permit signature verification
bytes32 private immutable nameHash;

/// @dev The hash of the version string used in the permit signature verification
bytes32 private immutable versionHash;

/// @notice Computes the nameHash and versionHash
constructor(string memory name_, string memory symbol_, string memory version_) ERC721(name_, symbol_) {
nameHash = keccak256(bytes(name_));
versionHash = keccak256(bytes(version_));
}

/// @inheritdoc IERC721Permit
function DOMAIN_SEPARATOR() public view override returns (bytes32) {
return keccak256(
abi.encode(
// keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)')
0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f,
nameHash,
versionHash,
ChainId.get(),
address(this)
)
);
}

/// @inheritdoc IERC721Permit
/// @dev Value is equal to keccak256("Permit(address spender,uint256 tokenId,uint256 nonce,uint256 deadline)");
bytes32 public constant override PERMIT_TYPEHASH =
0x49ecf333e5b8c95c40fdafc95c1ad136e8914a8fb55e9dc8bb01eaa83a2df9ad;

/// @inheritdoc IERC721Permit
function permit(address spender, uint256 tokenId, uint256 deadline, uint8 v, bytes32 r, bytes32 s)
external
payable
override
{
require(block.timestamp <= deadline, "Permit expired");

bytes32 digest = keccak256(
abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR(),
keccak256(abi.encode(PERMIT_TYPEHASH, spender, tokenId, _getAndIncrementNonce(tokenId), deadline))
)
);
address owner = ownerOf(tokenId);
require(spender != owner, "ERC721Permit: approval to current owner");

if (Address.isContract(owner)) {
require(IERC1271(owner).isValidSignature(digest, abi.encodePacked(r, s, v)) == 0x1626ba7e, "Unauthorized");
} else {
address recoveredAddress = ecrecover(digest, v, r, s);
require(recoveredAddress != address(0), "Invalid signature");
require(recoveredAddress == owner, "Unauthorized");
}

approve(spender, tokenId);
}
}
2 changes: 1 addition & 1 deletion contracts/base/SelfPermit.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
pragma solidity ^0.8.19;

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {IERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol";
import {IERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/draft-IERC20Permit.sol";

import {IERC20PermitAllowed} from "../interfaces/external/IERC20PermitAllowed.sol";
import {ISelfPermit} from "../interfaces/ISelfPermit.sol";
Expand Down
25 changes: 25 additions & 0 deletions contracts/interfaces/IERC721Permit.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity >=0.7.5;

/// @title ERC721 with permit
/// @notice Extension to ERC721 that includes a permit function for signature based approvals
interface IERC721Permit {
/// @notice The permit typehash used in the permit signature
/// @return The typehash for the permit
function PERMIT_TYPEHASH() external pure returns (bytes32);

/// @notice The domain separator used in the permit signature
/// @return The domain seperator used in encoding of permit signature
function DOMAIN_SEPARATOR() external view returns (bytes32);

/// @notice Approve of a specific token ID for spending by spender via signature
/// @param spender The account that is being approved
/// @param tokenId The ID of the token that is being approved for spending
/// @param deadline The deadline timestamp by which the call must be mined for the approve to work
/// @param v Must produce valid secp256k1 signature from the holder along with `r` and `s`
/// @param r Must produce valid secp256k1 signature from the holder along with `v` and `s`
/// @param s Must produce valid secp256k1 signature from the holder along with `r` and `v`
function permit(address spender, uint256 tokenId, uint256 deadline, uint8 v, bytes32 r, bytes32 s)
external
payable;
}
13 changes: 13 additions & 0 deletions contracts/libraries/ChainId.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity >=0.7.0;

/// @title Function for getting the current chain ID
library ChainId {
/// @dev Gets the current chain ID
/// @return chainId The current chain ID
function get() internal view returns (uint256 chainId) {
assembly {
chainId := chainid()
}
}
}

0 comments on commit 240c8e1

Please sign in to comment.