Files
v4-periphery/test/erc721Permit/ERC721Permit.permit.t.sol

298 lines
11 KiB
Solidity

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "forge-std/Test.sol";
import {SignatureVerification} from "permit2/src/libraries/SignatureVerification.sol";
import {ERC721PermitHash} from "../../src/libraries/ERC721PermitHash.sol";
import {MockERC721Permit} from "../mocks/MockERC721Permit.sol";
import {IERC721Permit_v4} from "../../src/interfaces/IERC721Permit_v4.sol";
import {IERC721} from "forge-std/interfaces/IERC721.sol";
import {IUnorderedNonce} from "../../src/interfaces/IUnorderedNonce.sol";
contract ERC721PermitTest is Test {
MockERC721Permit erc721Permit;
address alice;
uint256 alicePK;
address bob;
uint256 bobPK;
string constant name = "Mock ERC721Permit_v4";
string constant symbol = "MOCK721";
function setUp() public {
(alice, alicePK) = makeAddrAndKey("ALICE");
(bob, bobPK) = makeAddrAndKey("BOB");
erc721Permit = new MockERC721Permit(name, symbol);
}
// --- Test the overriden approval ---
function test_fuzz_approve(address spender) public {
uint256 tokenId = erc721Permit.mint();
assertEq(erc721Permit.getApproved(tokenId), address(0));
vm.expectEmit(true, true, true, true, address(erc721Permit));
emit IERC721.Approval(address(this), spender, tokenId);
erc721Permit.approve(spender, tokenId);
assertEq(erc721Permit.getApproved(tokenId), spender);
}
function test_fuzz_approvedOperator_reapproves(address operator, address spender) public {
uint256 tokenId = erc721Permit.mint();
erc721Permit.setApprovalForAll(operator, true);
assertEq(erc721Permit.isApprovedForAll(address(this), operator), true);
assertEq(erc721Permit.getApproved(tokenId), address(0));
vm.startPrank(operator);
vm.expectEmit(true, true, true, true, address(erc721Permit));
emit IERC721.Approval(address(this), spender, tokenId);
erc721Permit.approve(spender, tokenId);
vm.stopPrank();
assertEq(erc721Permit.getApproved(tokenId), spender);
}
function test_fuzz_approve_unauthorizedRevert(address caller) public {
uint256 tokenId = erc721Permit.mint();
vm.prank(caller);
if (caller != address(this)) vm.expectRevert(IERC721Permit_v4.Unauthorized.selector);
erc721Permit.approve(address(this), tokenId);
}
// --- Test the signature-based approvals (permit) ---
function test_permitTypeHash() public pure {
assertEq(
ERC721PermitHash.PERMIT_TYPEHASH,
keccak256("Permit(address spender,uint256 tokenId,uint256 nonce,uint256 deadline)")
);
}
function test_fuzz_permitHash(address spender, uint256 tokenId, uint256 nonce, uint256 deadline) public pure {
bytes32 expectedHash =
keccak256(abi.encode(ERC721PermitHash.PERMIT_TYPEHASH, spender, tokenId, nonce, deadline));
assertEq(expectedHash, ERC721PermitHash.hashPermit(spender, tokenId, nonce, deadline));
}
function test_domainSeparator() public view {
assertEq(
erc721Permit.DOMAIN_SEPARATOR(),
keccak256(
abi.encode(
keccak256("EIP712Domain(string name,uint256 chainId,address verifyingContract)"),
keccak256(bytes(name)),
block.chainid,
address(erc721Permit)
)
)
);
}
/// @dev spender uses alice's signature to approve itself
function test_fuzz_erc721permit_spender(address spender) public {
vm.assume(spender != alice);
vm.prank(alice);
uint256 tokenId = erc721Permit.mint();
uint256 nonce = 1;
bytes32 digest = _getPermitDigest(spender, tokenId, nonce, block.timestamp);
(uint8 v, bytes32 r, bytes32 s) = vm.sign(alicePK, digest);
bytes memory signature = abi.encodePacked(r, s, v);
// no approvals existed
assertEq(erc721Permit.getApproved(tokenId), address(0));
assertEq(erc721Permit.isApprovedForAll(alice, spender), false);
// nonce was unspent
(uint256 wordPos, uint256 bitPos) = _getBitmapFromNonce(nonce);
assertEq(erc721Permit.nonces(alice, wordPos) & (1 << bitPos), 0);
// -- Permit -- //
vm.startPrank(spender);
vm.expectEmit(true, true, true, true, address(erc721Permit));
emit IERC721.Approval(alice, spender, tokenId);
erc721Permit.permit(spender, tokenId, block.timestamp, nonce, signature);
vm.stopPrank();
// approvals set
assertEq(erc721Permit.getApproved(tokenId), spender);
assertEq(erc721Permit.isApprovedForAll(alice, spender), false);
// nonce was spent
assertEq(erc721Permit.nonces(alice, wordPos) & (1 << bitPos), 2); // 2 = 0010
}
/// @dev a third party caller uses alice's signature to give `spender` the approval
function test_fuzz_erc721permit_caller(address caller, address spender) public {
vm.assume(spender != alice);
vm.prank(alice);
uint256 tokenId = erc721Permit.mint();
uint256 nonce = 1;
bytes32 digest = _getPermitDigest(spender, tokenId, nonce, block.timestamp);
(uint8 v, bytes32 r, bytes32 s) = vm.sign(alicePK, digest);
bytes memory signature = abi.encodePacked(r, s, v);
// no approvals existed
assertEq(erc721Permit.getApproved(tokenId), address(0));
assertEq(erc721Permit.isApprovedForAll(alice, spender), false);
// nonce was unspent
(uint256 wordPos, uint256 bitPos) = _getBitmapFromNonce(nonce);
assertEq(erc721Permit.nonces(alice, wordPos) & (1 << bitPos), 0);
// -- Permit by third-party caller -- //
vm.startPrank(caller);
vm.expectEmit(true, true, true, true, address(erc721Permit));
emit IERC721.Approval(alice, spender, tokenId);
erc721Permit.permit(spender, tokenId, block.timestamp, nonce, signature);
vm.stopPrank();
// approvals set
assertEq(erc721Permit.getApproved(tokenId), spender);
assertEq(erc721Permit.isApprovedForAll(alice, spender), false);
// nonce was spent
assertEq(erc721Permit.nonces(alice, wordPos) & (1 << bitPos), 2); // 2 = 0010
}
function test_fuzz_erc721permit_nonceAlreadyUsed() public {
vm.prank(alice);
uint256 tokenIdAlice = erc721Permit.mint();
// alice gives bob operator permissions
uint256 nonce = 1;
_permit(alicePK, tokenIdAlice, bob, nonce);
// alice cannot reuse the nonce
bytes32 digest = _getPermitDigest(bob, tokenIdAlice, nonce, block.timestamp);
(uint8 v, bytes32 r, bytes32 s) = vm.sign(alicePK, digest);
bytes memory signature = abi.encodePacked(r, s, v);
vm.startPrank(alice);
vm.expectRevert(IUnorderedNonce.NonceAlreadyUsed.selector);
erc721Permit.permit(bob, tokenIdAlice, block.timestamp, nonce, signature);
vm.stopPrank();
}
function test_fuzz_erc721permit_nonceAlreadyUsed_twoPositions() public {
vm.prank(alice);
uint256 tokenIdAlice = erc721Permit.mint();
vm.prank(alice);
uint256 tokenIdAlice2 = erc721Permit.mint();
// alice gives bob operator permissions for first token
uint256 nonce = 1;
_permit(alicePK, tokenIdAlice, bob, nonce);
// alice cannot reuse the nonce for the second token
bytes32 digest = _getPermitDigest(bob, tokenIdAlice2, nonce, block.timestamp);
(uint8 v, bytes32 r, bytes32 s) = vm.sign(alicePK, digest);
bytes memory signature = abi.encodePacked(r, s, v);
vm.startPrank(alice);
vm.expectRevert(IUnorderedNonce.NonceAlreadyUsed.selector);
erc721Permit.permit(bob, tokenIdAlice2, block.timestamp, nonce, signature);
vm.stopPrank();
}
function test_fuzz_erc721permit_unauthorized() public {
vm.prank(alice);
uint256 tokenId = erc721Permit.mint();
uint256 nonce = 1;
bytes32 digest = _getPermitDigest(bob, tokenId, nonce, block.timestamp);
// bob attempts signing an approval for himself
(uint8 v, bytes32 r, bytes32 s) = vm.sign(bobPK, digest);
bytes memory signature = abi.encodePacked(r, s, v);
// approvals unset
assertEq(erc721Permit.getApproved(tokenId), address(0));
assertEq(erc721Permit.isApprovedForAll(alice, bob), false);
// nonce was unspent
(uint256 wordPos, uint256 bitPos) = _getBitmapFromNonce(nonce);
assertEq(erc721Permit.nonces(alice, wordPos) & (1 << bitPos), 0);
vm.startPrank(bob);
vm.expectRevert(SignatureVerification.InvalidSigner.selector);
erc721Permit.permit(bob, tokenId, block.timestamp, nonce, signature);
vm.stopPrank();
// approvals unset
assertEq(erc721Permit.getApproved(tokenId), address(0));
assertEq(erc721Permit.isApprovedForAll(alice, bob), false);
// nonce was unspent
assertEq(erc721Permit.nonces(alice, wordPos) & (1 << bitPos), 0);
}
function test_fuzz_erc721Permit_SignatureDeadlineExpired(address spender) public {
vm.prank(alice);
uint256 tokenId = erc721Permit.mint();
uint256 nonce = 1;
uint256 deadline = vm.getBlockTimestamp();
bytes32 digest = _getPermitDigest(spender, tokenId, nonce, deadline);
(uint8 v, bytes32 r, bytes32 s) = vm.sign(alicePK, digest);
bytes memory signature = abi.encodePacked(r, s, v);
// no approvals existed
assertEq(erc721Permit.getApproved(tokenId), address(0));
assertEq(erc721Permit.isApprovedForAll(alice, spender), false);
// nonce was unspent
(uint256 wordPos, uint256 bitPos) = _getBitmapFromNonce(nonce);
assertEq(erc721Permit.nonces(alice, wordPos) & (1 << bitPos), 0);
// fast forward to exceed deadline
skip(1);
// -- Permit but deadline expired -- //
vm.startPrank(spender);
vm.expectRevert(IERC721Permit_v4.SignatureDeadlineExpired.selector);
erc721Permit.permit(spender, tokenId, deadline, nonce, signature);
vm.stopPrank();
// approvals unset
assertEq(erc721Permit.getApproved(tokenId), address(0));
assertEq(erc721Permit.isApprovedForAll(alice, spender), false);
// nonce was unspent
assertEq(erc721Permit.nonces(alice, wordPos) & (1 << bitPos), 0);
}
// Helpers related to permit
function _permit(uint256 privateKey, uint256 tokenId, address operator, uint256 nonce) internal {
bytes32 digest = _getPermitDigest(operator, tokenId, nonce, block.timestamp);
(uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest);
bytes memory signature = abi.encodePacked(r, s, v);
vm.prank(operator);
erc721Permit.permit(operator, tokenId, block.timestamp, nonce, signature);
}
function _getPermitDigest(address spender, uint256 tokenId, uint256 nonce, uint256 deadline)
internal
view
returns (bytes32 digest)
{
digest = keccak256(
abi.encodePacked(
"\x19\x01",
erc721Permit.DOMAIN_SEPARATOR(),
keccak256(abi.encode(ERC721PermitHash.PERMIT_TYPEHASH, spender, tokenId, nonce, deadline))
)
);
}
// copied the private function from UnorderedNonce.sol
function _getBitmapFromNonce(uint256 nonce) private pure returns (uint256 wordPos, uint256 bitPos) {
wordPos = uint248(nonce >> 8);
bitPos = uint8(nonce);
}
}