// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity =0.8.20; import '@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol'; import '@openzeppelin/contracts/utils/Address.sol'; import '../interfaces/external/IERC1271.sol'; import '../interfaces/IERC721Permit.sol'; import './BlockTimestamp.sol'; /// @title ERC721 with permit /// @notice Nonfungible tokens that support an approve via signature, i.e. permit abstract contract ERC721Permit is BlockTimestamp, ERC721Enumerable, IERC721Permit { bytes32 private constant _TYPE_HASH = keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'); /// @dev Gets the current nonce for a token ID and then increments it, returning the original value function _getAndIncrementNonce(uint256 tokenId) internal virtual returns (uint256); /// @dev The hash of the name used in the permit signature verification bytes32 private immutable nameHash; /// @dev The hash of the version string used in the permit signature verification bytes32 private immutable versionHash; bytes32 private immutable _cachedDomainSeparator; uint256 private immutable _cachedChainId; address private immutable _cachedThis; /// @notice Computes the nameHash and versionHash constructor(string memory name_, string memory symbol_, string memory version_) ERC721(name_, symbol_) { nameHash = keccak256(bytes(name_)); versionHash = keccak256(bytes(version_)); _cachedChainId = block.chainid; _cachedDomainSeparator = _buildDomainSeparator(); _cachedThis = address(this); } /// @inheritdoc IERC721Permit function DOMAIN_SEPARATOR() public view override returns (bytes32) { if (address(this) == _cachedThis && block.chainid == _cachedChainId) { return _cachedDomainSeparator; } else { return _buildDomainSeparator(); } } function _buildDomainSeparator() private view returns (bytes32) { return keccak256(abi.encode(_TYPE_HASH, nameHash, versionHash, block.chainid, address(this))); } /// @inheritdoc IERC721Permit bytes32 public constant override PERMIT_TYPEHASH = keccak256('Permit(address spender,uint256 tokenId,uint256 nonce,uint256 deadline)'); /// @inheritdoc IERC721Permit function permit( address spender, uint256 tokenId, uint256 deadline, uint8 v, bytes32 r, bytes32 s ) external payable override { require(_blockTimestamp() <= deadline, 'Permit expired'); bytes32 digest = keccak256( abi.encodePacked( '\x19\x01', DOMAIN_SEPARATOR(), keccak256(abi.encode(PERMIT_TYPEHASH, spender, tokenId, _getAndIncrementNonce(tokenId), deadline)) ) ); address owner = ownerOf(tokenId); require(spender != owner, 'Approval to current owner'); if (Address.isContract(owner)) { _checkAuthorization(IERC1271(owner).isValidSignature(digest, abi.encodePacked(r, s, v)) == 0x1626ba7e); } else { address recoveredAddress = ecrecover(digest, v, r, s); require(recoveredAddress != address(0), 'Invalid signature'); _checkAuthorization(recoveredAddress == owner); } _approve(spender, tokenId); } function _checkAuthorization(bool isAuthorized) private pure { require(isAuthorized, 'Unauthorized'); } }