// SPDX-License-Identifier: MIT pragma solidity 0.8.19; import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import {Pausable} from "@openzeppelin/contracts/security/Pausable.sol"; import {HasSignature} from "../core/HasSignature.sol"; import {TimeChecker} from "../utils/TimeChecker.sol"; interface INFT { function mint(address to, uint256 tokenID) external; function transferFrom(address from, address to, uint256 tokenId) external; } contract NFTLock is ERC721Holder, HasSignature, TimeChecker, Pausable { using EnumerableSet for EnumerableSet.UintSet; uint256 public immutable _CACHED_CHAIN_ID; address public immutable _CACHED_THIS; address public verifier; struct NFTInfo { uint256 tokenId; bool isMint; } mapping(address nft => mapping(uint256 tokenId => address user)) public lockedOriginal; mapping(address nft => mapping(address user => EnumerableSet.UintSet tokenIdSet)) private lockedRecords; mapping(address nft => bool status) public supportNftList; event UnLock(address indexed nft, address indexed user, uint256 nonce, NFTInfo[] nftList); event Lock(address indexed nft, address indexed user, uint256[] tokenIds); event VerifierUpdated(address indexed verifier); constructor(uint256 _duration, address _verifier) TimeChecker(_duration) { _CACHED_CHAIN_ID = block.chainid; _CACHED_THIS = address(this); verifier = _verifier; } function lock(address nft, uint256[] calldata tokenIds) external whenNotPaused{ require(tokenIds.length <= 100, "tokenIds too many"); address to = _msgSender(); for (uint256 i = 0; i < tokenIds.length; i++) { INFT(nft).transferFrom(to, address(this), tokenIds[i]); lockedOriginal[nft][tokenIds[i]] = to; lockedRecords[nft][to].add(tokenIds[i]); } emit Lock(nft, to, tokenIds); } /** * @dev unlock or mint nft * if tokenId not exists, mint it * if exists and user is owner, unlock it */ function unlockOrMint( address nft, NFTInfo[] calldata nftList, uint256 signTime, uint256 saltNonce, bytes calldata signature ) external signatureValid(signature) timeValid(signTime) whenNotPaused { require(nftList.length <= 100, "tokenIds too many"); address to = _msgSender(); bytes32 messageHash = getMessageHash(to, nft, nftList, _CACHED_THIS, _CACHED_CHAIN_ID, signTime, saltNonce); checkSigner(verifier, messageHash, signature); for (uint256 i = 0; i < nftList.length; i++) { if (nftList[i].isMint) { INFT(nft).mint(to, nftList[i].tokenId); } else { require(lockedOriginal[nft][nftList[i].tokenId] == to, "not owner"); INFT(nft).transferFrom(address(this), to, nftList[i].tokenId); lockedRecords[nft][to].remove(nftList[i].tokenId); delete lockedOriginal[nft][nftList[i].tokenId]; } } _useSignature(signature); emit UnLock(nft, to, saltNonce, nftList); } /** ------get------- **/ function lockedNum(address token, address user) public view returns (uint256) { return lockedRecords[token][user].length(); } function lockedNft(address token, address user) public view returns (uint256[] memory) { return lockedRecords[token][user].values(); } function addSupportNftList(address[] calldata nftList) external onlyOwner { for (uint256 i = 0; i < nftList.length; i++) { supportNftList[nftList[i]] = true; } } function removeSupportNft(address nftAddress) external onlyOwner { require(supportNftList[nftAddress], "can't remove"); delete supportNftList[nftAddress]; } /** * @dev update verifier address */ function updateVerifier(address _verifier) external onlyOwner { require(_verifier != address(0), "NFTClaimer: address can not be zero"); verifier = _verifier; emit VerifierUpdated(_verifier); } function getMessageHash( address _to, address _nft, NFTInfo[] memory _ids, address _contract, uint256 _chainId, uint256 _signTime, uint256 _saltNonce ) public pure returns (bytes32) { bytes memory encoded = abi.encodePacked(_to, _nft, _contract, _chainId, _signTime, _saltNonce); for (uint256 i = 0; i < _ids.length; ++i) { encoded = bytes.concat(encoded, abi.encodePacked(_ids[i].tokenId)); encoded = bytes.concat(encoded, abi.encodePacked(_ids[i].isMint)); } return keccak256(encoded); } }