-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat: Add ERC721PermitLib * feat: Add view for permitCheck * feat: Change function parameter by mixedcase rule * feat: Add test cases for ERC721Permit
- Loading branch information
1 parent
50c89f4
commit 5fe0d6b
Showing
6 changed files
with
236 additions
and
49 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
// SPDX-License-Identifier: GPL-2.0-or-later | ||
// Copyright (C) 2024 PancakeSwap | ||
pragma solidity ^0.8.19; | ||
|
||
import {Address} from "@openzeppelin/contracts/utils/Address.sol"; | ||
import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol"; | ||
|
||
library ERC721PermitLib { | ||
error PermitExpired(); | ||
error ApproveToOneself(); | ||
error Unauthorized(); | ||
error InvalidSignature(); | ||
|
||
/// @dev Value is equal to keccak256("Permit(address spender,uint256 tokenId,uint256 nonce,uint256 deadline)"); | ||
bytes32 public constant PERMIT_TYPEHASH = 0x49ecf333e5b8c95c40fdafc95c1ad136e8914a8fb55e9dc8bb01eaa83a2df9ad; | ||
|
||
function DOMAIN_SEPARATOR(bytes32 nameHash, bytes32 versionHash) external view returns (bytes32) { | ||
return keccak256( | ||
abi.encode( | ||
/// @dev keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)') | ||
0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f, | ||
nameHash, | ||
versionHash, | ||
block.chainid, | ||
address(this) | ||
) | ||
); | ||
} | ||
|
||
function permitCheck( | ||
address spender, | ||
uint256 tokenId, | ||
uint256 deadline, | ||
uint8 v, | ||
bytes32 r, | ||
bytes32 s, | ||
address owner, | ||
bytes32 domainSeparatorHash, | ||
uint256 nonce | ||
) external view { | ||
if (block.timestamp > deadline) { | ||
revert PermitExpired(); | ||
} | ||
|
||
bytes32 digest = keccak256( | ||
abi.encodePacked( | ||
"\x19\x01", | ||
domainSeparatorHash, | ||
keccak256(abi.encode(PERMIT_TYPEHASH, spender, tokenId, nonce, deadline)) | ||
) | ||
); | ||
if (spender == owner) { | ||
revert ApproveToOneself(); | ||
} | ||
|
||
if (Address.isContract(owner)) { | ||
/// @dev cast 4 isValidSignature(bytes32,bytes) == 0x1626ba7e | ||
if (IERC1271(owner).isValidSignature(digest, abi.encodePacked(r, s, v)) == 0x1626ba7e) { | ||
revert Unauthorized(); | ||
} | ||
} else { | ||
address recoveredAddress = ecrecover(digest, v, r, s); | ||
if (recoveredAddress == address(0)) { | ||
revert InvalidSignature(); | ||
} | ||
if (recoveredAddress != owner) { | ||
revert Unauthorized(); | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
// SPDX-License-Identifier: UNLICENSED | ||
pragma solidity ^0.8.19; | ||
|
||
contract ERC721SigUtils { | ||
/// @notice The ERC721 token domain separator | ||
bytes32 internal immutable DOMAIN_SEPARATOR; | ||
|
||
constructor(bytes32 _DOMAIN_SEPARATOR) { | ||
DOMAIN_SEPARATOR = _DOMAIN_SEPARATOR; | ||
} | ||
|
||
/// @dev Value is equal to keccak256("Permit(address spender,uint256 tokenId,uint256 nonce,uint256 deadline)"); | ||
bytes32 public constant PERMIT_TYPEHASH = 0x49ecf333e5b8c95c40fdafc95c1ad136e8914a8fb55e9dc8bb01eaa83a2df9ad; | ||
|
||
struct Permit { | ||
address spender; | ||
uint256 tokenId; | ||
uint256 nonce; | ||
uint256 deadline; | ||
} | ||
|
||
/// @dev Computes the hash of a permit | ||
/// @param _permit The approval to execute on-chain | ||
/// @return The encoded permit | ||
function getStructHash(Permit memory _permit) internal pure returns (bytes32) { | ||
return keccak256(abi.encode(PERMIT_TYPEHASH, _permit.spender, _permit.tokenId, _permit.nonce, _permit.deadline)); | ||
} | ||
|
||
/// @notice Computes the hash of a fully encoded EIP-712 message for the domain | ||
/// @param _permit The approval to execute on-chain | ||
/// @return The digest to sign and use to recover the signer | ||
function getTypedDataHash(Permit memory _permit) public view returns (bytes32) { | ||
return keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, getStructHash(_permit))); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
// SPDX-License-Identifier: UNLICENSED | ||
pragma solidity ^0.8.19; | ||
|
||
import {ERC721Permit} from "../../src/pool-cl/base/ERC721Permit.sol"; | ||
|
||
contract MockERC721Permit is ERC721Permit { | ||
uint256 public tokenId; | ||
mapping(uint256 => uint256) public tokenNonce; | ||
|
||
constructor() ERC721Permit("Pancake V4 Positions NFT-V1", "PCS-V4-POS", "1") {} | ||
|
||
function mint() external { | ||
_mint(msg.sender, tokenId++); | ||
} | ||
|
||
function mintTo(address to) external { | ||
_mint(to, tokenId++); | ||
} | ||
|
||
function _getAndIncrementNonce(uint256 _tokenId) internal override returns (uint256) { | ||
return tokenNonce[_tokenId]++; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
// SPDX-License-Identifier: UNLICENSED | ||
pragma solidity ^0.8.19; | ||
|
||
import {Test} from "forge-std/Test.sol"; | ||
import {ERC721SigUtils} from "../helpers/ERC721SigUtils.sol"; | ||
import {MockERC721Permit} from "../helpers/MockERC721Permit.sol"; | ||
import {ERC721PermitLib} from "../../src/pool-cl/libraries/ERC721PermitLib.sol"; | ||
|
||
contract ERC721PermitTest is Test { | ||
MockERC721Permit ERC721PermitToken; | ||
ERC721SigUtils sigUtils; | ||
|
||
uint256 alicePrivateKey = 0xA11CE; | ||
address alice; | ||
uint256 bobPrivateKey = 0xB0B; | ||
address bob; | ||
uint256 carolPrivateKey = 0xC0C; | ||
address carol; | ||
|
||
function setUp() public { | ||
ERC721PermitToken = new MockERC721Permit(); | ||
sigUtils = new ERC721SigUtils(ERC721PermitToken.DOMAIN_SEPARATOR()); | ||
alice = vm.addr(alicePrivateKey); | ||
bob = vm.addr(bobPrivateKey); | ||
carol = vm.addr(carolPrivateKey); | ||
ERC721PermitToken.mintTo(alice); | ||
} | ||
|
||
function testERC721Permit() public { | ||
vm.startPrank(alice); | ||
assertEq(ERC721PermitToken.ownerOf(0), alice); | ||
|
||
(uint8 v, bytes32 r, bytes32 s) = getPermitSignature(bob, 0, block.timestamp + 60, 0); | ||
|
||
ERC721PermitToken.permit(bob, 0, block.timestamp + 60, v, r, s); | ||
assertEq(ERC721PermitToken.getApproved(0), bob); | ||
|
||
ERC721PermitToken.transferFrom(alice, carol, 0); | ||
assertEq(ERC721PermitToken.ownerOf(0), carol); | ||
assertEq(ERC721PermitToken.tokenNonce(0), 1); | ||
} | ||
|
||
function testERC721Permit_PermitExpired() public { | ||
vm.startPrank(alice); | ||
assertEq(ERC721PermitToken.ownerOf(0), alice); | ||
|
||
(uint8 v, bytes32 r, bytes32 s) = getPermitSignature(bob, 0, block.timestamp - 1, 0); | ||
|
||
vm.expectRevert(ERC721PermitLib.PermitExpired.selector); | ||
ERC721PermitToken.permit(bob, 0, block.timestamp - 1, v, r, s); | ||
} | ||
|
||
function testERC721Permit_Unauthorized() public { | ||
vm.startPrank(alice); | ||
assertEq(ERC721PermitToken.ownerOf(0), alice); | ||
|
||
(uint8 v, bytes32 r, bytes32 s) = getPermitSignature(bob, 0, block.timestamp + 60, 0); | ||
|
||
vm.expectRevert(ERC721PermitLib.Unauthorized.selector); | ||
ERC721PermitToken.permit(carol, 0, block.timestamp + 60, v, r, s); | ||
} | ||
|
||
function testERC721Permit_ApproveToOneself() public { | ||
vm.startPrank(alice); | ||
assertEq(ERC721PermitToken.ownerOf(0), alice); | ||
|
||
(uint8 v, bytes32 r, bytes32 s) = getPermitSignature(alice, 0, block.timestamp + 60, 0); | ||
|
||
vm.expectRevert(ERC721PermitLib.ApproveToOneself.selector); | ||
ERC721PermitToken.permit(alice, 0, block.timestamp + 60, v, r, s); | ||
} | ||
|
||
function testERC721Permit_InvalidSignature() public { | ||
vm.startPrank(alice); | ||
assertEq(ERC721PermitToken.ownerOf(0), alice); | ||
|
||
(uint8 v, bytes32 r, bytes32 s) = getPermitSignature(bob, 0, block.timestamp + 60, 1); | ||
// modify v to simulate invalid signature | ||
if (v > 1) { | ||
v = 0; | ||
} else { | ||
v = 1; | ||
} | ||
vm.expectRevert(ERC721PermitLib.InvalidSignature.selector); | ||
ERC721PermitToken.permit(bob, 0, block.timestamp + 60, v, r, s); | ||
} | ||
|
||
/// @dev get a permit signature from alice -> ERC721Permit | ||
function getPermitSignature(address spender, uint256 tokenId, uint256 deadline, uint256 nonce) | ||
internal | ||
view | ||
returns (uint8 v, bytes32 r, bytes32 s) | ||
{ | ||
// Generate permit signature | ||
ERC721SigUtils.Permit memory permit = | ||
ERC721SigUtils.Permit({spender: spender, tokenId: tokenId, nonce: nonce, deadline: deadline}); | ||
bytes32 digest = sigUtils.getTypedDataHash(permit); | ||
(v, r, s) = vm.sign(alicePrivateKey, digest); | ||
} | ||
} |