From 240c8e1eef4962ea4f95baeea6c0a88e80c6c980 Mon Sep 17 00:00:00 2001 From: saucepoint Date: Mon, 17 Jun 2024 09:45:38 -0400 Subject: [PATCH] add ERC721Permit --- .forge-snapshots/FullRangeInitialize.snap | 2 +- .forge-snapshots/decreaseLiquidity_erc20.snap | 2 +- .../decreaseLiquidity_erc6909.snap | 2 +- .forge-snapshots/increaseLiquidity_erc20.snap | 2 +- .../increaseLiquidity_erc6909.snap | 2 +- .forge-snapshots/mintWithLiquidity.snap | 2 +- contracts/NonfungiblePositionManager.sol | 20 +++-- contracts/base/ERC721Permit.sol | 76 +++++++++++++++++++ contracts/base/SelfPermit.sol | 2 +- contracts/interfaces/IERC721Permit.sol | 25 ++++++ contracts/libraries/ChainId.sol | 13 ++++ 11 files changed, 135 insertions(+), 13 deletions(-) create mode 100644 contracts/base/ERC721Permit.sol create mode 100644 contracts/interfaces/IERC721Permit.sol create mode 100644 contracts/libraries/ChainId.sol diff --git a/.forge-snapshots/FullRangeInitialize.snap b/.forge-snapshots/FullRangeInitialize.snap index 22412ada..9661da18 100644 --- a/.forge-snapshots/FullRangeInitialize.snap +++ b/.forge-snapshots/FullRangeInitialize.snap @@ -1 +1 @@ -1037821 \ No newline at end of file +1039616 \ No newline at end of file diff --git a/.forge-snapshots/decreaseLiquidity_erc20.snap b/.forge-snapshots/decreaseLiquidity_erc20.snap index 9d667ef7..558500f4 100644 --- a/.forge-snapshots/decreaseLiquidity_erc20.snap +++ b/.forge-snapshots/decreaseLiquidity_erc20.snap @@ -1 +1 @@ -187091 \ No newline at end of file +187220 \ No newline at end of file diff --git a/.forge-snapshots/decreaseLiquidity_erc6909.snap b/.forge-snapshots/decreaseLiquidity_erc6909.snap index e9492b3e..8d2b2de0 100644 --- a/.forge-snapshots/decreaseLiquidity_erc6909.snap +++ b/.forge-snapshots/decreaseLiquidity_erc6909.snap @@ -1 +1 @@ -166084 \ No newline at end of file +166214 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc20.snap b/.forge-snapshots/increaseLiquidity_erc20.snap index 5280964c..fd8256e5 100644 --- a/.forge-snapshots/increaseLiquidity_erc20.snap +++ b/.forge-snapshots/increaseLiquidity_erc20.snap @@ -1 +1 @@ -187781 \ No newline at end of file +187943 \ No newline at end of file diff --git a/.forge-snapshots/increaseLiquidity_erc6909.snap b/.forge-snapshots/increaseLiquidity_erc6909.snap index 460aeb49..075aab60 100644 --- a/.forge-snapshots/increaseLiquidity_erc6909.snap +++ b/.forge-snapshots/increaseLiquidity_erc6909.snap @@ -1 +1 @@ -163384 \ No newline at end of file +163546 \ No newline at end of file diff --git a/.forge-snapshots/mintWithLiquidity.snap b/.forge-snapshots/mintWithLiquidity.snap index b7ef4c1e..3ed18a4f 100644 --- a/.forge-snapshots/mintWithLiquidity.snap +++ b/.forge-snapshots/mintWithLiquidity.snap @@ -1 +1 @@ -485624 \ No newline at end of file +485501 \ No newline at end of file diff --git a/contracts/NonfungiblePositionManager.sol b/contracts/NonfungiblePositionManager.sol index f2acdbc1..0e1b32d8 100644 --- a/contracts/NonfungiblePositionManager.sol +++ b/contracts/NonfungiblePositionManager.sol @@ -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"; @@ -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; @@ -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 @@ -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); @@ -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) { diff --git a/contracts/base/ERC721Permit.sol b/contracts/base/ERC721Permit.sol new file mode 100644 index 00000000..8eb86521 --- /dev/null +++ b/contracts/base/ERC721Permit.sol @@ -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); + } +} diff --git a/contracts/base/SelfPermit.sol b/contracts/base/SelfPermit.sol index 40449636..60ae6762 100644 --- a/contracts/base/SelfPermit.sol +++ b/contracts/base/SelfPermit.sol @@ -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"; diff --git a/contracts/interfaces/IERC721Permit.sol b/contracts/interfaces/IERC721Permit.sol new file mode 100644 index 00000000..daa27030 --- /dev/null +++ b/contracts/interfaces/IERC721Permit.sol @@ -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; +} diff --git a/contracts/libraries/ChainId.sol b/contracts/libraries/ChainId.sol new file mode 100644 index 00000000..7e67989c --- /dev/null +++ b/contracts/libraries/ChainId.sol @@ -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() + } + } +}