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

ERC721Permit #210

Merged
merged 47 commits into from
Aug 3, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
0c159e2
allow for nonsigners to call permit
saucepoint Jul 19, 2024
d13fbe7
forge fmt
saucepoint Jul 19, 2024
ff0b56e
test permit with multicall
saucepoint Jul 19, 2024
558b621
make DOMAIN_SEPARATOR immutable
saucepoint Jul 19, 2024
36ccc28
avoid chain fork replays
saucepoint Jul 19, 2024
86e520b
misc test cleanup
saucepoint Jul 19, 2024
e0ec23c
custom errors
saucepoint Jul 19, 2024
fc539e3
move magic hex to a constant
saucepoint Jul 19, 2024
095ad00
unpayable permit
saucepoint Jul 19, 2024
85390a6
use OZ EIP712
saucepoint Jul 20, 2024
b5d18dc
separate out UnorderedNonce into a reusable contract
saucepoint Jul 20, 2024
1521e70
move token URI to posm
saucepoint Jul 21, 2024
16939fb
add back in payable permit
saucepoint Jul 21, 2024
ed59b58
fix cherry picked commits
saucepoint Jul 26, 2024
b0559b8
remove public digest getter
saucepoint Jul 26, 2024
e458882
replace range with config naming
saucepoint Jul 26, 2024
88981c1
merge in main; regenerate gas
saucepoint Jul 30, 2024
9ba2556
deprecate old test: requiring permission to increase liq
saucepoint Jul 30, 2024
3cc8407
pr feedback
saucepoint Jul 30, 2024
a229cf8
borrow pertmi2 nonce tests for UnorderedNonce
saucepoint Jul 30, 2024
76a5e47
dedicated permit and approve testing for ERC721Permit
saucepoint Jul 30, 2024
cdde9e6
pr feedback: operator should be broadcaster of permit calls
saucepoint Jul 30, 2024
c0a26b8
reorganize permit hashing and verification
saucepoint Jul 30, 2024
834f331
merge in main; fix fuzz test for SelfPermit; regenerate gas
saucepoint Jul 30, 2024
019bad1
refactor ERC721Permit signature verification with generic signature c…
saucepoint Jul 30, 2024
0d445f2
remove deprecated library
saucepoint Jul 30, 2024
741e660
fix imports
saucepoint Jul 31, 2024
8dc0ca3
merge in main; regenerate gas
saucepoint Jul 31, 2024
85eae38
formatting
saucepoint Jul 31, 2024
ff5bb68
Merge branch 'main' into posm-erc721permit-signed
saucepoint Aug 1, 2024
d188aa2
merge main; regenerate gas
saucepoint Aug 1, 2024
600dd6d
pr feedback
saucepoint Aug 1, 2024
3cab51c
optimize nonce bit flipping
saucepoint Aug 1, 2024
b15da09
Merge branch 'main' into posm-erc721permit-signed
saucepoint Aug 1, 2024
750dcba
Merge branch 'main' into posm-erc721permit-signed
saucepoint Aug 2, 2024
5c566d6
merge in main; regenerate gas
saucepoint Aug 2, 2024
6eafa63
discard public PERMIT_TYPEHASH
saucepoint Aug 2, 2024
ce52a7f
merge in main; regenerate gas
saucepoint Aug 2, 2024
e58f516
Merge branch 'main' into posm-erc721permit-signed
saucepoint Aug 2, 2024
ff6e58c
renaming
saucepoint Aug 2, 2024
33832c0
library-ify bit flipping
saucepoint Aug 2, 2024
d59bd1d
merge in main; regenerate gas
saucepoint Aug 2, 2024
42c8268
merge in main; fix conflicts; regenerate gas
saucepoint Aug 2, 2024
57542bd
yall crazy for sending through the ringer
saucepoint Aug 2, 2024
778d7f3
merge in main; regenerate gas
saucepoint Aug 2, 2024
9021dd8
nits
saucepoint Aug 3, 2024
9732562
merge in main; regenerate gas
saucepoint Aug 3, 2024
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/PositionManager_permit.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
79569
79594
Original file line number Diff line number Diff line change
@@ -1 +1 @@
62481
62506
2 changes: 1 addition & 1 deletion .forge-snapshots/PositionManager_permit_twice.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
45381
45406
43 changes: 5 additions & 38 deletions src/base/UnorderedNonce.sol
Original file line number Diff line number Diff line change
@@ -1,53 +1,20 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.24;

import {UnorderedNonceLibrary} from "../libraries/UnorderedNonceLibrary.sol";

/// @title Unordered Nonce
/// @notice Contract state and methods for using unordered nonces in signatures
contract UnorderedNonce {
saucepoint marked this conversation as resolved.
Show resolved Hide resolved
error NonceAlreadyUsed();
using UnorderedNonceLibrary for *;

mapping(address owner => mapping(uint256 word => uint256 bitmap)) public nonces;
Copy link
Member

Choose a reason for hiding this comment

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

natspec that the word is actually max type(uint248).max


/// @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
function _useUnorderedNonce(address owner, uint256 nonce) internal {
saucepoint marked this conversation as resolved.
Show resolved Hide resolved
// consume the bit by flipping it in storage
// reverts if the bit was already spent
_flipBit(nonces[owner], nonce);
}

function _flipBit(mapping(uint256 => uint256) storage bitmap, uint256 nonce) private {
// equivalent to:
// uint256 wordPos = uint248(nonce >> 8);
// uint256 bitPos = uint8(nonce);
// uint256 bit = 1 << bitPos;
// uint256 flipped = nonces[owner][wordPos] ^= bit;
// if (flipped & bit == 0) revert NonceAlreadyUsed();

assembly ("memory-safe") {
// wordPos = uint248(nonce >> 8)
let wordPos := shr(8, nonce)

// bit = 1 << uint8(nonce)
let bit := shl(and(nonce, 0xFF), 1)

// slot of bitmap[wordPos] is keccak256(abi.encode(wordPos, bitmap.slot))
mstore(0, wordPos)
mstore(0x20, bitmap.slot)
let slot := keccak256(0, 0x40)

// uint256 previousBits = bitmap[wordPos]
let previousBits := sload(slot)

// revert if it's already been used
if eq(and(previousBits, bit), bit) {
mstore(0, 0x1fb09b80) // 4 bytes of NonceAlreadyUsed.selector
revert(0x1c, 0x04)
}

// bitmap[wordPos] = (previousBits | bit)
sstore(slot, or(previousBits, bit))
}
(uint256 wordPos, uint256 bitPos) = nonce.getBitmapPositions();
nonces[owner].flipBit(wordPos, bitPos);
Copy link
Member

Choose a reason for hiding this comment

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

is it more expensive if we did nonces[owner][wordPos].flipBit(bitPos)? can you see? i think i just like that abstraction a little better

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

its not as friendly since flipBit would calculate the new bitmap and return it so the caller can set it in storage

the alternative/cleaner syntax might be nonces[owner].spendNonce(nonce) where spendNonce does the wordPos/bitPos logic

Copy link
Member

Choose a reason for hiding this comment

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

hmmm

Copy link
Contributor

Choose a reason for hiding this comment

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

ok i misunderstood - now i see it lol
i agree that doing it on the word would be messy becaus eyou have to write

nonces[owner][wordPos] = nonces[owner][wordPos].flipBit(x)

Copy link
Member

Choose a reason for hiding this comment

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

i think i still prefer the abstraction where we were to change it to

nonces[owner][wordPos].flipTick bc the functions become more reusable (to extract bitPos and wordPos) from nonce, then to get the new bitmap...

buttt i get it if its too expensive

maybe we could still have nonces[owner].spendNonce() that still use other helpers for those two things? im just noticing that we have to rewrite helpers in tests to do the same thing weve written here/ I could see those functions being helpful generally in a library

Copy link
Member

Choose a reason for hiding this comment

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

Can you see how much gas is saved not doing it this way? @saucepoint

like is it significantly more expsenive to do

nonces[owner][wordPos] = nonces[owner][wordPos].flipTick(bitPos) ?

Copy link
Member

Choose a reason for hiding this comment

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

i will also say tho.. this is not a super heavily used codepath so we could just go back to the solidity version and be done w it, theres no need to hyper optimize this path

Copy link
Contributor

Choose a reason for hiding this comment

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

yeah im happy with that 🤷 no strong opinions

}
}
46 changes: 46 additions & 0 deletions src/libraries/UnorderedNonceLibrary.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// 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
library UnorderedNonceLibrary {
error NonceAlreadyUsed();

/// @notice Given an unordered nonce, return the word and bit position to set in a bitmap
function getBitmapPositions(uint256 nonce) internal pure returns (uint256 wordPos, uint256 bitPos) {
assembly {
wordPos := shr(8, nonce)
bitPos := and(nonce, 0xFF)
}
}

/// @notice Flip a bit in a bitmap, reverting if the bit was already set
function flipBit(mapping(uint256 => uint256) storage self, uint256 wordPos, uint256 bitPos) internal {
// equivalent to:
// uint256 bit = 1 << bitPos;
// uint256 flipped = nonces[owner][wordPos] ^= bit;
// if (flipped & bit == 0) revert NonceAlreadyUsed();

assembly ("memory-safe") {
// slot of self[wordPos] is keccak256(abi.encode(wordPos, self.slot))
mstore(0, wordPos)
mstore(0x20, self.slot)
let slot := keccak256(0, 0x40)

// uint256 previousBits = self[wordPos]
let previousBits := sload(slot)

// bit = 1 << bitPos
let bit := shl(bitPos, 1)

// revert if it's already been used
if eq(and(previousBits, bit), bit) {
mstore(0, 0x1fb09b80) // 4 bytes of NonceAlreadyUsed.selector
revert(0x1c, 0x04)
}

// self[wordPos] = (previousBits | bit)
sstore(slot, or(previousBits, bit))
}
}
}
5 changes: 3 additions & 2 deletions test/ERC721Permit.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {MockERC721Permit} from "./mocks/MockERC721Permit.sol";
import {IERC721Permit} from "../src/interfaces/IERC721Permit.sol";
import {IERC721} from "forge-std/interfaces/IERC721.sol";
import {UnorderedNonce} from "../src/base/UnorderedNonce.sol";
import {UnorderedNonceLibrary} from "../src/libraries/UnorderedNonceLibrary.sol";

contract ERC721PermitTest is Test {
MockERC721Permit erc721Permit;
Expand Down Expand Up @@ -165,7 +166,7 @@ contract ERC721PermitTest is Test {
bytes memory signature = abi.encodePacked(r, s, v);

vm.startPrank(alice);
vm.expectRevert(UnorderedNonce.NonceAlreadyUsed.selector);
vm.expectRevert(UnorderedNonceLibrary.NonceAlreadyUsed.selector);
erc721Permit.permit(bob, tokenIdAlice, block.timestamp, nonce, signature);
vm.stopPrank();
}
Expand All @@ -188,7 +189,7 @@ contract ERC721PermitTest is Test {
bytes memory signature = abi.encodePacked(r, s, v);

vm.startPrank(alice);
vm.expectRevert(UnorderedNonce.NonceAlreadyUsed.selector);
vm.expectRevert(UnorderedNonceLibrary.NonceAlreadyUsed.selector);
erc721Permit.permit(bob, tokenIdAlice2, block.timestamp, nonce, signature);
vm.stopPrank();
}
Expand Down
111 changes: 56 additions & 55 deletions test/UnorderedNonce.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pragma solidity ^0.8.20;

import "forge-std/Test.sol";
import {UnorderedNonce} from "../src/base/UnorderedNonce.sol";
import {UnorderedNonceLibrary} from "../src/libraries/UnorderedNonceLibrary.sol";
import {MockUnorderedNonce} from "./mocks/MockUnorderedNonce.sol";

contract UnorderedNonceTest is Test {
Expand All @@ -13,66 +14,66 @@ contract UnorderedNonceTest is Test {
}

function testLowNonces() public {
unorderedNonce.batchSpendNonces(address(this), 5);
unorderedNonce.batchSpendNonces(address(this), 0);
unorderedNonce.batchSpendNonces(address(this), 1);

vm.expectRevert(UnorderedNonce.NonceAlreadyUsed.selector);
unorderedNonce.batchSpendNonces(address(this), 1);
vm.expectRevert(UnorderedNonce.NonceAlreadyUsed.selector);
unorderedNonce.batchSpendNonces(address(this), 5);
vm.expectRevert(UnorderedNonce.NonceAlreadyUsed.selector);
unorderedNonce.batchSpendNonces(address(this), 0);
unorderedNonce.batchSpendNonces(address(this), 4);
unorderedNonce.spendNonce(address(this), 5);
unorderedNonce.spendNonce(address(this), 0);
unorderedNonce.spendNonce(address(this), 1);

vm.expectRevert(UnorderedNonceLibrary.NonceAlreadyUsed.selector);
unorderedNonce.spendNonce(address(this), 1);
vm.expectRevert(UnorderedNonceLibrary.NonceAlreadyUsed.selector);
unorderedNonce.spendNonce(address(this), 5);
vm.expectRevert(UnorderedNonceLibrary.NonceAlreadyUsed.selector);
unorderedNonce.spendNonce(address(this), 0);
unorderedNonce.spendNonce(address(this), 4);
}

function testNonceWordBoundary() public {
unorderedNonce.batchSpendNonces(address(this), 255);
unorderedNonce.batchSpendNonces(address(this), 256);
unorderedNonce.spendNonce(address(this), 255);
unorderedNonce.spendNonce(address(this), 256);

vm.expectRevert(UnorderedNonce.NonceAlreadyUsed.selector);
unorderedNonce.batchSpendNonces(address(this), 255);
vm.expectRevert(UnorderedNonce.NonceAlreadyUsed.selector);
unorderedNonce.batchSpendNonces(address(this), 256);
vm.expectRevert(UnorderedNonceLibrary.NonceAlreadyUsed.selector);
unorderedNonce.spendNonce(address(this), 255);
vm.expectRevert(UnorderedNonceLibrary.NonceAlreadyUsed.selector);
unorderedNonce.spendNonce(address(this), 256);
}

function testHighNonces() public {
unorderedNonce.batchSpendNonces(address(this), 2 ** 240);
unorderedNonce.batchSpendNonces(address(this), 2 ** 240 + 1);
unorderedNonce.spendNonce(address(this), 2 ** 240);
unorderedNonce.spendNonce(address(this), 2 ** 240 + 1);

vm.expectRevert(UnorderedNonce.NonceAlreadyUsed.selector);
unorderedNonce.batchSpendNonces(address(this), 2 ** 240);
vm.expectRevert(UnorderedNonce.NonceAlreadyUsed.selector);
unorderedNonce.batchSpendNonces(address(this), 2 ** 240 + 1);
vm.expectRevert(UnorderedNonceLibrary.NonceAlreadyUsed.selector);
unorderedNonce.spendNonce(address(this), 2 ** 240);
vm.expectRevert(UnorderedNonceLibrary.NonceAlreadyUsed.selector);
unorderedNonce.spendNonce(address(this), 2 ** 240 + 1);

unorderedNonce.batchSpendNonces(address(this), 2 ** 240 + 2);
unorderedNonce.spendNonce(address(this), 2 ** 240 + 2);
}

function testInvalidateFullWord() public {
unorderedNonce.invalidateUnorderedNonces(0, 2 ** 256 - 1);

vm.expectRevert(UnorderedNonce.NonceAlreadyUsed.selector);
unorderedNonce.batchSpendNonces(address(this), 0);
vm.expectRevert(UnorderedNonce.NonceAlreadyUsed.selector);
unorderedNonce.batchSpendNonces(address(this), 1);
vm.expectRevert(UnorderedNonce.NonceAlreadyUsed.selector);
unorderedNonce.batchSpendNonces(address(this), 254);
vm.expectRevert(UnorderedNonce.NonceAlreadyUsed.selector);
unorderedNonce.batchSpendNonces(address(this), 255);
unorderedNonce.batchSpendNonces(address(this), 256);
unorderedNonce.batchSpendNonces(0, 2 ** 256 - 1);

vm.expectRevert(UnorderedNonceLibrary.NonceAlreadyUsed.selector);
unorderedNonce.spendNonce(address(this), 0);
vm.expectRevert(UnorderedNonceLibrary.NonceAlreadyUsed.selector);
unorderedNonce.spendNonce(address(this), 1);
vm.expectRevert(UnorderedNonceLibrary.NonceAlreadyUsed.selector);
unorderedNonce.spendNonce(address(this), 254);
vm.expectRevert(UnorderedNonceLibrary.NonceAlreadyUsed.selector);
unorderedNonce.spendNonce(address(this), 255);
unorderedNonce.spendNonce(address(this), 256);
}

function testInvalidateNonzeroWord() public {
unorderedNonce.invalidateUnorderedNonces(1, 2 ** 256 - 1);

unorderedNonce.batchSpendNonces(address(this), 0);
unorderedNonce.batchSpendNonces(address(this), 254);
unorderedNonce.batchSpendNonces(address(this), 255);
vm.expectRevert(UnorderedNonce.NonceAlreadyUsed.selector);
unorderedNonce.batchSpendNonces(address(this), 256);
vm.expectRevert(UnorderedNonce.NonceAlreadyUsed.selector);
unorderedNonce.batchSpendNonces(address(this), 511);
unorderedNonce.batchSpendNonces(address(this), 512);
unorderedNonce.batchSpendNonces(1, 2 ** 256 - 1);

unorderedNonce.spendNonce(address(this), 0);
unorderedNonce.spendNonce(address(this), 254);
unorderedNonce.spendNonce(address(this), 255);
vm.expectRevert(UnorderedNonceLibrary.NonceAlreadyUsed.selector);
unorderedNonce.spendNonce(address(this), 256);
vm.expectRevert(UnorderedNonceLibrary.NonceAlreadyUsed.selector);
unorderedNonce.spendNonce(address(this), 511);
unorderedNonce.spendNonce(address(this), 512);
}

function test_fuzz_InvalidateNonzeroWord(uint256 word, uint256 nonce) public {
Expand All @@ -81,30 +82,30 @@ contract UnorderedNonceTest is Test {
// word = 0, bits [0, 256)
// word = 1, bits [256, 512)
// word = 2, bits [512, 768), etc
unorderedNonce.invalidateUnorderedNonces(word, 2 ** 256 - 1);
unorderedNonce.batchSpendNonces(word, 2 ** 256 - 1);

// bound the nonce to be from 0 to 256 bits after the word
nonce = bound(nonce, 0, (word + 2) * 256);

if ((word * 256) <= nonce && nonce < ((word + 1) * 256)) {
vm.expectRevert(UnorderedNonce.NonceAlreadyUsed.selector);
vm.expectRevert(UnorderedNonceLibrary.NonceAlreadyUsed.selector);
}
unorderedNonce.batchSpendNonces(address(this), nonce);
unorderedNonce.spendNonce(address(this), nonce);
}

function test_fuzz_UsingNonceTwiceFails(uint256 nonce) public {
unorderedNonce.batchSpendNonces(address(this), nonce);
vm.expectRevert(UnorderedNonce.NonceAlreadyUsed.selector);
unorderedNonce.batchSpendNonces(address(this), nonce);
unorderedNonce.spendNonce(address(this), nonce);
vm.expectRevert(UnorderedNonceLibrary.NonceAlreadyUsed.selector);
unorderedNonce.spendNonce(address(this), nonce);
}

function test_fuzz_UseTwoRandomNonces(uint256 first, uint256 second) public {
unorderedNonce.batchSpendNonces(address(this), first);
unorderedNonce.spendNonce(address(this), first);
if (first == second) {
vm.expectRevert(UnorderedNonce.NonceAlreadyUsed.selector);
unorderedNonce.batchSpendNonces(address(this), second);
vm.expectRevert(UnorderedNonceLibrary.NonceAlreadyUsed.selector);
unorderedNonce.spendNonce(address(this), second);
} else {
unorderedNonce.batchSpendNonces(address(this), second);
unorderedNonce.spendNonce(address(this), second);
}
}
}
50 changes: 50 additions & 0 deletions test/libraries/UnorderedNonceLibrary.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "forge-std/Test.sol";
import {UnorderedNonceLibrary} from "../../src/libraries/UnorderedNonceLibrary.sol";

contract UnorderedNonceLibraryTest is Test {
using UnorderedNonceLibrary for *;

mapping(uint256 word => uint256 bitmap) public nonces0;
mapping(uint256 word => uint256 bitmap) public nonces1;

/// @dev test getBitmapPositions() is returning the correct word and bit positions
function test_getBitmapPositions(uint256 nonce) public pure {
(uint256 wordPos, uint256 bitPos) = nonce.getBitmapPositions();

assertEq(wordPos, nonce >> 8);
assertEq(bitPos, nonce & 0xFF);
}

/// @dev test flipBit() is changing the bit as expected
function test_flipBit(uint256 nonce) public {
(uint256 wordPos, uint256 bitPos) = nonce.getBitmapPositions();
nonces0.flipBit(wordPos, bitPos);

uint256 bit = 1 << bitPos;
assertEq(nonces0[wordPos], bit);
}

/// @dev test flipBit()'s assembly is equivalent to manually flipping the bit
function test_flipBit_equivalence(uint256 nonce) public {
(uint256 wordPos0, uint256 bitPos0) = nonce.getBitmapPositions();
nonces0.flipBit(wordPos0, bitPos0);

// manually flip nonce without assembly
(uint256 wordPos1, uint256 bitPos1) = _flipNonce(nonce);

assertEq(wordPos0, wordPos1);
assertEq(bitPos0, bitPos1);
assertEq(nonces0[wordPos0], nonces1[wordPos1]);
}

function _flipNonce(uint256 nonce) internal returns (uint256 wordPos, uint256 bitPos) {
wordPos = nonce >> 8;
bitPos = uint8(nonce);

uint256 bit = 1 << bitPos;
nonces1[wordPos] ^= bit;
}
}
4 changes: 2 additions & 2 deletions test/mocks/MockUnorderedNonce.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ pragma solidity ^0.8.20;
import {UnorderedNonce} from "../../src/base/UnorderedNonce.sol";

contract MockUnorderedNonce is UnorderedNonce {
function batchSpendNonces(address owner, uint256 nonce) external {
function spendNonce(address owner, uint256 nonce) external {
_useUnorderedNonce(owner, nonce);
}

/// @dev Bulk-spend nonces on a single word. FOR TESTING ONLY
function invalidateUnorderedNonces(uint256 wordPos, uint256 mask) external {
function batchSpendNonces(uint256 wordPos, uint256 mask) external {
nonces[msg.sender][wordPos] |= mask;
}
}
Loading