add contract for nft lock

This commit is contained in:
CounterFire2023 2024-06-03 14:06:45 +08:00
parent 89c02d5e47
commit 0cb0cadc67
13 changed files with 1886 additions and 940 deletions

View File

@ -1,3 +1,8 @@
{ {
"printWidth": 120 "printWidth": 120,
"tabWidth": 2,
"useTabs": false,
"singleQuote": false,
"bracketSpacing": false,
"explicitTypes": "always"
} }

View File

@ -38,7 +38,7 @@
"avoid-throw": "error", "avoid-throw": "error",
"avoid-tx-origin": "error", "avoid-tx-origin": "error",
"check-send-result": "error", "check-send-result": "error",
"compiler-version": ["error", "0.8.23"], "compiler-version": ["error", "0.8.19"],
"func-visibility": ["error", {"ignoreConstructors": true}], "func-visibility": ["error", {"ignoreConstructors": true}],
"multiple-sends": "warn", "multiple-sends": "warn",
"no-complex-fallback": "warn", "no-complex-fallback": "warn",

File diff suppressed because one or more lines are too long

View File

@ -92,7 +92,15 @@ contract NFTClaimStage2 is HasSignature, ReentrancyGuard {
} }
require(tokenAmount >= mintConfig.mintPrice * count, "NFTClaimer: insufficient token amount"); require(tokenAmount >= mintConfig.mintPrice * count, "NFTClaimer: insufficient token amount");
address to = _msgSender(); address to = _msgSender();
bytes32 criteriaMessageHash = getMessageHash(to, nftAddress, ids, tokenAmount, _CACHED_THIS, _CACHED_CHAIN_ID, saltNonce); bytes32 criteriaMessageHash = getMessageHash(
to,
nftAddress,
ids,
tokenAmount,
_CACHED_THIS,
_CACHED_CHAIN_ID,
saltNonce
);
checkSigner(verifier, criteriaMessageHash, signature); checkSigner(verifier, criteriaMessageHash, signature);
IERC20(mintConfig.currency).transferFrom(to, mintConfig.feeToAddress, tokenAmount); IERC20(mintConfig.currency).transferFrom(to, mintConfig.feeToAddress, tokenAmount);
for (uint256 i = 0; i < count; ++i) { for (uint256 i = 0; i < count; ++i) {

View File

@ -3,67 +3,81 @@ pragma solidity 0.8.19;
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol";
import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; import {Pausable} from "@openzeppelin/contracts/security/Pausable.sol";
contract NFTLock is AccessControl, ERC721Holder { 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; using EnumerableSet for EnumerableSet.UintSet;
mapping(address => mapping(uint256 => address)) lockedOriginal; uint256 public immutable _CACHED_CHAIN_ID;
mapping(address => mapping(address => EnumerableSet.UintSet)) lockedRecords; address public immutable _CACHED_THIS;
mapping(address => bool) supportNftList; address public verifier;
bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE");
bytes32 public constant UNLOCK_ROLE = keccak256("UNLOCK_ROLE");
bytes32 public constant RELEASE_ROLE = keccak256("RELEASE_ROLE");
event Lock(address indexed nft, address indexed user, uint256 indexed tokenId); struct NFTInfo {
event UnLock(address indexed nft, address indexed user, uint256 indexed tokenId); uint256 tokenId;
event BatchLock(address indexed nft, address indexed user, uint256[] tokenId); bool isMint;
event Release(address indexed nft, uint256[] tokenIds, string serverId); }
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;
constructor() { event UnLock(address indexed nft, address indexed user, uint256 nonce, NFTInfo[] nftList);
_setupRole(DEFAULT_ADMIN_ROLE, msg.sender); event Lock(address indexed nft, address indexed user, uint256[] tokenIds);
_setupRole(OPERATOR_ROLE, msg.sender); event VerifierUpdated(address indexed verifier);
_setupRole(UNLOCK_ROLE, msg.sender);
_setupRole(RELEASE_ROLE, msg.sender); constructor(uint256 _duration, address _verifier) TimeChecker(_duration) {
_CACHED_CHAIN_ID = block.chainid;
_CACHED_THIS = address(this);
verifier = _verifier;
} }
function lock(address nft, uint256 tokenId) external { function lock(address nft, uint256[] calldata tokenIds) external whenNotPaused{
require(supportNftList[nft], "can't lock this nft");
IERC721(nft).transferFrom(msg.sender, address(this), tokenId);
lockedOriginal[nft][tokenId] = msg.sender;
lockedRecords[nft][msg.sender].add(tokenId);
emit Lock(nft, msg.sender, tokenId);
}
function unlock(address nft, address to, uint256 tokenId) external onlyUnlocker {
IERC721(nft).transferFrom(address(this), to, tokenId);
lockedRecords[nft][lockedOriginal[nft][tokenId]].remove(tokenId);
delete lockedOriginal[nft][tokenId];
emit UnLock(nft, to, tokenId);
}
function batchLock(address nft, uint256[] calldata tokenIds) external {
require(tokenIds.length <= 100, "tokenIds too many"); require(tokenIds.length <= 100, "tokenIds too many");
address to = _msgSender();
for (uint256 i = 0; i < tokenIds.length; i++) { for (uint256 i = 0; i < tokenIds.length; i++) {
IERC721(nft).transferFrom(msg.sender, address(this), tokenIds[i]); INFT(nft).transferFrom(to, address(this), tokenIds[i]);
lockedOriginal[nft][tokenIds[i]] = msg.sender; lockedOriginal[nft][tokenIds[i]] = to;
lockedRecords[nft][msg.sender].add(tokenIds[i]); lockedRecords[nft][to].add(tokenIds[i]);
} }
emit BatchLock(nft, msg.sender, tokenIds); emit Lock(nft, to, tokenIds);
} }
/**
function release(address nft, uint256[] calldata tokenIds, string calldata serverId) external onlyReleaser { * @dev unlock or mint nft
require(tokenIds.length <= 100, "tokenIds too many"); * if tokenId not exists, mint it
for (uint256 i = 0; i < tokenIds.length; i++) { * if exists and user is owner, unlock it
IERC721(nft).transferFrom(msg.sender, address(this), tokenIds[i]); */
lockedRecords[nft][msg.sender].add(tokenIds[i]); function unlockOrMint(
} address nft,
emit Release(nft, tokenIds, serverId); NFTInfo[] calldata nftList,
} uint256 signTime,
uint256 saltNonce,
function originalOwner(address token, uint256 tokenId) public view returns (address) { bytes calldata signature
return lockedOriginal[token][tokenId]; ) 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) { function lockedNum(address token, address user) public view returns (uint256) {
return lockedRecords[token][user].length(); return lockedRecords[token][user].length();
} }
@ -72,54 +86,39 @@ contract NFTLock is AccessControl, ERC721Holder {
return lockedRecords[token][user].values(); return lockedRecords[token][user].values();
} }
/** ------set------- **/ function addSupportNftList(address[] calldata nftList) external onlyOwner {
function setOperatorRole(address to) external {
grantRole(OPERATOR_ROLE, to);
}
function removeOperatorRole(address to) external {
revokeRole(OPERATOR_ROLE, to);
}
function setReleaseRole(address to) external {
grantRole(RELEASE_ROLE, to);
}
function removeReleaseRole(address to) external {
revokeRole(RELEASE_ROLE, to);
}
function setUnlockRole(address to) external {
grantRole(UNLOCK_ROLE, to);
}
function removeUnlockRole(address to) external {
revokeRole(UNLOCK_ROLE, to);
}
function addSupportNftList(address[] calldata nftList) external onlyOperator {
for (uint256 i = 0; i < nftList.length; i++) { for (uint256 i = 0; i < nftList.length; i++) {
supportNftList[nftList[i]] = true; supportNftList[nftList[i]] = true;
} }
} }
function removeSupportNft(address nftAddress) external onlyOperator { function removeSupportNft(address nftAddress) external onlyOwner {
require(supportNftList[nftAddress], "can't remove"); require(supportNftList[nftAddress], "can't remove");
delete supportNftList[nftAddress]; delete supportNftList[nftAddress];
} }
/** ------modifier------- **/ /**
modifier onlyOperator() { * @dev update verifier address
require(hasRole(OPERATOR_ROLE, msg.sender), "not operator role"); */
_; function updateVerifier(address _verifier) external onlyOwner {
require(_verifier != address(0), "NFTClaimer: address can not be zero");
verifier = _verifier;
emit VerifierUpdated(_verifier);
} }
modifier onlyUnlocker() { function getMessageHash(
require(hasRole(UNLOCK_ROLE, msg.sender), "not unlocker role"); 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);
modifier onlyReleaser() {
require(hasRole(RELEASE_ROLE, msg.sender), "not releaser role");
_;
} }
} }

View File

@ -8,8 +8,8 @@ contract TimeChecker is Ownable {
event DurationUpdated(uint256 indexed duration); event DurationUpdated(uint256 indexed duration);
constructor() { constructor(uint256 _duration) {
duration = 1 days; duration = _duration;
minDuration = 30 minutes; minDuration = 30 minutes;
} }

View File

@ -0,0 +1,29 @@
import { HardhatRuntimeEnvironment } from "hardhat/types";
import { DeployFunction } from "hardhat-deploy/types";
import { updateArray } from "../scripts/utils"
const deployNFTClaim: DeployFunction =
async function (hre: HardhatRuntimeEnvironment) {
const provider = hre.ethers.provider;
const from = await (await provider.getSigner()).getAddress();
const verifier = '0x50A8e60041A206AcaA5F844a1104896224be6F39'
const ret = await hre.deployments.deploy("NFTLock", {
from,
args: [3600, verifier],
log: true,
});
console.log("==NFTLock addr=", ret.address);
updateArray({
name: "NFTLock",
type: "logic",
json: "assets/contracts/NFTLock.json",
address: ret.address,
network: hre.network.name,
});
};
deployNFTClaim.tags = ["NFTLock"];
export default deployNFTClaim;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -16,5 +16,11 @@
"type": "logic", "type": "logic",
"json": "assets/contracts/NFTClaimStage2.json", "json": "assets/contracts/NFTClaimStage2.json",
"address": "0xf45702180314187a3549FEDac3B78349b47ca6A0" "address": "0xf45702180314187a3549FEDac3B78349b47ca6A0"
},
{
"name": "NFTLock",
"type": "logic",
"json": "assets/contracts/NFTLock.json",
"address": "0x59e751c2037B710090035B6ea928e0cce80aC03f"
} }
] ]

View File

@ -10,6 +10,7 @@
"deploy": "hardhat deploy --network imtbl_test", "deploy": "hardhat deploy --network imtbl_test",
"deploy:nftclaim": "hardhat deploy --tags NFTClaimStage2 --network imtbl_test --reset", "deploy:nftclaim": "hardhat deploy --tags NFTClaimStage2 --network imtbl_test --reset",
"deploy:nft": "hardhat deploy --tags CFNFTGame --network imtbl_test --reset", "deploy:nft": "hardhat deploy --tags CFNFTGame --network imtbl_test --reset",
"deploy:nftlock": "hardhat deploy --tags NFTLock --network imtbl_test --reset",
"deploy:testtoken": "hardhat deploy --tags TestToken --network imtbl_test --reset", "deploy:testtoken": "hardhat deploy --tags TestToken --network imtbl_test --reset",
"solhint": "solhint --config ./.solhint.json" "solhint": "solhint --config ./.solhint.json"
}, },

111
test/testNFTLocker.ts Normal file
View File

@ -0,0 +1,111 @@
import { expect } from 'chai'
import hre from "hardhat";
import {
getBytes,
solidityPackedKeccak256,
} from 'ethers'
import {
loadFixture,
} from "@nomicfoundation/hardhat-toolbox/network-helpers";
describe('NFTLock', function() {
async function deployOneContract() {
const [owner, otherAccount] = await hre.ethers.getSigners();
const verifier = owner.address;
const OperatorAllowlist = await hre.ethers.getContractFactory("OperatorAllowlist");
const operatorAllowlist = await OperatorAllowlist.deploy(owner.address);
const CFNFTGame = await hre.ethers.getContractFactory("CFNFTGame");
const nft = await CFNFTGame.deploy(owner.address, 'name', 'symbol', 'baseURI', 'contractURI', operatorAllowlist.target, owner.address, 5);
const nftAddress = nft.target;
const NFTLock = await hre.ethers.getContractFactory("NFTLock");
const nftLock = await NFTLock.deploy( 3600, verifier );
await nft.grantMinterRole(nftLock.target)
await nft.grantMinterRole(owner.address)
await nftLock.addSupportNftList([nftAddress]);
await nft.mint(otherAccount.address, "1001");
const chainId = hre.network.config.chainId
await operatorAllowlist.grantRegistrarRole(owner.address)
await operatorAllowlist.addAddressToAllowlist([nftLock.target])
return { nftLock, owner, otherAccount, verifier, nftAddress, nft, chainId };
}
describe("Deployment", function () {
it('should deploy NFTLock with the correct verifier', async function() {
const { nftLock, verifier } = await loadFixture(deployOneContract);
expect(await nftLock.verifier()).to.equal(verifier);
});
it('should deploy NFTLock with the correct NFT address', async function() {
const { nftLock, nftAddress } = await loadFixture(deployOneContract);
expect(await nftLock.supportNftList(nftAddress)).to.equal(true);
});
})
describe("Lock", function () {
it('should lock NFT', async function() {
const { nftLock, nft, otherAccount } = await loadFixture(deployOneContract);
const tokenId = "1001"
// @ts-ignore
await nft.connect(otherAccount).approve(nftLock.target, tokenId);
//@ts-ignore
await nftLock.connect(otherAccount).lock(nft.target, [tokenId]);
expect(await nft.balanceOf(nftLock.target)).to.equal(1);
expect(await nftLock.lockedOriginal(nft.target, tokenId)).to.equal(otherAccount.address);
expect(await nft.ownerOf(tokenId)).to.equal(nftLock.target);
});
})
describe("UnLock", function () {
it('should unlock NFT from lock', async function() {
const { nftLock, nft, otherAccount, chainId, owner } = await loadFixture(deployOneContract);
const tokenId = '1001'
// @ts-ignore
await nft.connect(otherAccount).approve(nftLock.target, tokenId);
//@ts-ignore
await nftLock.connect(otherAccount).lock(nft.target, [tokenId]);
const nonce = (Math.random() * 1000) | 0;
const now = Date.now() / 1000 | 0;
let localMsgHash = solidityPackedKeccak256(["address", "address", "address", "uint256", "uint256", "uint256", "uint256", "bool"],
[otherAccount.address, nft.target, nftLock.target, chainId, now, nonce, tokenId, false]);
const signature = await owner.signMessage(getBytes(localMsgHash));
//@ts-ignore
await nftLock.connect(otherAccount).unlockOrMint(nft.target, [[tokenId, false]], now, nonce, signature);
expect(await nft.ownerOf(tokenId)).to.equal(otherAccount.address);
});
it('should mint NFT from lock', async function() {
const { nftLock, nft, otherAccount, chainId, owner } = await loadFixture(deployOneContract);
const tokenId = '1002'
const isMint = true
const nonce = (Math.random() * 1000) | 0;
const now = Date.now() / 1000 | 0;
let localMsgHash = solidityPackedKeccak256(["address", "address", "address", "uint256", "uint256", "uint256", "uint256", "bool"],
[otherAccount.address, nft.target, nftLock.target, chainId, now, nonce, tokenId, isMint]);
const signature = await owner.signMessage(getBytes(localMsgHash));
//@ts-ignore
await nftLock.connect(otherAccount).unlockOrMint(nft.target, [[tokenId, isMint]], now, nonce, signature);
expect(await nft.ownerOf(tokenId)).to.equal(otherAccount.address);
});
it('should revert NFT mint for nft already minted', async function() {
const { nftLock, nft, otherAccount, chainId, owner } = await loadFixture(deployOneContract);
const tokenId = '1001'
const isMint = true
const nonce = (Math.random() * 1000) | 0;
const now = Date.now() / 1000 | 0;
let localMsgHash = solidityPackedKeccak256(["address", "address", "address", "uint256", "uint256", "uint256", "uint256", "bool"],
[otherAccount.address, nft.target, nftLock.target, chainId, now, nonce, tokenId, isMint]);
const signature = await owner.signMessage(getBytes(localMsgHash));
//@ts-ignore
await expect(nftLock.connect(otherAccount).unlockOrMint(nft.target, [[tokenId, isMint]], now, nonce, signature)).to.be.revertedWith(
"ERC721: token already minted"
);
});
});
})