add contracts for nft and nft claimer; add test for nft claimer

This commit is contained in:
CounterFire2023 2024-05-30 18:02:13 +08:00
parent 59b6fc8f34
commit 89c02d5e47
26 changed files with 4264 additions and 847 deletions

1
.gitignore vendored
View File

@ -30,3 +30,4 @@ ignition/deployments/chain-31337
/contracts/dist/
/contracts/types/
openzeppelin
imtbl

View File

@ -1,3 +1,3 @@
{
"solidity.compileUsingRemoteVersion": "v0.8.23+commit.f704f362"
"solidity.compileUsingRemoteVersion": "v0.8.19+commit.7dd6d404"
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,41 @@
const market = {
feeToAddress: "0x50A8e60041A206AcaA5F844a1104896224be6F39",
mallFeeAddress: "0x50A8e60041A206AcaA5F844a1104896224be6F39",
paymentTokens: [
"0x514609B71340E149Cb81A80A953D07A7Fe41bd4F", // USDT
],
};
const admins = {
admin: "0x50A8e60041A206AcaA5F844a1104896224be6F39",
proposers: [
"0x50A8e60041A206AcaA5F844a1104896224be6F39",
"0x746338765a8FbDD1c5aB61bfb92CD6D960C3C662",
],
confirmers: ["0x50A8e60041A206AcaA5F844a1104896224be6F39"],
executors: [
"0x50A8e60041A206AcaA5F844a1104896224be6F39",
"0x746338765a8FbDD1c5aB61bfb92CD6D960C3C662",
"0x22d491Bde2303f2f43325b2108D26f1eAbA1e32b",
],
};
const token = {
baseTokenURI: "https://market.cebg.games/api/nft/info/",
contractURI: 'https://market.cebg.games/api/nft/info/',
royaltyReceiver: '0x5Ab03Aa79Ab91B7420b5CFF134a4188388888888',
royaltyFee: 5,
};
const imtbl = {
operatorAllowlist: '0x6b969FD89dE634d8DE3271EbE97734FEFfcd58eE'
}
var config = {
market,
admins,
token,
imtbl,
};
module.exports = config;

View File

@ -1,99 +1,125 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;
pragma solidity 0.8.19;
import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {HasSignature} from "../core/HasSignature.sol";
/**
* Contract for the activity of NFT claim stage 2.
*/
interface IClaimAbleNFT {
function safeMint(
address to
) external returns (uint256);
function safeMint(address to, uint256 tokenID) external;
}
contract NFTClaimStage2 is HasSignature, ReentrancyGuard{
uint256 private immutable _CACHED_CHAIN_ID;
address private immutable _CACHED_THIS;
address public immutable nftAddress;
address public verifier;
contract NFTClaimStage2 is HasSignature, ReentrancyGuard {
struct MintConfig {
uint256 parse1MaxSupply; // max supply for phase1
uint256 maxSupply; // max supply for phase2
address currency; // token address which user must pay to mint
uint256 mintPrice; // in wei
address feeToAddress; // wallet address to receive mint fee
}
// parse: 0: not open or end, 1: phase1, 2: phase2
uint256 public mintParse = 0;
bool public isPaused = false;
uint256 public immutable _CACHED_CHAIN_ID;
address public immutable _CACHED_THIS;
address public immutable nftAddress;
mapping(address user => uint256 status) public claimHistory;
address public verifier;
MintConfig public mintConfig;
uint256 public parse1Count;
uint256 public totalCount;
event NFTClaimed(
address indexed nftAddress,
address indexed to,
uint256 tokenId,
uint256 nonce
);
event NFTClaimed(address indexed nftAddress, address indexed to, uint256[] ids);
event StateUpdated(bool isPaused);
event VerifierUpdated(address indexed verifier);
event ParseUpdated(uint256 _parse);
event MintConfigUpdated(MintConfig config);
event VerifierUpdated(address indexed verifier);
constructor(address _nftAddress) {
_CACHED_CHAIN_ID = block.chainid;
_CACHED_THIS = address(this);
nftAddress = _nftAddress;
}
constructor(address _nftAddress, address _verifier, MintConfig memory _mintConfig) {
_CACHED_CHAIN_ID = block.chainid;
_CACHED_THIS = address(this);
nftAddress = _nftAddress;
verifier = _verifier;
mintConfig = _mintConfig;
}
modifier whenNotPaused() {
require(!isPaused, "NFTClaimer: paused");
_;
}
modifier whenNotPaused() {
require(mintParse > 0, "NFTClaimer: not begin or ended");
_;
}
function updatePaused(bool _isPaused) external onlyOwner {
isPaused = _isPaused;
emit StateUpdated(_isPaused);
}
function updateMintParse(uint256 _mintParse) external onlyOwner {
mintParse = _mintParse;
emit ParseUpdated(_mintParse);
}
/**
* @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 updateMintConfig(MintConfig calldata config) external onlyOwner {
mintConfig = config;
emit MintConfigUpdated(config);
}
/**
* @dev claim NFT
* Get whitelist signature from a third-party service, then call this method to claim NFT
* @param saltNonce nonce
* @param signature signature
*/
function claim(
uint256 saltNonce,
bytes calldata signature
) external nonReentrant whenNotPaused {
address to = _msgSender();
require(claimHistory[to] == 0, "NFTClaimer: already claimed");
bytes32 criteriaMessageHash = getMessageHash(
to,
nftAddress,
saltNonce
);
checkSigner(verifier, criteriaMessageHash, signature);
uint256 tokenId = IClaimAbleNFT(nftAddress).safeMint(to);
claimHistory[to] = tokenId;
_useSignature(signature);
emit NFTClaimed(nftAddress, to, tokenId, saltNonce);
}
/**
* @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 _address,
uint256 _saltNonce
) public view returns (bytes32) {
bytes memory encoded = abi.encodePacked(
_to,
_address,
_CACHED_CHAIN_ID,
_CACHED_THIS,
_saltNonce
);
return keccak256(encoded);
}
/**
* @dev claim NFT
* Get whitelist signature from a third-party service, then call this method to claim NFT
* @param saltNonce nonce
* @param signature signature
*/
function claim(
uint256[] memory ids,
uint256 tokenAmount,
uint256 saltNonce,
bytes calldata signature
) external nonReentrant whenNotPaused {
// get current parse;
uint256 count = ids.length;
require(count > 0, "NFTClaimer: ids length must be greater than 0");
if (mintParse == 1) {
require(count <= mintConfig.parse1MaxSupply - parse1Count, "NFTClaimer: exceed parse 1 max supply");
} else {
require(count <= mintConfig.maxSupply - totalCount, "NFTClaimer: exceed max supply");
}
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);
checkSigner(verifier, criteriaMessageHash, signature);
IERC20(mintConfig.currency).transferFrom(to, mintConfig.feeToAddress, tokenAmount);
for (uint256 i = 0; i < count; ++i) {
IClaimAbleNFT(nftAddress).safeMint(to, ids[i]);
}
// require(count > 2, "run to here");
totalCount += count;
if (mintParse == 1) {
parse1Count += count;
}
_useSignature(signature);
emit NFTClaimed(nftAddress, to, ids);
}
function getMessageHash(
address _to,
address _address,
uint256[] memory _ids,
uint256 _tokenAmount,
address _contract,
uint256 _chainId,
uint256 _saltNonce
) public pure returns (bytes32) {
bytes memory encoded = abi.encodePacked(_to, _address, _tokenAmount, _contract, _chainId, _saltNonce);
for (uint256 i = 0; i < _ids.length; ++i) {
encoded = bytes.concat(encoded, abi.encodePacked(_ids[i]));
}
return keccak256(encoded);
}
}

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;
pragma solidity 0.8.19;
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
@ -14,13 +14,13 @@ contract HasSignature is Ownable {
bytes32 ethSignedMessageHash = ECDSA.toEthSignedMessageHash(hash);
address recovered = ECDSA.recover(ethSignedMessageHash, signature);
require(recovered == signer, "[BE] invalid signature");
require(recovered == signer, "invalid signature");
}
modifier signatureValid(bytes calldata signature) {
require(
!_usedSignatures[signature],
"[BE] signature used. please send another transaction with new signature"
"signature used. please send another transaction with new signature"
);
_;
}

125
contracts/game/NFTLock.sol Normal file
View File

@ -0,0 +1,125 @@
// 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 {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
contract NFTLock is AccessControl, ERC721Holder {
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");
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);
constructor() {
_setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
_setupRole(OPERATOR_ROLE, msg.sender);
_setupRole(UNLOCK_ROLE, msg.sender);
_setupRole(RELEASE_ROLE, msg.sender);
}
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 {
require(tokenIds.length <= 100, "tokenIds too many");
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]);
}
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];
}
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();
}
/** ------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 {
for (uint256 i = 0; i < nftList.length; i++) {
supportNftList[nftList[i]] = true;
}
}
function removeSupportNft(address nftAddress) external onlyOperator {
require(supportNftList[nftAddress], "can't remove");
delete supportNftList[nftAddress];
}
/** ------modifier------- **/
modifier onlyOperator() {
require(hasRole(OPERATOR_ROLE, msg.sender), "not operator role");
_;
}
modifier onlyUnlocker() {
require(hasRole(UNLOCK_ROLE, msg.sender), "not unlocker role");
_;
}
modifier onlyReleaser() {
require(hasRole(RELEASE_ROLE, msg.sender), "not releaser role");
_;
}
}

View File

@ -0,0 +1,178 @@
// Copyright Immutable Pty Ltd 2018 - 2023
// SPDX-License-Identifier: Apache 2.0
pragma solidity 0.8.19;
// Access Control
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
// Introspection
import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
// Interfaces
import {IOperatorAllowlist} from "@imtbl/contracts/contracts/allowlist/IOperatorAllowlist.sol";
// Interface to retrieve the implemention stored inside the Proxy contract
interface IProxy {
// Returns the current implementation address used by the proxy contract
// solhint-disable-next-line func-name-mixedcase
function PROXY_getImplementation() external view returns (address);
}
/*
OperatorAllowlist is an implementation of a Allowlist registry, storing addresses and bytecode
which are allowed to be approved operators and execute transfers of interfacing token contracts (e.g. ERC721/ERC1155).
The registry will be a deployed contract that tokens may interface with and point to.
OperatorAllowlist is not designed to be upgradeable or extended.
*/
contract OperatorAllowlist is ERC165, AccessControl, IOperatorAllowlist {
/// ===== State Variables =====
/// @notice Only REGISTRAR_ROLE can invoke white listing registration and removal
bytes32 public constant REGISTRAR_ROLE = bytes32("REGISTRAR_ROLE");
/// @notice Mapping of Allowlisted addresses
mapping(address aContract => bool allowed) private addressAllowlist;
/// @notice Mapping of Allowlisted implementation addresses
mapping(address impl => bool allowed) private addressImplementationAllowlist;
/// @notice Mapping of Allowlisted bytecodes
mapping(bytes32 bytecodeHash => bool allowed) private bytecodeAllowlist;
/// ===== Events =====
/// @notice Emitted when a target address is added or removed from the Allowlist
event AddressAllowlistChanged(address indexed target, bool added);
/// @notice Emitted when a target smart contract wallet is added or removed from the Allowlist
event WalletAllowlistChanged(bytes32 indexed targetBytes, address indexed targetAddress, bool added);
/// ===== Constructor =====
/**
* @notice Grants `DEFAULT_ADMIN_ROLE` to the supplied `admin` address
* @param admin the address to grant `DEFAULT_ADMIN_ROLE` to
*/
constructor(address admin) {
_grantRole(DEFAULT_ADMIN_ROLE, admin);
}
/// ===== External functions =====
/**
* @notice Add a target address to Allowlist
* @param addressTargets the addresses to be added to the allowlist
*/
function addAddressToAllowlist(address[] calldata addressTargets) external onlyRole(REGISTRAR_ROLE) {
for (uint256 i; i < addressTargets.length; i++) {
addressAllowlist[addressTargets[i]] = true;
emit AddressAllowlistChanged(addressTargets[i], true);
}
}
/**
* @notice Remove a target address from Allowlist
* @param addressTargets the addresses to be removed from the allowlist
*/
function removeAddressFromAllowlist(address[] calldata addressTargets) external onlyRole(REGISTRAR_ROLE) {
for (uint256 i; i < addressTargets.length; i++) {
delete addressAllowlist[addressTargets[i]];
emit AddressAllowlistChanged(addressTargets[i], false);
}
}
/**
* @notice Add a smart contract wallet to the Allowlist.
* This will allowlist the proxy and implementation contract pair.
* First, the bytecode of the proxy is added to the bytecode allowlist.
* Second, the implementation address stored in the proxy is stored in the
* implementation address allowlist.
* @param walletAddr the wallet address to be added to the allowlist
*/
function addWalletToAllowlist(address walletAddr) external onlyRole(REGISTRAR_ROLE) {
// get bytecode of wallet
bytes32 codeHash;
// solhint-disable-next-line no-inline-assembly
assembly {
codeHash := extcodehash(walletAddr)
}
bytecodeAllowlist[codeHash] = true;
// get address of wallet module
address impl = IProxy(walletAddr).PROXY_getImplementation();
addressImplementationAllowlist[impl] = true;
emit WalletAllowlistChanged(codeHash, walletAddr, true);
}
/**
* @notice Remove a smart contract wallet from the Allowlist
* This will remove the proxy bytecode hash and implementation contract address pair from the allowlist
* @param walletAddr the wallet address to be removed from the allowlist
*/
function removeWalletFromAllowlist(address walletAddr) external onlyRole(REGISTRAR_ROLE) {
// get bytecode of wallet
bytes32 codeHash;
// solhint-disable-next-line no-inline-assembly
assembly {
codeHash := extcodehash(walletAddr)
}
delete bytecodeAllowlist[codeHash];
// get address of wallet module
address impl = IProxy(walletAddr).PROXY_getImplementation();
delete addressImplementationAllowlist[impl];
emit WalletAllowlistChanged(codeHash, walletAddr, false);
}
/**
* @notice Allows admin to grant `user` `REGISTRAR_ROLE` role
* @param user the address that `REGISTRAR_ROLE` will be granted to
*/
function grantRegistrarRole(address user) external onlyRole(DEFAULT_ADMIN_ROLE) {
grantRole(REGISTRAR_ROLE, user);
}
/**
* @notice Allows admin to revoke `REGISTRAR_ROLE` role from `user`
* @param user the address that `REGISTRAR_ROLE` will be revoked from
*/
function revokeRegistrarRole(address user) external onlyRole(DEFAULT_ADMIN_ROLE) {
revokeRole(REGISTRAR_ROLE, user);
}
/// ===== View functions =====
/**
* @notice Returns true if an address is Allowlisted, false otherwise
* @param target the address that will be checked for presence in the allowlist
*/
function isAllowlisted(address target) external view override returns (bool) {
if (addressAllowlist[target]) {
return true;
}
// Check if caller is a Allowlisted smart contract wallet
bytes32 codeHash;
// solhint-disable-next-line no-inline-assembly
assembly {
codeHash := extcodehash(target)
}
if (bytecodeAllowlist[codeHash]) {
// If wallet proxy bytecode is approved, check addr of implementation contract
address impl = IProxy(target).PROXY_getImplementation();
return addressImplementationAllowlist[impl];
}
return false;
}
/**
* @notice ERC-165 interface support
* @param interfaceId The interface identifier, which is a 4-byte selector.
*/
function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, AccessControl) returns (bool) {
return interfaceId == type(IOperatorAllowlist).interfaceId || super.supportsInterface(interfaceId);
}
}

View File

@ -1,8 +1,7 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;
pragma solidity 0.8.19;
// import {ImmutableERC721Base} from "@imtbl/contracts/token/erc721/abstract/ImmutableERC721Base.sol";
import {ImmutableERC721MintByID} from "@imtbl/contracts/token/erc721/preset/ImmutableERC721MintByID.sol";
import {ImmutableERC721MintByID} from "@imtbl/contracts/contracts/token/erc721/preset/ImmutableERC721MintByID.sol";
contract CFNFTGame is ImmutableERC721MintByID{
/**

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;
pragma solidity 0.8.19;
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
contract TimeChecker is Ownable {

35
deploy/1_deploy_nft.ts Normal file
View File

@ -0,0 +1,35 @@
import { HardhatRuntimeEnvironment } from "hardhat/types";
import { DeployFunction } from "hardhat-deploy/types";
import { updateArray } from "../scripts/utils"
const deployNFTForGame: DeployFunction =
async function (hre: HardhatRuntimeEnvironment) {
const provider = hre.ethers.provider;
const from = await (await provider.getSigner()).getAddress();
const config = require(`../config/config_${hre.network.name}`);
console.log(config);
const owner = from;
const name = "CFHero";
const symbol = "CFH";
// testnet: 0x6b969FD89dE634d8DE3271EbE97734FEFfcd58eE
// mainnet: 0x5F5EBa8133f68ea22D712b0926e2803E78D89221
const { operatorAllowlist } = config.imtbl;
const { royaltyReceiver, feeNumerator, baseURI, contractURI } = config.token;
const ret = await hre.deployments.deploy("CFNFTGame", {
from,
args: [owner, name, symbol, baseURI, contractURI, operatorAllowlist, royaltyReceiver, feeNumerator],
log: true,
});
console.log("==CFNFTGame addr=", ret.address);
updateArray({
name: "CFHero",
type: "erc721",
json: "assets/contracts/CFNFTGame.json",
address: ret.address,
network: hre.network.name,
});
};
deployNFTForGame.tags = ["CFNFTGame"];
export default deployNFTForGame;

View File

@ -1,22 +0,0 @@
import { HardhatRuntimeEnvironment } from "hardhat/types";
import { DeployFunction } from "hardhat-deploy/types";
import { ethers } from "hardhat";
const deployNFTClaim: DeployFunction =
async function (hre: HardhatRuntimeEnvironment) {
const provider = ethers.provider;
const from = await (await provider.getSigner()).getAddress();
console.log(from);
const ret = await hre.deployments.deploy("NFTClaimStage2", {
from,
args: [],
log: true,
});
console.log("==NFTClaimStage2 addr=", ret.address);
};
deployNFTClaim.tags = ["NFTClaimStage2"];
export default deployNFTClaim;

View File

@ -1,22 +0,0 @@
import { HardhatRuntimeEnvironment } from "hardhat/types";
import { DeployFunction } from "hardhat-deploy/types";
import { ethers } from "hardhat";
const deployNFTForGame: DeployFunction =
async function (hre: HardhatRuntimeEnvironment) {
const provider = ethers.provider;
const from = await (await provider.getSigner()).getAddress();
console.log(from);
const ret = await hre.deployments.deploy("CFNFTGame", {
from,
args: [],
log: true,
});
console.log("==CFNFTGame addr=", ret.address);
};
deployNFTForGame.tags = ["CFNFTGame"];
export default deployNFTForGame;

View File

@ -0,0 +1,38 @@
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();
console.log(from);
const nftAddress = '0xaa34B79A0Ab433eaC900fB3CB9f191F5Cd27501D';
const verifier = '0x5Ab03Aa79Ab91B7420b5CFF134a4188388888888'
const mintConfig = [
1000,
2000,
'0xaa34B79A0Ab433eaC900fB3CB9f191F5Cd27501D',
100,
'0x5Ab03Aa79Ab91B7420b5CFF134a4188388888888'
]
const ret = await hre.deployments.deploy("NFTClaimStage2", {
from,
args: [nftAddress, verifier, mintConfig],
log: true,
});
console.log("==NFTClaimStage2 addr=", ret.address);
updateArray({
name: "NFTClaimStage2",
type: "logic",
json: "assets/contracts/NFTClaimStage2.json",
address: ret.address,
network: hre.network.name,
});
};
deployNFTClaim.tags = ["NFTClaimStage2"];
export default deployNFTClaim;

View File

@ -0,0 +1,30 @@
import { HardhatRuntimeEnvironment } from "hardhat/types";
import { DeployFunction } from "hardhat-deploy/types";
import { updateArray } from "../scripts/utils"
const deployNFTForGame: DeployFunction =
async function (hre: HardhatRuntimeEnvironment) {
const provider = hre.ethers.provider;
const from = await (await provider.getSigner()).getAddress();
const config = require(`../config/config_${hre.network.name}`);
const owner = from;
const name = "Test Token";
const symbol = "TETH";
const ret = await hre.deployments.deploy("ImmutableERC20MinterBurnerPermit", {
from,
args: [owner, owner, owner, name, symbol, '10000000000000000000000000'],
log: true,
});
console.log("==ImmutableERC20MinterBurnerPermit addr=", ret.address);
updateArray({
name: "TestToken",
type: "erc20",
json: "assets/contracts/ImmutableERC20MinterBurnerPermit.json",
address: ret.address,
network: hre.network.name,
});
};
deployNFTForGame.tags = ["TestToken"];
export default deployNFTForGame;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -7,6 +7,9 @@
"@openzeppelin/contracts/security/ReentrancyGuard.sol": {
"content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v4.9.0) (security/ReentrancyGuard.sol)\n\npragma solidity ^0.8.0;\n\n/**\n * @dev Contract module that helps prevent reentrant calls to a function.\n *\n * Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier\n * available, which can be applied to functions to make sure there are no nested\n * (reentrant) calls to them.\n *\n * Note that because there is a single `nonReentrant` guard, functions marked as\n * `nonReentrant` may not call one another. This can be worked around by making\n * those functions `private`, and then adding `external` `nonReentrant` entry\n * points to them.\n *\n * TIP: If you would like to learn more about reentrancy and alternative ways\n * to protect against it, check out our blog post\n * https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul].\n */\nabstract contract ReentrancyGuard {\n // Booleans are more expensive than uint256 or any type that takes up a full\n // word because each write operation emits an extra SLOAD to first read the\n // slot's contents, replace the bits taken up by the boolean, and then write\n // back. This is the compiler's defense against contract upgrades and\n // pointer aliasing, and it cannot be disabled.\n\n // The values being non-zero value makes deployment a bit more expensive,\n // but in exchange the refund on every call to nonReentrant will be lower in\n // amount. Since refunds are capped to a percentage of the total\n // transaction's gas, it is best to keep them low in cases like this one, to\n // increase the likelihood of the full refund coming into effect.\n uint256 private constant _NOT_ENTERED = 1;\n uint256 private constant _ENTERED = 2;\n\n uint256 private _status;\n\n constructor() {\n _status = _NOT_ENTERED;\n }\n\n /**\n * @dev Prevents a contract from calling itself, directly or indirectly.\n * Calling a `nonReentrant` function from another `nonReentrant`\n * function is not supported. It is possible to prevent this from happening\n * by making the `nonReentrant` function external, and making it call a\n * `private` function that does the actual work.\n */\n modifier nonReentrant() {\n _nonReentrantBefore();\n _;\n _nonReentrantAfter();\n }\n\n function _nonReentrantBefore() private {\n // On the first call to nonReentrant, _status will be _NOT_ENTERED\n require(_status != _ENTERED, \"ReentrancyGuard: reentrant call\");\n\n // Any calls to nonReentrant after this point will fail\n _status = _ENTERED;\n }\n\n function _nonReentrantAfter() private {\n // By storing the original value once again, a refund is triggered (see\n // https://eips.ethereum.org/EIPS/eip-2200)\n _status = _NOT_ENTERED;\n }\n\n /**\n * @dev Returns true if the reentrancy guard is currently set to \"entered\", which indicates there is a\n * `nonReentrant` function in the call stack.\n */\n function _reentrancyGuardEntered() internal view returns (bool) {\n return _status == _ENTERED;\n }\n}\n"
},
"@openzeppelin/contracts/token/ERC20/IERC20.sol": {
"content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/IERC20.sol)\n\npragma solidity ^0.8.0;\n\n/**\n * @dev Interface of the ERC20 standard as defined in the EIP.\n */\ninterface IERC20 {\n /**\n * @dev Emitted when `value` tokens are moved from one account (`from`) to\n * another (`to`).\n *\n * Note that `value` may be zero.\n */\n event Transfer(address indexed from, address indexed to, uint256 value);\n\n /**\n * @dev Emitted when the allowance of a `spender` for an `owner` is set by\n * a call to {approve}. `value` is the new allowance.\n */\n event Approval(address indexed owner, address indexed spender, uint256 value);\n\n /**\n * @dev Returns the amount of tokens in existence.\n */\n function totalSupply() external view returns (uint256);\n\n /**\n * @dev Returns the amount of tokens owned by `account`.\n */\n function balanceOf(address account) external view returns (uint256);\n\n /**\n * @dev Moves `amount` tokens from the caller's account to `to`.\n *\n * Returns a boolean value indicating whether the operation succeeded.\n *\n * Emits a {Transfer} event.\n */\n function transfer(address to, uint256 amount) external returns (bool);\n\n /**\n * @dev Returns the remaining number of tokens that `spender` will be\n * allowed to spend on behalf of `owner` through {transferFrom}. This is\n * zero by default.\n *\n * This value changes when {approve} or {transferFrom} are called.\n */\n function allowance(address owner, address spender) external view returns (uint256);\n\n /**\n * @dev Sets `amount` as the allowance of `spender` over the caller's tokens.\n *\n * Returns a boolean value indicating whether the operation succeeded.\n *\n * IMPORTANT: Beware that changing an allowance with this method brings the risk\n * that someone may use both the old and the new allowance by unfortunate\n * transaction ordering. One possible solution to mitigate this race\n * condition is to first reduce the spender's allowance to 0 and set the\n * desired value afterwards:\n * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729\n *\n * Emits an {Approval} event.\n */\n function approve(address spender, uint256 amount) external returns (bool);\n\n /**\n * @dev Moves `amount` tokens from `from` to `to` using the\n * allowance mechanism. `amount` is then deducted from the caller's\n * allowance.\n *\n * Returns a boolean value indicating whether the operation succeeded.\n *\n * Emits a {Transfer} event.\n */\n function transferFrom(address from, address to, uint256 amount) external returns (bool);\n}\n"
},
"@openzeppelin/contracts/utils/Context.sol": {
"content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v4.9.4) (utils/Context.sol)\n\npragma solidity ^0.8.0;\n\n/**\n * @dev Provides information about the current execution context, including the\n * sender of the transaction and its data. While these are generally available\n * via msg.sender and msg.data, they should not be accessed in such a direct\n * manner, since when dealing with meta-transactions the account sending and\n * paying for execution may not be the actual sender (as far as an application\n * is concerned).\n *\n * This contract is only required for intermediate, library-like contracts.\n */\nabstract contract Context {\n function _msgSender() internal view virtual returns (address) {\n return msg.sender;\n }\n\n function _msgData() internal view virtual returns (bytes calldata) {\n return msg.data;\n }\n\n function _contextSuffixLength() internal view virtual returns (uint256) {\n return 0;\n }\n}\n"
},
@ -22,17 +25,11 @@
"@openzeppelin/contracts/utils/Strings.sol": {
"content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v4.9.0) (utils/Strings.sol)\n\npragma solidity ^0.8.0;\n\nimport \"./math/Math.sol\";\nimport \"./math/SignedMath.sol\";\n\n/**\n * @dev String operations.\n */\nlibrary Strings {\n bytes16 private constant _SYMBOLS = \"0123456789abcdef\";\n uint8 private constant _ADDRESS_LENGTH = 20;\n\n /**\n * @dev Converts a `uint256` to its ASCII `string` decimal representation.\n */\n function toString(uint256 value) internal pure returns (string memory) {\n unchecked {\n uint256 length = Math.log10(value) + 1;\n string memory buffer = new string(length);\n uint256 ptr;\n /// @solidity memory-safe-assembly\n assembly {\n ptr := add(buffer, add(32, length))\n }\n while (true) {\n ptr--;\n /// @solidity memory-safe-assembly\n assembly {\n mstore8(ptr, byte(mod(value, 10), _SYMBOLS))\n }\n value /= 10;\n if (value == 0) break;\n }\n return buffer;\n }\n }\n\n /**\n * @dev Converts a `int256` to its ASCII `string` decimal representation.\n */\n function toString(int256 value) internal pure returns (string memory) {\n return string(abi.encodePacked(value < 0 ? \"-\" : \"\", toString(SignedMath.abs(value))));\n }\n\n /**\n * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation.\n */\n function toHexString(uint256 value) internal pure returns (string memory) {\n unchecked {\n return toHexString(value, Math.log256(value) + 1);\n }\n }\n\n /**\n * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed length.\n */\n function toHexString(uint256 value, uint256 length) internal pure returns (string memory) {\n bytes memory buffer = new bytes(2 * length + 2);\n buffer[0] = \"0\";\n buffer[1] = \"x\";\n for (uint256 i = 2 * length + 1; i > 1; --i) {\n buffer[i] = _SYMBOLS[value & 0xf];\n value >>= 4;\n }\n require(value == 0, \"Strings: hex length insufficient\");\n return string(buffer);\n }\n\n /**\n * @dev Converts an `address` with fixed length of 20 bytes to its not checksummed ASCII `string` hexadecimal representation.\n */\n function toHexString(address addr) internal pure returns (string memory) {\n return toHexString(uint256(uint160(addr)), _ADDRESS_LENGTH);\n }\n\n /**\n * @dev Returns true if the two strings are equal.\n */\n function equal(string memory a, string memory b) internal pure returns (bool) {\n return keccak256(bytes(a)) == keccak256(bytes(b));\n }\n}\n"
},
"contracts/activity/TreasureHunt.sol": {
"content": "// SPDX-License-Identifier: MIT\npragma solidity 0.8.23;\nimport \"@openzeppelin/contracts/security/ReentrancyGuard.sol\";\nimport \"../core/HasSignature.sol\";\nimport \"../utils/TimeChecker.sol\";\n\n\ncontract TreasureHunt is HasSignature, ReentrancyGuard, TimeChecker{\n mapping(address => mapping(uint256 => uint256)) public checkinHistory;\n mapping(address => mapping(uint256 => uint256)) public exploreHistory;\n mapping(address => mapping(uint256 => uint256)) public enhanceHistory;\n mapping(address => mapping(uint256 => uint256)) public claimTaskHistory;\n mapping(address => mapping(uint256 => uint256)) public openBoxHistory;\n uint256 private immutable _CACHED_CHAIN_ID;\n address private immutable _CACHED_THIS;\n address private verifier;\n\n bool public isPaused = false;\n\n event ActionEvent(\n address indexed user,\n uint256 indexed action,\n uint256 value\n );\n\n event StateUpdated(bool isPaused);\n event VerifierUpdated(address indexed verifier);\n\n constructor() {\n _CACHED_CHAIN_ID = block.chainid;\n _CACHED_THIS = address(this);\n }\n\n modifier whenNotPaused() {\n require(!isPaused, \"TreasureHunt: paused\");\n _;\n }\n\n function updatePaused(bool _isPaused) external onlyOwner {\n isPaused = _isPaused;\n emit StateUpdated(_isPaused);\n }\n\n /**\n * @dev update verifier address\n */\n function updateVerifier(address _verifier) external onlyOwner {\n require(_verifier != address(0), \"TreasureHunt: address can not be zero\");\n verifier = _verifier;\n emit VerifierUpdated(_verifier);\n }\n \n // daily checkin\n function dailyCheckin() external whenNotPaused {\n address user = _msgSender();\n uint256 day = block.timestamp / 1 days;\n require(checkinHistory[user][day] == 0, \"TreasureHunt: already checked in\");\n checkinHistory[user][day] = 1;\n emit ActionEvent(user, 1, day);\n }\n\n // explore\n function explore(\n uint256 step\n ) external whenNotPaused {\n address user = _msgSender();\n exploreHistory[user][step] = 1;\n emit ActionEvent(user, 2, step);\n } \n\n // enhance box\n function enhanceBox(\n uint256 boxId\n ) external whenNotPaused {\n address user = _msgSender();\n require(enhanceHistory[user][boxId] == 0, \"TreasureHunt: already enhanced\");\n enhanceHistory[user][boxId] = 1;\n emit ActionEvent(user, 3, boxId);\n } \n\n // open box\n function openBox(\n uint256 boxId\n ) external whenNotPaused {\n address user = _msgSender();\n require(openBoxHistory[user][boxId] == 0, \"TreasureHunt: already opened\");\n openBoxHistory[user][boxId] = 1;\n emit ActionEvent(user, 4, boxId);\n }\n\n // claim task reward\n function claimTaskReward(\n uint256 taskId\n ) external whenNotPaused{\n address user = _msgSender();\n require(claimTaskHistory[user][taskId] == 0, \"TreasureHunt: already claimed\");\n claimTaskHistory[user][taskId] = 1;\n emit ActionEvent(user, 5, taskId);\n } \n\n function generalAction(\n uint256 actionType,\n uint256 val,\n uint256 signTime,\n uint256 saltNonce,\n bytes calldata signature\n ) external nonReentrant whenNotPaused timeValid(signTime){\n address user = _msgSender();\n bytes32 criteriaMessageHash = getMessageHash(\n user,\n actionType,\n val,\n _CACHED_THIS,\n _CACHED_CHAIN_ID,\n signTime,\n saltNonce\n );\n checkSigner(verifier, criteriaMessageHash, signature);\n if (actionType == 1) {\n require(checkinHistory[user][val] == 0, \"TreasureHunt: already checked in\");\n checkinHistory[user][val] = 1;\n } else if (actionType == 2) {\n require(exploreHistory[user][val] == 0, \"TreasureHunt: already explored\");\n exploreHistory[user][val] = 1;\n } else if (actionType == 3) {\n require(enhanceHistory[user][val] == 0, \"TreasureHunt: already enhanced\");\n enhanceHistory[user][val] = 1;\n } else if (actionType == 4) {\n require(openBoxHistory[user][val] == 0, \"TreasureHunt: already opened\");\n openBoxHistory[user][val] = 1;\n } else if (actionType == 5) {\n require(claimTaskHistory[user][val] == 0, \"TreasureHunt: already claimed\");\n claimTaskHistory[user][val] = 1;\n } else {\n revert(\"TreasureHunt: invalid action type\");\n }\n emit ActionEvent(user, actionType, val);\n }\n\n function getMessageHash(\n address _user,\n uint256 _type,\n uint256 _val,\n address _contract,\n uint256 _chainId,\n uint256 _signTime,\n uint256 _saltNonce\n ) public pure returns (bytes32) {\n bytes memory encoded = abi.encodePacked(\n _user,\n _type,\n _val,\n _contract,\n _chainId,\n _signTime,\n _saltNonce\n );\n return keccak256(encoded);\n }\n}\n"
"contracts/activity/NFTClaimStage2.sol": {
"content": "// SPDX-License-Identifier: MIT\npragma solidity 0.8.19;\n\nimport {ReentrancyGuard} from \"@openzeppelin/contracts/security/ReentrancyGuard.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {HasSignature} from \"../core/HasSignature.sol\";\n\n/**\n * Contract for the activity of NFT claim stage 2.\n */\ninterface IClaimAbleNFT {\n function safeMint(address to, uint256 tokenID) external;\n}\n\ncontract NFTClaimStage2 is HasSignature, ReentrancyGuard {\n struct MintConfig {\n uint256 parse1MaxSupply; // max supply for phase1\n uint256 maxSupply; // max supply for phase2\n address currency; // token address which user must pay to mint\n uint256 mintPrice; // in wei\n address feeToAddress; // wallet address to receive mint fee\n }\n // parse: 0: not open or end, 1: phase1, 2: phase2\n uint256 public mintParse = 0;\n\n uint256 public immutable _CACHED_CHAIN_ID;\n address public immutable _CACHED_THIS;\n address public immutable nftAddress;\n\n address public verifier;\n MintConfig public mintConfig;\n uint256 public parse1Count;\n uint256 public totalCount;\n\n event NFTClaimed(address indexed nftAddress, address indexed to, uint256[] ids);\n\n event ParseUpdated(uint256 _parse);\n event MintConfigUpdated(MintConfig config);\n event VerifierUpdated(address indexed verifier);\n\n constructor(address _nftAddress, address _verifier, MintConfig memory _mintConfig) {\n _CACHED_CHAIN_ID = block.chainid;\n _CACHED_THIS = address(this);\n nftAddress = _nftAddress;\n verifier = _verifier;\n mintConfig = _mintConfig;\n }\n\n modifier whenNotPaused() {\n require(mintParse > 0, \"NFTClaimer: not begin or ended\");\n _;\n }\n\n function updateMintParse(uint256 _mintParse) external onlyOwner {\n mintParse = _mintParse;\n emit ParseUpdated(_mintParse);\n }\n\n function updateMintConfig(MintConfig calldata config) external onlyOwner {\n mintConfig = config;\n emit MintConfigUpdated(config);\n }\n\n /**\n * @dev update verifier address\n */\n function updateVerifier(address _verifier) external onlyOwner {\n require(_verifier != address(0), \"NFTClaimer: address can not be zero\");\n verifier = _verifier;\n emit VerifierUpdated(_verifier);\n }\n\n /**\n * @dev claim NFT\n * Get whitelist signature from a third-party service, then call this method to claim NFT\n * @param saltNonce nonce\n * @param signature signature\n */\n function claim(\n uint256[] memory ids,\n uint256 tokenAmount,\n uint256 saltNonce,\n bytes calldata signature\n ) external nonReentrant whenNotPaused {\n // get current parse;\n uint256 count = ids.length;\n require(count > 0, \"NFTClaimer: ids length must be greater than 0\");\n if (mintParse == 1) {\n require(count <= mintConfig.parse1MaxSupply - parse1Count, \"NFTClaimer: exceed parse 1 max supply\");\n } else {\n require(count <= mintConfig.maxSupply - totalCount, \"NFTClaimer: exceed max supply\");\n }\n require(tokenAmount >= mintConfig.mintPrice * count, \"NFTClaimer: insufficient token amount\");\n address to = _msgSender();\n bytes32 criteriaMessageHash = getMessageHash(to, nftAddress, ids, tokenAmount, _CACHED_THIS, _CACHED_CHAIN_ID, saltNonce);\n checkSigner(verifier, criteriaMessageHash, signature);\n IERC20(mintConfig.currency).transferFrom(to, mintConfig.feeToAddress, tokenAmount);\n for (uint256 i = 0; i < count; ++i) {\n IClaimAbleNFT(nftAddress).safeMint(to, ids[i]);\n }\n // require(count > 2, \"run to here\");\n totalCount += count;\n if (mintParse == 1) {\n parse1Count += count;\n }\n _useSignature(signature);\n emit NFTClaimed(nftAddress, to, ids);\n }\n\n function getMessageHash(\n address _to,\n address _address,\n uint256[] memory _ids,\n uint256 _tokenAmount,\n address _contract,\n uint256 _chainId,\n uint256 _saltNonce\n ) public pure returns (bytes32) {\n bytes memory encoded = abi.encodePacked(_to, _address, _tokenAmount, _contract, _chainId, _saltNonce);\n for (uint256 i = 0; i < _ids.length; ++i) {\n encoded = bytes.concat(encoded, abi.encodePacked(_ids[i]));\n }\n return keccak256(encoded);\n }\n}\n"
},
"contracts/core/HasSignature.sol": {
"content": "// SPDX-License-Identifier: MIT\npragma solidity 0.8.23;\nimport \"@openzeppelin/contracts/utils/cryptography/ECDSA.sol\";\nimport \"@openzeppelin/contracts/access/Ownable.sol\";\n\ncontract HasSignature is Ownable {\n mapping(bytes => bool) private _usedSignatures;\n\n function checkSigner(\n address signer,\n bytes32 hash,\n bytes memory signature\n ) public pure {\n bytes32 ethSignedMessageHash = ECDSA.toEthSignedMessageHash(hash);\n\n address recovered = ECDSA.recover(ethSignedMessageHash, signature);\n require(recovered == signer, \"[BE] invalid signature\");\n }\n\n modifier signatureValid(bytes calldata signature) {\n require(\n !_usedSignatures[signature],\n \"[BE] signature used. please send another transaction with new signature\"\n );\n _;\n }\n\n function _useSignature(bytes calldata signature) internal {\n if (!_usedSignatures[signature]) {\n _usedSignatures[signature] = true;\n }\n }\n}\n"
},
"contracts/Lock.sol": {
"content": "// SPDX-License-Identifier: UNLICENSED\npragma solidity 0.8.23;\n\n// Uncomment this line to use console.log\n// import \"hardhat/console.sol\";\n\ncontract Lock {\n uint public unlockTime;\n address payable public owner;\n\n event Withdrawal(uint amount, uint when);\n\n constructor(uint _unlockTime) payable {\n require(\n block.timestamp < _unlockTime,\n \"Unlock time should be in the future\"\n );\n\n unlockTime = _unlockTime;\n owner = payable(msg.sender);\n }\n\n function withdraw() public {\n // Uncomment this line, and the import of \"hardhat/console.sol\", to print a log in your terminal\n // console.log(\"Unlock time is %o and block timestamp is %o\", unlockTime, block.timestamp);\n\n require(block.timestamp >= unlockTime, \"You can't withdraw yet\");\n require(msg.sender == owner, \"You aren't the owner\");\n\n emit Withdrawal(address(this).balance, block.timestamp);\n\n owner.transfer(address(this).balance);\n }\n}\n"
},
"contracts/utils/TimeChecker.sol": {
"content": "// SPDX-License-Identifier: MIT\npragma solidity 0.8.23;\nimport \"@openzeppelin/contracts/access/Ownable.sol\";\n\ncontract TimeChecker is Ownable {\n uint256 public duration;\n uint256 public minDuration;\n\n event DurationUpdated(uint256 indexed duration);\n\n constructor() {\n duration = 1 days;\n minDuration = 30 minutes;\n }\n\n /**\n * @dev Check if the time is valid\n */\n modifier timeValid(uint256 time) {\n require(\n time + duration >= block.timestamp,\n \"expired, please send another transaction with new signature\"\n );\n _;\n }\n\n\n /**\n * @dev Change duration value\n */\n function updateDuation(uint256 valNew) external onlyOwner {\n require(valNew > minDuration, \"duration too short\");\n duration = valNew;\n emit DurationUpdated(valNew);\n }\n}\n"
"content": "// SPDX-License-Identifier: MIT\npragma solidity 0.8.19;\nimport {ECDSA} from \"@openzeppelin/contracts/utils/cryptography/ECDSA.sol\";\nimport {Ownable} from \"@openzeppelin/contracts/access/Ownable.sol\";\n\ncontract HasSignature is Ownable {\n mapping(bytes signature => bool status) private _usedSignatures;\n\n function checkSigner(\n address signer,\n bytes32 hash,\n bytes memory signature\n ) public pure {\n bytes32 ethSignedMessageHash = ECDSA.toEthSignedMessageHash(hash);\n\n address recovered = ECDSA.recover(ethSignedMessageHash, signature);\n require(recovered == signer, \"invalid signature\");\n }\n\n modifier signatureValid(bytes calldata signature) {\n require(\n !_usedSignatures[signature],\n \"signature used. please send another transaction with new signature\"\n );\n _;\n }\n\n function _useSignature(bytes calldata signature) internal {\n if (!_usedSignatures[signature]) {\n _usedSignatures[signature] = true;\n }\n }\n}\n"
}
},
"settings": {
@ -40,7 +37,7 @@
"enabled": true,
"runs": 200
},
"evmVersion": "paris",
"viaIR": true,
"outputSelection": {
"*": {
"*": [

View File

@ -8,15 +8,6 @@ dotenv.config();
const config: HardhatUserConfig = {
solidity: {
compilers: [
{
version: "0.8.23",
settings: {
optimizer: {
enabled: true,
runs: 200,
},
},
},
{
version: "0.8.19",
settings: {
@ -24,6 +15,7 @@ const config: HardhatUserConfig = {
enabled: true,
runs: 200,
},
viaIR: true,
},
},
{

20
out/imtbl_test_dev.json Normal file
View File

@ -0,0 +1,20 @@
[
{
"name": "CFHero",
"type": "erc721",
"json": "assets/contracts/CFNFTGame.json",
"address": "0xaa34B79A0Ab433eaC900fB3CB9f191F5Cd27501D"
},
{
"name": "TestToken",
"type": "erc20",
"json": "assets/contracts/ImmutableERC20MinterBurnerPermit.json",
"address": "0xFd42bfb03212dA7e1A4608a44d7658641D99CF34"
},
{
"name": "NFTClaimStage2",
"type": "logic",
"json": "assets/contracts/NFTClaimStage2.json",
"address": "0xf45702180314187a3549FEDac3B78349b47ca6A0"
}
]

View File

@ -5,10 +5,12 @@
"main": "index.js",
"scripts": {
"test": "hardhat test",
"compile": "hardhat compile",
"compile": "hardhat compile --show-stack-traces",
"clean": "hardhat clean",
"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:testtoken": "hardhat deploy --tags TestToken --network imtbl_test --reset",
"solhint": "solhint --config ./.solhint.json"
},
"author": "",
@ -34,6 +36,7 @@
"chai": "^4.2.0",
"dotenv": "^16.4.5",
"ethers": "^6.12.1",
"fs-jetpack": "^5.1.0",
"hardhat": "^2.22.4",
"hardhat-deploy": "^0.12.4",
"hardhat-deploy-ethers": "^0.4.2",

29
scripts/utils.ts Normal file
View File

@ -0,0 +1,29 @@
import { read, write } from "fs-jetpack";
export const updateArray = ({ name, type, json, address, network }: { name: string, type: string, json: string, address: string, network: string }) => {
let env = process.env.NODE_ENV || "dev";
const filename = `./out/${network}_${env}.json`;
let cfgs = read(filename, "json");
cfgs = cfgs || [];
if (cfgs.find((item: any) => item.name === name)) {
cfgs.splice(
cfgs.findIndex((item: any) => item.name === name),
1,
);
}
cfgs.push({
name,
type,
json,
address,
});
write(filename, cfgs);
return cfgs;
};
export const loadData = function ({ network }: { network: string }) {
let env = process.env.NODE_ENV || "dev";
const filename = `./out/${network}_${env}.json`;
return read(filename, "json");
};

260
test/testNFTClaim.ts Normal file
View File

@ -0,0 +1,260 @@
import { expect } from 'chai'
import hre from "hardhat";
import {
getBytes,
solidityPackedKeccak256,
} from 'ethers'
import {
loadFixture,
} from "@nomicfoundation/hardhat-toolbox/network-helpers";
describe('NFTClaimStage2', function() {
async function deployOneContract() {
// Contracts are deployed using the first signer/account by default
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 CFFT = await hre.ethers.getContractFactory("ImmutableERC20MinterBurnerPermit");
const ft = await CFFT.deploy(owner.address, owner.address, owner.address, "test usdc", "usdc", '100000000000000000000000000');
await ft.grantMinterRole(owner.address);
await ft.mint(otherAccount.address, '1000');
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 NFTClaimStage2 = await hre.ethers.getContractFactory("NFTClaimStage2");
const mintConfig = [
1000,
2000,
ft.target,
100,
owner.address
]
const nftClaimer = await NFTClaimStage2.deploy( nftAddress, verifier, mintConfig);
await nft.grantMinterRole(nftClaimer.target)
const chainId = hre.network.config.chainId
return { nftClaimer, owner, otherAccount, verifier, nftAddress, ft, nft, chainId };
}
describe("Deployment", function () {
it('should deploy NFTClaimStage2', async function() {
const { nftClaimer } = await loadFixture(deployOneContract);
expect((await nftClaimer.mintConfig()).mintPrice).to.equal(100);
});
it('should deploy NFTClaimStage2 with the correct verifier', async function() {
const { nftClaimer, verifier } = await loadFixture(deployOneContract);
expect(await nftClaimer.verifier()).to.equal(verifier);
});
it('should deploy NFTClaimStage2 with the correct NFT address', async function() {
const { nftClaimer, nftAddress } = await loadFixture(deployOneContract);
expect(await nftClaimer.nftAddress()).to.equal(nftAddress);
});
})
describe("update settings", function () {
it('should update mintConfig', async function() {
const { nftClaimer } = await loadFixture(deployOneContract);
const mintConfig = [
1000,
2000,
'0xaa34B79A0Ab433eaC900fB3CB9f191F5Cd27501D',
200,
'0x5Ab03Aa79Ab91B7420b5CFF134a4188388888888'
]
await nftClaimer.updateMintConfig(mintConfig);
expect((await nftClaimer.mintConfig()).mintPrice).to.equal(200);
});
it('should update verifier', async function() {
const { nftClaimer, otherAccount } = await loadFixture(deployOneContract);
await nftClaimer.updateVerifier(otherAccount.address);
expect(await nftClaimer.verifier()).to.equal(otherAccount.address);
});
it('should update parse', async function() {
const { nftClaimer } = await loadFixture(deployOneContract);
await nftClaimer.updateMintParse(1);
expect(await nftClaimer.mintParse()).to.equal(1);
});
});
describe("claim", function () {
it('should claim NFT', async function() {
const { nftClaimer, owner, otherAccount, ft, nft, chainId } = await loadFixture(deployOneContract);
await nftClaimer.updateMintParse(1);
const price = 100
//use ft.connect() to send a transaction from another account
// @ts-ignore
await ft.connect(otherAccount).approve(nftClaimer.target, price)
const nonce = (Math.random() * 1000) | 0;
const ids = ['1002']
let localMsgHash = solidityPackedKeccak256(["address", "address", "uint256", "address", "uint256", "uint256", ...ids.map(() => "uint256")],
[otherAccount.address, nft.target, price, nftClaimer.target, chainId, nonce, ...ids]);
const contractMsgHash = await nftClaimer.getMessageHash(
otherAccount.address, nft.target, ids, price, nftClaimer.target, chainId, nonce
)
expect(localMsgHash).equal(contractMsgHash);
expect(await nftClaimer.verifier()).equal(owner.address);
expect(await nftClaimer.nftAddress()).equal(nft.target);
const signature = await owner.signMessage(getBytes(contractMsgHash));
// @ts-ignore
await nftClaimer.connect(otherAccount).claim(ids, price, nonce, signature);
expect(await nft.balanceOf(otherAccount.address)).to.equal(1);
expect(await nftClaimer.totalCount()).to.equal(1);
});
it('should claim NFTS', async function() {
const { nftClaimer, owner, otherAccount, ft, nft, chainId } = await loadFixture(deployOneContract);
await nftClaimer.updateMintParse(1);
const price = 100
const ids = ['1002', '1003', '1004']
const tokenAmount = price * ids.length
// @ts-ignore
await ft.connect(otherAccount).approve(nftClaimer.target, tokenAmount)
const nonce = (Math.random() * 1000) | 0;
let localMsgHash = solidityPackedKeccak256(["address", "address", "uint256", "address", "uint256", "uint256", ...ids.map(() => "uint256")],
[otherAccount.address, nft.target, tokenAmount, nftClaimer.target, chainId, nonce, ...ids]);
const signature = await owner.signMessage(getBytes(localMsgHash));
// @ts-ignore
await nftClaimer.connect(otherAccount).claim(ids, tokenAmount, nonce, signature);
expect(await nft.balanceOf(otherAccount.address)).to.equal(ids.length);
expect(await ft.balanceOf(owner.address)).to.equal(tokenAmount)
});
it('should revert claim NFT if the signature error', async function() {
const { nftClaimer, owner, otherAccount, ft, nft, chainId } = await loadFixture(deployOneContract);
await nftClaimer.updateMintParse(1);
const price = 100
// @ts-ignore
await ft.connect(otherAccount).approve(nftClaimer.target, price)
const nonce = (Math.random() * 1000) | 0;
const ids = ['1002']
let localMsgHash = solidityPackedKeccak256(["address", "address", "uint256", "address", "uint256", "uint256", ...ids.map(() => "uint256")],
[otherAccount.address, nft.target, price, nftClaimer.target, chainId, nonce, ...ids]);
const signature = await owner.signMessage(getBytes(localMsgHash));
const nonce2 = (Math.random() * 1000) | 0;
// @ts-ignore
await expect(nftClaimer.connect(otherAccount).claim(ids, price, nonce2, signature)).to.be.revertedWith(
"invalid signature"
);
});
it('should revert claim NFT if the balance not enough', async function() {
const { nftClaimer, owner, otherAccount, ft, nft, chainId } = await loadFixture(deployOneContract);
await nftClaimer.updateMintParse(1);
const price = 1001
const mintConfig = [
1000,
2000,
ft.target,
price,
owner.address
]
await nftClaimer.updateMintConfig(mintConfig);
// @ts-ignore
await ft.connect(otherAccount).approve(nftClaimer.target, price)
const nonce = (Math.random() * 1000) | 0;
const ids = ['1002']
let localMsgHash = solidityPackedKeccak256(["address", "address", "uint256", "address", "uint256", "uint256", ...ids.map(() => "uint256")],
[otherAccount.address, nft.target, price, nftClaimer.target, chainId, nonce, ...ids]);
const signature = await owner.signMessage(getBytes(localMsgHash));
// @ts-ignore
await expect(nftClaimer.connect(otherAccount).claim(ids, price, nonce, signature)).to.be.revertedWith(
"ERC20: transfer amount exceeds balance"
);
});
it('should revert claim NFT if allowance not enough', async function() {
const { nftClaimer, owner, otherAccount, ft, nft, chainId } = await loadFixture(deployOneContract);
await nftClaimer.updateMintParse(1);
const price = 100
// @ts-ignore
await ft.connect(otherAccount).approve(nftClaimer.target, price - 1)
const nonce = (Math.random() * 1000) | 0;
const ids = ['1002']
let localMsgHash = solidityPackedKeccak256(["address", "address", "uint256", "address", "uint256", "uint256", ...ids.map(() => "uint256")],
[otherAccount.address, nft.target, price, nftClaimer.target, chainId, nonce, ...ids]);
const signature = await owner.signMessage(getBytes(localMsgHash));
// @ts-ignore
await expect(nftClaimer.connect(otherAccount).claim(ids, price, nonce, signature)).to.be.revertedWith(
"ERC20: insufficient allowance"
);
});
it('should revert claim NFT if the price not correct', async function() {
const { nftClaimer, owner, otherAccount, ft, nft, chainId } = await loadFixture(deployOneContract);
await nftClaimer.updateMintParse(1);
const price = 99
// @ts-ignore
await ft.connect(otherAccount).approve(nftClaimer.target, price)
const nonce = (Math.random() * 1000) | 0;
const ids = ['1002']
let localMsgHash = solidityPackedKeccak256(["address", "address", "uint256", "address", "uint256", "uint256", ...ids.map(() => "uint256")],
[otherAccount.address, nft.target, price, nftClaimer.target, chainId, nonce, ...ids]);
const signature = await owner.signMessage(getBytes(localMsgHash));
// @ts-ignore
await expect(nftClaimer.connect(otherAccount).claim(ids, price, nonce, signature)).to.be.revertedWith(
"NFTClaimer: insufficient token amount"
);
});
it('should revert claim NFT if the activity not open', async function() {
const { nftClaimer, owner, otherAccount, ft, nft, chainId } = await loadFixture(deployOneContract);
await nftClaimer.updateMintParse(0);
const price = 100
// @ts-ignore
await ft.connect(otherAccount).approve(nftClaimer.target, price)
const nonce = (Math.random() * 1000) | 0;
const ids = ['1002']
let localMsgHash = solidityPackedKeccak256(["address", "address", "uint256", "address", "uint256", "uint256", ...ids.map(() => "uint256")],
[otherAccount.address, nft.target, price, nftClaimer.target, chainId, nonce, ...ids]);
const signature = await owner.signMessage(getBytes(localMsgHash));
// @ts-ignore
await expect(nftClaimer.connect(otherAccount).claim(ids, price, nonce, signature)).to.be.revertedWith(
"NFTClaimer: not begin or ended"
);
});
it('should revert claim NFT if the exceed parse1 max num', async function() {
const { nftClaimer, owner, otherAccount, ft, nft, chainId } = await loadFixture(deployOneContract);
await nftClaimer.updateMintParse(1);
const price = 100
const mintConfig = [
1,
2000,
ft.target,
price,
owner.address
]
await nftClaimer.updateMintConfig(mintConfig);
const ids = ['1002', '1003']
// @ts-ignore
await ft.connect(otherAccount).approve(nftClaimer.target, price * ids.length)
const nonce = (Math.random() * 1000) | 0;
let localMsgHash = solidityPackedKeccak256(["address", "address", "uint256", "address", "uint256", "uint256", ...ids.map(() => "uint256")],
[otherAccount.address, nft.target, price * ids.length, nftClaimer.target, chainId, nonce, ...ids]);
const signature = await owner.signMessage(getBytes(localMsgHash));
// @ts-ignore
await expect(nftClaimer.connect(otherAccount).claim(ids, price, nonce, signature)).to.be.revertedWith(
"NFTClaimer: exceed parse 1 max supply"
);
});
it('should revert claim NFT if the exceed total num', async function() {
const { nftClaimer, owner, otherAccount, ft, nft, chainId } = await loadFixture(deployOneContract);
await nftClaimer.updateMintParse(2);
const price = 100
const mintConfig = [
1000,
1,
ft.target,
price,
owner.address
]
await nftClaimer.updateMintConfig(mintConfig);
const ids = ['1002', '1003']
// @ts-ignore
await ft.connect(otherAccount).approve(nftClaimer.target, price * ids.length)
const nonce = (Math.random() * 1000) | 0;
let localMsgHash = solidityPackedKeccak256(["address", "address", "uint256", "address", "uint256", "uint256", ...ids.map(() => "uint256")],
[otherAccount.address, nft.target, price * ids.length, nftClaimer.target, chainId, nonce, ...ids]);
const signature = await owner.signMessage(getBytes(localMsgHash));
// @ts-ignore
await expect(nftClaimer.connect(otherAccount).claim(ids, price, nonce, signature)).to.be.revertedWith(
"NFTClaimer: exceed max supply"
);
});
});
})

View File

@ -2420,6 +2420,13 @@ fs-extra@^9.1.0:
jsonfile "^6.0.1"
universalify "^2.0.0"
fs-jetpack@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/fs-jetpack/-/fs-jetpack-5.1.0.tgz#dcd34d709b69007c9dc2420a6f2b9e8f986cff0d"
integrity sha512-Xn4fDhLydXkuzepZVsr02jakLlmoARPy+YWIclo4kh0GyNGUHnTqeH/w/qIsVn50dFxtp8otPL2t/HcPJBbxUA==
dependencies:
minimatch "^5.1.0"
fs-readdir-recursive@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz#e32fc030a2ccee44a6b5371308da54be0b397d27"
@ -3228,7 +3235,7 @@ minimatch@5.0.1:
dependencies:
brace-expansion "^2.0.1"
minimatch@^5.0.1:
minimatch@^5.0.1, minimatch@^5.1.0:
version "5.1.6"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96"
integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==