add contract for nft lock
This commit is contained in:
parent
89c02d5e47
commit
0cb0cadc67
@ -1,3 +1,8 @@
|
||||
{
|
||||
"printWidth": 120
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"singleQuote": false,
|
||||
"bracketSpacing": false,
|
||||
"explicitTypes": "always"
|
||||
}
|
||||
|
@ -38,7 +38,7 @@
|
||||
"avoid-throw": "error",
|
||||
"avoid-tx-origin": "error",
|
||||
"check-send-result": "error",
|
||||
"compiler-version": ["error", "0.8.23"],
|
||||
"compiler-version": ["error", "0.8.19"],
|
||||
"func-visibility": ["error", {"ignoreConstructors": true}],
|
||||
"multiple-sends": "warn",
|
||||
"no-complex-fallback": "warn",
|
||||
|
601
build/contracts/NFTLock.json
Normal file
601
build/contracts/NFTLock.json
Normal file
File diff suppressed because one or more lines are too long
@ -92,7 +92,15 @@ contract NFTClaimStage2 is HasSignature, ReentrancyGuard {
|
||||
}
|
||||
require(tokenAmount >= mintConfig.mintPrice * count, "NFTClaimer: insufficient token amount");
|
||||
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);
|
||||
IERC20(mintConfig.currency).transferFrom(to, mintConfig.feeToAddress, tokenAmount);
|
||||
for (uint256 i = 0; i < count; ++i) {
|
||||
|
@ -3,67 +3,81 @@ 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 {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
|
||||
contract NFTLock is AccessControl, ERC721Holder {
|
||||
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;
|
||||
|
||||
mapping(address => mapping(uint256 => address)) lockedOriginal;
|
||||
mapping(address => mapping(address => EnumerableSet.UintSet)) lockedRecords;
|
||||
mapping(address => bool) supportNftList;
|
||||
bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE");
|
||||
bytes32 public constant UNLOCK_ROLE = keccak256("UNLOCK_ROLE");
|
||||
bytes32 public constant RELEASE_ROLE = keccak256("RELEASE_ROLE");
|
||||
uint256 public immutable _CACHED_CHAIN_ID;
|
||||
address public immutable _CACHED_THIS;
|
||||
address public verifier;
|
||||
|
||||
event Lock(address indexed nft, address indexed user, uint256 indexed tokenId);
|
||||
event UnLock(address indexed nft, address indexed user, uint256 indexed tokenId);
|
||||
event BatchLock(address indexed nft, address indexed user, uint256[] tokenId);
|
||||
event Release(address indexed nft, uint256[] tokenIds, string serverId);
|
||||
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;
|
||||
|
||||
constructor() {
|
||||
_setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
|
||||
_setupRole(OPERATOR_ROLE, msg.sender);
|
||||
_setupRole(UNLOCK_ROLE, msg.sender);
|
||||
_setupRole(RELEASE_ROLE, msg.sender);
|
||||
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 tokenId) external {
|
||||
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 {
|
||||
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++) {
|
||||
IERC721(nft).transferFrom(msg.sender, address(this), tokenIds[i]);
|
||||
lockedOriginal[nft][tokenIds[i]] = msg.sender;
|
||||
lockedRecords[nft][msg.sender].add(tokenIds[i]);
|
||||
INFT(nft).transferFrom(to, address(this), tokenIds[i]);
|
||||
lockedOriginal[nft][tokenIds[i]] = to;
|
||||
lockedRecords[nft][to].add(tokenIds[i]);
|
||||
}
|
||||
emit BatchLock(nft, msg.sender, tokenIds);
|
||||
}
|
||||
|
||||
function release(address nft, uint256[] calldata tokenIds, string calldata serverId) external onlyReleaser {
|
||||
require(tokenIds.length <= 100, "tokenIds too many");
|
||||
for (uint256 i = 0; i < tokenIds.length; i++) {
|
||||
IERC721(nft).transferFrom(msg.sender, address(this), tokenIds[i]);
|
||||
lockedRecords[nft][msg.sender].add(tokenIds[i]);
|
||||
}
|
||||
emit Release(nft, tokenIds, serverId);
|
||||
}
|
||||
|
||||
function originalOwner(address token, uint256 tokenId) public view returns (address) {
|
||||
return lockedOriginal[token][tokenId];
|
||||
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();
|
||||
}
|
||||
@ -72,54 +86,39 @@ contract NFTLock is AccessControl, ERC721Holder {
|
||||
return lockedRecords[token][user].values();
|
||||
}
|
||||
|
||||
/** ------set------- **/
|
||||
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 {
|
||||
function addSupportNftList(address[] calldata nftList) external onlyOwner {
|
||||
for (uint256 i = 0; i < nftList.length; i++) {
|
||||
supportNftList[nftList[i]] = true;
|
||||
}
|
||||
}
|
||||
function removeSupportNft(address nftAddress) external onlyOperator {
|
||||
function removeSupportNft(address nftAddress) external onlyOwner {
|
||||
require(supportNftList[nftAddress], "can't remove");
|
||||
delete supportNftList[nftAddress];
|
||||
}
|
||||
|
||||
/** ------modifier------- **/
|
||||
modifier onlyOperator() {
|
||||
require(hasRole(OPERATOR_ROLE, msg.sender), "not operator role");
|
||||
_;
|
||||
/**
|
||||
* @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);
|
||||
}
|
||||
|
||||
modifier onlyUnlocker() {
|
||||
require(hasRole(UNLOCK_ROLE, msg.sender), "not unlocker role");
|
||||
_;
|
||||
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));
|
||||
}
|
||||
|
||||
modifier onlyReleaser() {
|
||||
require(hasRole(RELEASE_ROLE, msg.sender), "not releaser role");
|
||||
_;
|
||||
return keccak256(encoded);
|
||||
}
|
||||
}
|
||||
|
@ -8,8 +8,8 @@ contract TimeChecker is Ownable {
|
||||
|
||||
event DurationUpdated(uint256 indexed duration);
|
||||
|
||||
constructor() {
|
||||
duration = 1 days;
|
||||
constructor(uint256 _duration) {
|
||||
duration = _duration;
|
||||
minDuration = 30 minutes;
|
||||
}
|
||||
|
||||
|
29
deploy/4_deploy_nftlocker.ts
Normal file
29
deploy/4_deploy_nftlocker.ts
Normal 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
878
deployments/imtbl_test/NFTLock.json
Normal file
878
deployments/imtbl_test/NFTLock.json
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -16,5 +16,11 @@
|
||||
"type": "logic",
|
||||
"json": "assets/contracts/NFTClaimStage2.json",
|
||||
"address": "0xf45702180314187a3549FEDac3B78349b47ca6A0"
|
||||
},
|
||||
{
|
||||
"name": "NFTLock",
|
||||
"type": "logic",
|
||||
"json": "assets/contracts/NFTLock.json",
|
||||
"address": "0x59e751c2037B710090035B6ea928e0cce80aC03f"
|
||||
}
|
||||
]
|
@ -10,6 +10,7 @@
|
||||
"deploy": "hardhat deploy --network imtbl_test",
|
||||
"deploy:nftclaim": "hardhat deploy --tags NFTClaimStage2 --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",
|
||||
"solhint": "solhint --config ./.solhint.json"
|
||||
},
|
||||
|
111
test/testNFTLocker.ts
Normal file
111
test/testNFTLocker.ts
Normal 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"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
})
|
Loading…
x
Reference in New Issue
Block a user