mirror of
https://github.com/lux-amm/v4-periphery.git
synced 2026-10-30 20:54:39 +00:00
364 lines
14 KiB
Solidity
364 lines
14 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 ERC721PermitForAllTest 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 setApprovalForAll ---
|
|
function test_fuzz_setApprovalForAll(address operator) public {
|
|
assertEq(erc721Permit.isApprovedForAll(address(this), operator), false);
|
|
|
|
vm.expectEmit(true, true, true, true, address(erc721Permit));
|
|
emit IERC721.ApprovalForAll(address(this), operator, true);
|
|
erc721Permit.setApprovalForAll(operator, true);
|
|
assertEq(erc721Permit.isApprovedForAll(address(this), operator), true);
|
|
}
|
|
|
|
function test_fuzz_setApprovalForAll_revoke(address operator) public {
|
|
assertEq(erc721Permit.isApprovedForAll(address(this), operator), false);
|
|
erc721Permit.setApprovalForAll(operator, true);
|
|
assertEq(erc721Permit.isApprovedForAll(address(this), operator), true);
|
|
|
|
vm.expectEmit(true, true, true, true, address(erc721Permit));
|
|
emit IERC721.ApprovalForAll(address(this), operator, false);
|
|
erc721Permit.setApprovalForAll(operator, false);
|
|
assertEq(erc721Permit.isApprovedForAll(address(this), operator), false);
|
|
}
|
|
|
|
// --- Test the signature-based approvals (permitForAll) ---
|
|
function test_permitForAllTypeHash() public pure {
|
|
assertEq(
|
|
ERC721PermitHash.PERMIT_FOR_ALL_TYPEHASH,
|
|
keccak256("PermitForAll(address operator,bool approved,uint256 nonce,uint256 deadline)")
|
|
);
|
|
}
|
|
|
|
function test_fuzz_permitForAllHash(address operator, bool approved, uint256 nonce, uint256 deadline) public pure {
|
|
bytes32 expectedHash =
|
|
keccak256(abi.encode(ERC721PermitHash.PERMIT_FOR_ALL_TYPEHASH, operator, approved, nonce, deadline));
|
|
assertEq(expectedHash, ERC721PermitHash.hashPermitForAll(operator, approved, nonce, deadline));
|
|
}
|
|
|
|
/// @dev operator uses alice's signature to approve itself
|
|
function test_fuzz_erc721permitForAll_operator(address operator) public {
|
|
vm.assume(operator != alice);
|
|
vm.prank(alice);
|
|
uint256 tokenId = erc721Permit.mint();
|
|
|
|
uint256 nonce = 1;
|
|
bytes32 digest = _getPermitForAllDigest(operator, true, 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, operator), false);
|
|
|
|
// nonce was unspent
|
|
(uint256 wordPos, uint256 bitPos) = _getBitmapFromNonce(nonce);
|
|
assertEq(erc721Permit.nonces(alice, wordPos) & (1 << bitPos), 0);
|
|
|
|
// -- PermitForAll -- //
|
|
vm.startPrank(operator);
|
|
vm.expectEmit(true, true, true, true, address(erc721Permit));
|
|
emit IERC721.ApprovalForAll(alice, operator, true);
|
|
erc721Permit.permitForAll(alice, operator, true, block.timestamp, nonce, signature);
|
|
vm.stopPrank();
|
|
|
|
// approvals set
|
|
assertEq(erc721Permit.getApproved(tokenId), address(0));
|
|
assertEq(erc721Permit.isApprovedForAll(alice, operator), true);
|
|
|
|
// nonce was spent
|
|
assertEq(erc721Permit.nonces(alice, wordPos) & (1 << bitPos), 2); // 2 = 0010
|
|
}
|
|
|
|
/// @dev a third party caller uses alice's signature to give `operator` the approval
|
|
function test_fuzz_erc721permitForAll_caller(address caller, address operator) public {
|
|
vm.assume(operator != alice);
|
|
vm.prank(alice);
|
|
uint256 tokenId = erc721Permit.mint();
|
|
|
|
uint256 nonce = 1;
|
|
bytes32 digest = _getPermitForAllDigest(operator, true, 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, operator), false);
|
|
|
|
// nonce was unspent
|
|
(uint256 wordPos, uint256 bitPos) = _getBitmapFromNonce(nonce);
|
|
assertEq(erc721Permit.nonces(alice, wordPos) & (1 << bitPos), 0);
|
|
|
|
// -- PermitForAll -- //
|
|
vm.startPrank(caller);
|
|
vm.expectEmit(true, true, true, true, address(erc721Permit));
|
|
emit IERC721.ApprovalForAll(alice, operator, true);
|
|
erc721Permit.permitForAll(alice, operator, true, block.timestamp, nonce, signature);
|
|
vm.stopPrank();
|
|
|
|
// approvals set
|
|
assertEq(erc721Permit.getApproved(tokenId), address(0));
|
|
assertEq(erc721Permit.isApprovedForAll(alice, operator), true);
|
|
|
|
// nonce was spent
|
|
assertEq(erc721Permit.nonces(alice, wordPos) & (1 << bitPos), 2); // 2 = 0010
|
|
}
|
|
|
|
function test_fuzz_erc721permitForAll_nonceAlreadyUsed(uint256 nonce) public {
|
|
// alice gives bob operator permissions
|
|
_permitForAll(alicePK, alice, bob, true, nonce);
|
|
|
|
// alice cannot reuse the nonce
|
|
bytes32 digest = _getPermitForAllDigest(bob, true, 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.permitForAll(alice, bob, true, block.timestamp, nonce, signature);
|
|
vm.stopPrank();
|
|
}
|
|
|
|
function test_fuzz_erc721permitForAll_invalidSigner(uint256 nonce) public {
|
|
bytes32 digest = _getPermitForAllDigest(bob, true, 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.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.permitForAll(alice, bob, true, block.timestamp, nonce, signature);
|
|
vm.stopPrank();
|
|
|
|
// approvals unset
|
|
assertEq(erc721Permit.isApprovedForAll(alice, bob), false);
|
|
|
|
// nonce was unspent
|
|
assertEq(erc721Permit.nonces(alice, wordPos) & (1 << bitPos), 0);
|
|
}
|
|
|
|
function test_fuzz_erc721permitForAll_SignatureDeadlineExpired(address operator) public {
|
|
uint256 nonce = 1;
|
|
uint256 deadline = vm.getBlockTimestamp();
|
|
bytes32 digest = _getPermitForAllDigest(operator, true, 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.isApprovedForAll(alice, operator), 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);
|
|
|
|
// -- PermitForAll but deadline expired -- //
|
|
vm.startPrank(operator);
|
|
vm.expectRevert(IERC721Permit_v4.SignatureDeadlineExpired.selector);
|
|
erc721Permit.permitForAll(alice, operator, true, deadline, nonce, signature);
|
|
vm.stopPrank();
|
|
|
|
// approvals unset
|
|
assertEq(erc721Permit.isApprovedForAll(alice, operator), false);
|
|
|
|
// nonce was unspent
|
|
assertEq(erc721Permit.nonces(alice, wordPos) & (1 << bitPos), 0);
|
|
}
|
|
|
|
/// @dev a signature for permit() cannot be used for permitForAll()
|
|
function test_fuzz_erc721Permit_invalidSignatureForAll(address operator) public {
|
|
vm.prank(alice);
|
|
uint256 tokenId = erc721Permit.mint();
|
|
|
|
uint256 nonce = 1;
|
|
uint256 deadline = block.timestamp;
|
|
bytes32 digest = _getPermitDigest(operator, tokenId, nonce, deadline);
|
|
|
|
// alice signs a permit for operator
|
|
(uint8 v, bytes32 r, bytes32 s) = vm.sign(alicePK, digest);
|
|
bytes memory signature = abi.encodePacked(r, s, v);
|
|
|
|
// approvals unset
|
|
assertEq(erc721Permit.isApprovedForAll(alice, bob), false);
|
|
|
|
// nonce was unspent
|
|
(uint256 wordPos, uint256 bitPos) = _getBitmapFromNonce(nonce);
|
|
assertEq(erc721Permit.nonces(alice, wordPos) & (1 << bitPos), 0);
|
|
|
|
// signature does not work with permitForAll
|
|
vm.startPrank(bob);
|
|
vm.expectRevert(SignatureVerification.InvalidSigner.selector);
|
|
erc721Permit.permitForAll(alice, bob, true, deadline, nonce, signature);
|
|
vm.stopPrank();
|
|
|
|
// approvals unset
|
|
assertEq(erc721Permit.isApprovedForAll(alice, bob), false);
|
|
|
|
// nonce was unspent
|
|
assertEq(erc721Permit.nonces(alice, wordPos) & (1 << bitPos), 0);
|
|
}
|
|
|
|
/// @dev a signature for permitForAll() cannot be used for permit()
|
|
function test_fuzz_erc721PermitForAll_invalidSignatureForPermit(address operator) public {
|
|
vm.prank(alice);
|
|
uint256 tokenId = erc721Permit.mint();
|
|
|
|
uint256 nonce = 1;
|
|
uint256 deadline = block.timestamp;
|
|
bytes32 digest = _getPermitForAllDigest(operator, true, nonce, deadline);
|
|
|
|
// alice signs a permit for operator
|
|
(uint8 v, bytes32 r, bytes32 s) = vm.sign(alicePK, digest);
|
|
bytes memory signature = abi.encodePacked(r, s, v);
|
|
|
|
// approvals unset
|
|
assertEq(erc721Permit.getApproved(tokenId), address(0));
|
|
|
|
// nonce was unspent
|
|
(uint256 wordPos, uint256 bitPos) = _getBitmapFromNonce(nonce);
|
|
assertEq(erc721Permit.nonces(alice, wordPos) & (1 << bitPos), 0);
|
|
|
|
// signature does not work with permit
|
|
vm.startPrank(bob);
|
|
vm.expectRevert(SignatureVerification.InvalidSigner.selector);
|
|
erc721Permit.permit(bob, tokenId, deadline, nonce, signature);
|
|
vm.stopPrank();
|
|
|
|
// approvals unset
|
|
assertEq(erc721Permit.getApproved(tokenId), address(0));
|
|
|
|
// nonce was unspent
|
|
assertEq(erc721Permit.nonces(alice, wordPos) & (1 << bitPos), 0);
|
|
}
|
|
|
|
/// @dev a nonce used in permit is unusable for permitForAll
|
|
function test_fuzz_erc721PermitForAll_permitNonceUsed(uint256 nonce) public {
|
|
vm.prank(alice);
|
|
uint256 tokenId = erc721Permit.mint();
|
|
|
|
uint256 deadline = block.timestamp;
|
|
bytes32 digest = _getPermitDigest(bob, tokenId, nonce, deadline);
|
|
// alice signs a permit for bob
|
|
(uint8 v, bytes32 r, bytes32 s) = vm.sign(alicePK, digest);
|
|
bytes memory signature = abi.encodePacked(r, s, v);
|
|
|
|
// bob gives himself approval
|
|
vm.prank(bob);
|
|
erc721Permit.permit(bob, tokenId, deadline, nonce, signature);
|
|
assertEq(erc721Permit.getApproved(tokenId), bob);
|
|
assertEq(erc721Permit.isApprovedForAll(alice, bob), false);
|
|
|
|
// alice tries re-using the nonce for permitForAll
|
|
digest = _getPermitForAllDigest(bob, true, nonce, deadline);
|
|
(v, r, s) = vm.sign(alicePK, digest);
|
|
signature = abi.encodePacked(r, s, v);
|
|
|
|
// Nonce does not work with permitForAll
|
|
vm.startPrank(bob);
|
|
vm.expectRevert(IUnorderedNonce.NonceAlreadyUsed.selector);
|
|
erc721Permit.permitForAll(alice, bob, true, deadline, nonce, signature);
|
|
vm.stopPrank();
|
|
}
|
|
|
|
/// @notice revoking a nonce prevents it from being used in permitForAll()
|
|
function test_fuzz_erc721PermitForAll_revokedNonceUsed(uint256 nonce) public {
|
|
// alice revokes the nonce
|
|
vm.prank(alice);
|
|
erc721Permit.revokeNonce(nonce);
|
|
|
|
uint256 deadline = block.timestamp;
|
|
bytes32 digest = _getPermitForAllDigest(bob, true, nonce, deadline);
|
|
// alice signs a permit for bob
|
|
(uint8 v, bytes32 r, bytes32 s) = vm.sign(alicePK, digest);
|
|
bytes memory signature = abi.encodePacked(r, s, v);
|
|
|
|
// Nonce does not work with permitForAll
|
|
vm.startPrank(bob);
|
|
vm.expectRevert(IUnorderedNonce.NonceAlreadyUsed.selector);
|
|
erc721Permit.permitForAll(alice, bob, true, deadline, nonce, signature);
|
|
vm.stopPrank();
|
|
}
|
|
|
|
// Helpers related to permitForAll
|
|
function _permitForAll(uint256 privateKey, address owner, address operator, bool approved, uint256 nonce) internal {
|
|
bytes32 digest = _getPermitForAllDigest(operator, approved, 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.permitForAll(owner, operator, approved, block.timestamp, nonce, signature);
|
|
}
|
|
|
|
function _getPermitForAllDigest(address operator, bool approved, uint256 nonce, uint256 deadline)
|
|
internal
|
|
view
|
|
returns (bytes32 digest)
|
|
{
|
|
digest = keccak256(
|
|
abi.encodePacked(
|
|
"\x19\x01",
|
|
erc721Permit.DOMAIN_SEPARATOR(),
|
|
keccak256(abi.encode(ERC721PermitHash.PERMIT_FOR_ALL_TYPEHASH, operator, approved, nonce, deadline))
|
|
)
|
|
);
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|