Skip to content

Commit

Permalink
ERC721Permit (#210)
Browse files Browse the repository at this point in the history
* allow for nonsigners to call permit

* forge fmt

* test permit with multicall

* make DOMAIN_SEPARATOR immutable

* avoid chain fork replays

* misc test cleanup

* custom errors

* move magic hex to a constant

* unpayable permit

* use OZ EIP712

* separate out UnorderedNonce into a reusable contract

* move token URI to posm

* add back in payable permit

* fix cherry picked commits

* remove public digest getter

* replace range with config naming

* deprecate old test: requiring permission to increase liq

* pr feedback

* borrow pertmi2 nonce tests for UnorderedNonce

* dedicated permit and approve testing for ERC721Permit

* pr feedback: operator should be broadcaster of permit calls

* reorganize permit hashing and verification

* refactor ERC721Permit signature verification with generic signature calldata handler

* remove deprecated library

* fix imports

* formatting

* pr feedback

* optimize nonce bit flipping

* discard public PERMIT_TYPEHASH

* renaming

* library-ify bit flipping

* yall crazy for sending through the ringer

* nits
  • Loading branch information
saucepoint authored Aug 3, 2024
1 parent 6fe5428 commit 1f28ac2
Show file tree
Hide file tree
Showing 53 changed files with 985 additions and 73 deletions.
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_burn_empty.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
47168
47186
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_burn_empty_native.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
46986
47004
Original file line number Diff line number Diff line change
@@ -1 +1 @@
123040
123058
Original file line number Diff line number Diff line change
@@ -1 +1 @@
122739
122756
Original file line number Diff line number Diff line change
@@ -1 +1 @@
130119
130136
Original file line number Diff line number Diff line change
@@ -1 +1 @@
129817
129835
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_collect_native.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
141387
141409
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_collect_sameRange.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
150235
150257
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_collect_withClose.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
150235
150257
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_collect_withTakePair.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
149846
149868
Original file line number Diff line number Diff line change
@@ -1 +1 @@
108584
108602
Original file line number Diff line number Diff line change
@@ -1 +1 @@
115778
115800
Original file line number Diff line number Diff line change
@@ -1 +1 @@
115389
115411
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_decrease_burnEmpty.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
134178
134196
Original file line number Diff line number Diff line change
@@ -1 +1 @@
126917
126935
Original file line number Diff line number Diff line change
@@ -1 +1 @@
128494
128516
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
152144
Original file line number Diff line number Diff line change
@@ -1 +1 @@
152341
152363
Original file line number Diff line number Diff line change
@@ -1 +1 @@
151582
151604
Original file line number Diff line number Diff line change
@@ -1 +1 @@
134141
134163
Original file line number Diff line number Diff line change
@@ -1 +1 @@
130306
130328
Original file line number Diff line number Diff line change
@@ -1 +1 @@
171000
171022
Original file line number Diff line number Diff line change
@@ -1 +1 @@
140956
141002
1 change: 1 addition & 0 deletions .forge-snapshots/PositionManager_mint.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
372007
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_mint_native.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
336819
336841
1 change: 1 addition & 0 deletions .forge-snapshots/PositionManager_mint_nativeWithSweep.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
345190
Original file line number Diff line number Diff line change
@@ -1 +1 @@
345348
345370
Original file line number Diff line number Diff line change
@@ -1 +1 @@
344889
344911
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_mint_onSameTickLower.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
314801
314823
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_mint_onSameTickUpper.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
315443
315465
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_mint_sameRange.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
241025
241047
Original file line number Diff line number Diff line change
@@ -1 +1 @@
371171
371193
Original file line number Diff line number Diff line change
@@ -1 +1 @@
320819
320841
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_mint_withClose.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
372119
372141
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_mint_withSettlePair.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
371498
371520
Original file line number Diff line number Diff line change
@@ -1 +1 @@
416560
416516
1 change: 1 addition & 0 deletions .forge-snapshots/PositionManager_permit.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
79585
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
62497
1 change: 1 addition & 0 deletions .forge-snapshots/PositionManager_permit_twice.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
45397
5 changes: 5 additions & 0 deletions src/PositionManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ contract PositionManager is
_;
}

// TODO: to be implemented after audits
function tokenURI(uint256) public pure override returns (string memory) {
return "https://example.com";
}

/// @notice Reverts if the caller is not the owner or approved for the ERC721 token
/// @param caller The address of the caller
/// @param tokenId the unique identifier of the ERC721 token
Expand Down
68 changes: 59 additions & 9 deletions src/base/ERC721Permit.sol
Original file line number Diff line number Diff line change
@@ -1,20 +1,70 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.24;

import {IERC721} from "forge-std/interfaces/IERC721.sol";
import {ERC721} from "solmate/src/tokens/ERC721.sol";
import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import {ERC721PermitHashLibrary} from "../libraries/ERC721PermitHash.sol";
import {SignatureVerification} from "permit2/src/libraries/SignatureVerification.sol";

/// @notice An ERC721 contract that supports permit.
/// TODO: Support permit.
contract ERC721Permit is ERC721 {
constructor(string memory name_, string memory symbol_, string memory version_) ERC721(name_, symbol_) {}
import {IERC721Permit} from "../interfaces/IERC721Permit.sol";
import {UnorderedNonce} from "./UnorderedNonce.sol";

/// @title ERC721 with permit
/// @notice Nonfungible tokens that support an approve via signature, i.e. permit
abstract contract ERC721Permit is ERC721, IERC721Permit, EIP712, UnorderedNonce {
using SignatureVerification for bytes;

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

/// @inheritdoc IERC721Permit
function DOMAIN_SEPARATOR() external view returns (bytes32) {
return _domainSeparatorV4();
}

/// @inheritdoc IERC721Permit
function permit(address spender, uint256 tokenId, uint256 deadline, uint256 nonce, bytes calldata signature)
external
payable
{
if (block.timestamp > deadline) revert DeadlineExpired();

address owner = ownerOf(tokenId);
if (spender == owner) revert NoSelfPermit();

bytes32 hash = ERC721PermitHashLibrary.hash(spender, tokenId, nonce, deadline);
signature.verify(_hashTypedDataV4(hash), owner);

_useUnorderedNonce(owner, nonce);
_approve(owner, spender, tokenId);
}

/// @notice Change or reaffirm the approved address for an NFT
/// @dev override Solmate's ERC721 approve so approve() and permit() share the _approve method
/// The zero address indicates there is no approved address
/// Throws error unless `msg.sender` is the current NFT owner,
/// or an authorized operator of the current owner.
/// @param spender The new approved NFT controller
/// @param id The tokenId of the NFT to approve
function approve(address spender, uint256 id) public override {
address owner = _ownerOf[id];

if (msg.sender != owner && !isApprovedForAll[owner][msg.sender]) revert Unauthorized();

_approve(owner, spender, id);
}

function _approve(address owner, address spender, uint256 id) internal {
getApproved[id] = spender;
emit Approval(owner, spender, id);
}

function _isApprovedOrOwner(address spender, uint256 tokenId) internal view returns (bool) {
return spender == ownerOf(tokenId) || getApproved[tokenId] == spender
|| isApprovedForAll[ownerOf(tokenId)][spender];
}

// TODO: Use PositionDescriptor.
function tokenURI(uint256 id) public pure override returns (string memory) {
return string(abi.encode(id));
}
}
24 changes: 24 additions & 0 deletions src/base/UnorderedNonce.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.24;

/// @title Unordered Nonce
/// @notice Contract state and methods for using unordered nonces in signatures
contract UnorderedNonce {
error NonceAlreadyUsed();

/// @notice mapping of nonces consumed by each address, where a nonce is a single bit on the 256-bit bitmap
/// @dev word is at most type(uint248).max
mapping(address owner => mapping(uint256 word => uint256 bitmap)) public nonces;

/// @notice Consume a nonce, reverting if its already been used
/// @param owner address, the owner/signer of the nonce
/// @param nonce uint256, the nonce to consume. the top 248 bits are the word, the bottom 8 bits indicate the bit position
function _useUnorderedNonce(address owner, uint256 nonce) internal {
uint256 wordPos = nonce >> 8;
uint256 bitPos = uint8(nonce);

uint256 bit = 1 << bitPos;
uint256 flipped = nonces[owner][wordPos] ^= bit;
if (flipped & bit == 0) revert NonceAlreadyUsed();
}
}
14 changes: 5 additions & 9 deletions src/interfaces/IERC721Permit.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,9 @@ pragma solidity >=0.7.5;
/// @title ERC721 with permit
/// @notice Extension to ERC721 that includes a permit function for signature based approvals
interface IERC721Permit {
error NonceAlreadyUsed();

/// @notice The permit typehash used in the permit signature
/// @return The typehash for the permit
function PERMIT_TYPEHASH() external pure returns (bytes32);
error DeadlineExpired();
error NoSelfPermit();
error Unauthorized();

/// @notice The domain separator used in the permit signature
/// @return The domain seperator used in encoding of permit signature
Expand All @@ -18,11 +16,9 @@ interface IERC721Permit {
/// @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`
/// @param signature Concatenated data from a valid secp256k1 signature from the holder, i.e. abi.encodePacked(r, s, v)
/// @dev payable so it can be multicalled with NATIVE related actions
function permit(address spender, uint256 tokenId, uint256 deadline, uint256 nonce, uint8 v, bytes32 r, bytes32 s)
function permit(address spender, uint256 tokenId, uint256 deadline, uint256 nonce, bytes calldata signature)
external
payable;
}
13 changes: 0 additions & 13 deletions src/libraries/ChainId.sol

This file was deleted.

11 changes: 11 additions & 0 deletions src/libraries/ERC721PermitHash.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.24;

library ERC721PermitHashLibrary {
/// @dev Value is equal to keccak256("Permit(address spender,uint256 tokenId,uint256 nonce,uint256 deadline)");
bytes32 constant PERMIT_TYPEHASH = 0x49ecf333e5b8c95c40fdafc95c1ad136e8914a8fb55e9dc8bb01eaa83a2df9ad;

function hash(address spender, uint256 tokenId, uint256 nonce, uint256 deadline) internal pure returns (bytes32) {
return keccak256(abi.encode(PERMIT_TYPEHASH, spender, tokenId, nonce, deadline));
}
}
Loading

0 comments on commit 1f28ac2

Please sign in to comment.