Skip to content

Commit

Permalink
feat: Add ERC721PermitLib (#21)
Browse files Browse the repository at this point in the history
* feat: Add ERC721PermitLib

* feat: Add view for permitCheck

* feat: Change function parameter by mixedcase rule

* feat: Add test cases for ERC721Permit
  • Loading branch information
ChefSnoopy authored Apr 25, 2024
1 parent 50c89f4 commit 5fe0d6b
Show file tree
Hide file tree
Showing 6 changed files with 236 additions and 49 deletions.
51 changes: 7 additions & 44 deletions src/pool-cl/base/ERC721Permit.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,8 @@
pragma solidity ^0.8.19;

import {ERC721Enumerable, ERC721} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol";

import {IERC721Permit} from "../interfaces/IERC721Permit.sol";
import {ERC721PermitLib} from "../libraries/ERC721PermitLib.sol";

/// @title ERC721 with permit
/// @notice Nonfungible tokens that support an approve via signature, i.e. permit
Expand All @@ -28,59 +26,24 @@ abstract contract ERC721Permit is ERC721Enumerable, IERC721Permit {

/// @inheritdoc IERC721Permit
function DOMAIN_SEPARATOR() public view override returns (bytes32) {
return keccak256(
abi.encode(
/// @dev keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)')
0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f,
nameHash,
versionHash,
block.chainid,
address(this)
)
);
return ERC721PermitLib.DOMAIN_SEPARATOR(nameHash, versionHash);
}

/// @inheritdoc IERC721Permit
/// @dev Value is equal to keccak256("Permit(address spender,uint256 tokenId,uint256 nonce,uint256 deadline)");
bytes32 public constant override PERMIT_TYPEHASH =
0x49ecf333e5b8c95c40fdafc95c1ad136e8914a8fb55e9dc8bb01eaa83a2df9ad;
function PERMIT_TYPEHASH() external pure override returns (bytes32) {
return ERC721PermitLib.PERMIT_TYPEHASH;
}

/// @inheritdoc IERC721Permit
function permit(address spender, uint256 tokenId, uint256 deadline, uint8 v, bytes32 r, bytes32 s)
external
payable
override
{
if (block.timestamp > deadline) {
revert PermitExpired();
}

bytes32 digest = keccak256(
abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR(),
keccak256(abi.encode(PERMIT_TYPEHASH, spender, tokenId, _getAndIncrementNonce(tokenId), deadline))
)
ERC721PermitLib.permitCheck(
spender, tokenId, deadline, v, r, s, ownerOf(tokenId), DOMAIN_SEPARATOR(), _getAndIncrementNonce(tokenId)
);
address owner = ownerOf(tokenId);
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();
}
}

_approve(spender, tokenId);
}
Expand Down
5 changes: 0 additions & 5 deletions src/pool-cl/interfaces/IERC721Permit.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,6 @@ import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
/// @title ERC721 with permit
/// @notice Extension to ERC721 that includes a permit function for signature based approvals
interface IERC721Permit is IERC721 {
error PermitExpired();
error ApproveToOneself();
error Unauthorized();
error InvalidSignature();

/// @notice The permit typehash used in the permit signature
/// @return The typehash for the permit
function PERMIT_TYPEHASH() external pure returns (bytes32);
Expand Down
71 changes: 71 additions & 0 deletions src/pool-cl/libraries/ERC721PermitLib.sol
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();
}
}
}
}
35 changes: 35 additions & 0 deletions test/helpers/ERC721SigUtils.sol
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)));
}
}
23 changes: 23 additions & 0 deletions test/helpers/MockERC721Permit.sol
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]++;
}
}
100 changes: 100 additions & 0 deletions test/pool-cl/ERC721Permit.t.sol
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);
}
}

0 comments on commit 5fe0d6b

Please sign in to comment.