add contracts for nft and nft claim activity

This commit is contained in:
CounterFire2023 2024-02-26 17:05:34 +08:00
parent b656d3ae3b
commit a8ab7b4e3a
8 changed files with 45592 additions and 11 deletions

26048
build/contracts/BEBadgeV2.json Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,116 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;
import "../core/HasSignature.sol";
import "../utils/TimeChecker.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
/**
* Contract for activity NFT claim.
*/
interface IClaimAbleNFT {
function safeMint(
address to
) external returns (uint256);
}
contract NFTClaimer is HasSignature, TimeChecker, ReentrancyGuard{
uint256 private immutable _CACHED_CHAIN_ID;
address private immutable _CACHED_THIS;
address public signer;
uint256 public startTime;
uint256 public endTime;
mapping(address => bool) public tokenSupported;
mapping(address => uint256) public claimHistory;
event NFTClaimed(
address indexed nftAddress,
address indexed to,
uint256 tokenId,
uint256 nonce
);
event NFTSupportUpdated(address indexed nftAddress, bool support);
event SignerUpdated(address indexed signer);
event StartTimeUpdated(uint256 indexed startTime);
event EndTimeUpdated(uint256 indexed endTime);
constructor() {
_CACHED_CHAIN_ID = block.chainid;
_CACHED_THIS = address(this);
}
function updateTokenSupport(address nftToken, bool support) external onlyOwner {
tokenSupported[nftToken] = support;
emit NFTSupportUpdated(nftToken, support);
}
function updateStartTime(uint256 _startTime) external onlyOwner {
startTime = _startTime;
emit StartTimeUpdated(_startTime);
}
function updateEndTime(uint256 _endTime) external onlyOwner {
endTime = _endTime;
emit EndTimeUpdated(_endTime);
}
/**
* @dev update signer
* @param account new signer address
*/
function updateSigner(address account) external onlyOwner {
require(account != address(0), "NFTClaimer: address can not be zero");
signer = account;
emit SignerUpdated(account);
}
/**
* @dev claim NFT
* Get whitelist signature from a third-party service, then call this method to claim NFT
* @param nftAddress NFT address
* @param signTime sign time
* @param saltNonce nonce
* @param signature signature
*/
function claim(
address nftAddress,
uint256 signTime,
uint256 saltNonce,
bytes calldata signature
) external signatureValid(signature) timeValid(signTime) nonReentrant {
require(block.timestamp >= startTime, "NFTClaimer: not started");
require(block.timestamp <= endTime, "NFTClaimer: already ended");
require(tokenSupported[nftAddress], "NFTClaimer: unsupported NFT");
address to = _msgSender();
require(claimHistory[to] == 0, "NFTClaimer: already claimed");
bytes32 criteriaMessageHash = getMessageHash(
to,
nftAddress,
signTime,
saltNonce
);
checkSigner(signer, criteriaMessageHash, signature);
uint256 tokenId = IClaimAbleNFT(nftAddress).safeMint(to);
claimHistory[to] = tokenId;
_useSignature(signature);
emit NFTClaimed(nftAddress, to, tokenId, saltNonce);
}
function getMessageHash(
address _to,
address _address,
uint256 _signTime,
uint256 _saltNonce
) public view returns (bytes32) {
bytes memory encoded = abi.encodePacked(
_to,
_address,
_signTime,
_CACHED_CHAIN_ID,
_CACHED_THIS,
_saltNonce
);
return keccak256(encoded);
}
}

View File

@ -0,0 +1,128 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Burnable.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
contract BEBadgeV2 is AccessControl, ERC721Enumerable, ERC721Burnable {
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
string private _baseTokenURI = "";
uint256 public immutable supplyLimit;
uint256 private _tokenIndex;
uint256 public maxBatchSize = 500;
// ============ Events ============
event MetaAddressUpdated(address indexed metaAddress);
event BatchLimitUpdated(uint256 indexed maxBatchSize);
constructor(
string memory _name,
string memory _symbol,
uint256 _supplyLimt
) ERC721(_name, _symbol) {
_setRoleAdmin(MINTER_ROLE, DEFAULT_ADMIN_ROLE);
_setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
_setupRole(MINTER_ROLE, msg.sender);
supplyLimit = _supplyLimt;
}
/**
* @dev Batch mint tokens and transfer to specified address.
*
* Requirements:
* - Caller must have `MINTER_ROLE`.
* - The total supply limit should not be exceeded if supplyLimit is greater than zero.
* - The number of tokenIds offered for minting should not exceed maxBatchSize.
*/
function batchMint(
address to,
uint256 count
) external onlyRole(MINTER_ROLE) returns (uint256[] memory) {
require(count > 0, "count is too small");
require(count <= maxBatchSize, "Exceeds the maximum batch size");
require(
(supplyLimit == 0) || (totalSupply() + count <= supplyLimit),
"Exceeds the total supply"
);
uint256[] memory tokenIds = new uint256[](count);
for (uint256 i = 0; i < count; i++) {
_tokenIndex += 1;
_safeMint(to, _tokenIndex);
tokenIds[i] = _tokenIndex;
}
return tokenIds;
}
/**
* @dev Safely mints a new token and assigns it to the specified address.
* Only the account with the MINTER_ROLE can call this function.
*
* @param to The address to which the newly minted token will be assigned.
*/
function safeMint(address to) external onlyRole(MINTER_ROLE) returns (uint256){
require(
(supplyLimit == 0) || (totalSupply() < supplyLimit),
"Exceeds the total supply"
);
uint256 tokenId = ++_tokenIndex;
_safeMint(to, tokenId);
return tokenId;
}
/**
* @dev Set token URI
*/
function updateBaseURI(
string calldata baseTokenURI
) external onlyRole(DEFAULT_ADMIN_ROLE) {
_baseTokenURI = baseTokenURI;
}
function _baseURI() internal view virtual override returns (string memory) {
return _baseTokenURI;
}
/**
* @dev Updates the maximum batch size for a batch operation.
* @param valNew The new maximum batch size.
* Requirements:
* - The caller must have the DEFAULT_ADMIN_ROLE.
* - The new batch size must be greater than 0.
*/
function updateBatchLimit(
uint256 valNew
) external onlyRole(DEFAULT_ADMIN_ROLE) {
require(valNew > 0, "Batch size is too short");
maxBatchSize = valNew;
emit BatchLimitUpdated(valNew);
}
/**
* @dev See {IERC165-_beforeTokenTransfer}.
*/
function _beforeTokenTransfer(
address from,
address to,
uint256 firstTokenId,
uint256 batchSize
) internal virtual override(ERC721, ERC721Enumerable) {
super._beforeTokenTransfer(from, to, firstTokenId, batchSize);
}
/**
* @dev See {IERC165-supportsInterface}.
*/
function supportsInterface(
bytes4 interfaceId
)
public
view
virtual
override(ERC721, AccessControl, ERC721Enumerable)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
}

View File

@ -3,34 +3,34 @@ pragma solidity 0.8.10;
import "@openzeppelin/contracts/access/Ownable.sol";
contract TimeChecker is Ownable {
uint256 private _duration;
uint256 private minDuration;
uint256 public duration;
uint256 public minDuration;
event DurationUpdated(uint256 indexed duration);
constructor() {
_duration = 1 days;
duration = 1 days;
minDuration = 30 minutes;
}
/**
* @dev Check if the time is valid
*/
modifier timeValid(uint256 time) {
require(
time + _duration >= block.timestamp,
time + duration >= block.timestamp,
"expired, please send another transaction with new signature"
);
_;
}
/**
* @dev Returns the max duration for function called by user
*/
function getDuration() external view returns (uint256 duration) {
return _duration;
}
/**
* @dev Change duration value
*/
function updateDuation(uint256 valNew) external onlyOwner {
require(valNew > minDuration, "duration too short");
_duration = valNew;
duration = valNew;
emit DurationUpdated(valNew);
}
}

109
test/badgev2.test.js Normal file
View File

@ -0,0 +1,109 @@
const BEBadge = artifacts.require("BEBadgeV2");
const {
BN,
constants,
expectEvent,
expectRevert,
} = require("@openzeppelin/test-helpers");
contract("BEBadgeV2", (accounts) => {
let badge;
const owner = accounts[0];
const user = accounts[1];
const executor = accounts[2];
beforeEach(async () => {
badge = await BEBadge.new("CRYPTO ELITE'S HERO", "HERO", 3);
});
it("should batch mint two badges", async () => {
await badge.batchMint(user, 2, { from: owner });
const balanceOfUser = await badge.balanceOf(user);
assert.equal(balanceOfUser, 2, "Incorrect user balance");
});
it("should revert batch minting two badges with not mint role", async () => {
const mintRole = await badge.MINTER_ROLE();
const err = `AccessControl: account ${(user+'').toLowerCase()} is missing role ${mintRole}`
await expectRevert(
badge.batchMint(user, 2, { from: user }),
err
);
});
it("should revert batch minting badges with not enough supply", async () => {
const err = `Exceeds the total supply`
await expectRevert(
badge.batchMint(user, 4, { from: owner }),
err
);
});
it("should mint one badge", async () => {
await badge.safeMint(user, { from: owner });
const balanceOfUser = await badge.balanceOf(user);
assert.equal(balanceOfUser, 1, "Incorrect user balance");
});
it("should revert minting one badge with not mint role", async () => {
const mintRole = await badge.MINTER_ROLE();
const err = `AccessControl: account ${(user+'').toLowerCase()} is missing role ${mintRole}`
await expectRevert(
badge.safeMint(user, { from: user }),
err
);
});
it("should burn one badge", async () => {
await badge.safeMint(user, { from: owner });
const tokenId = await badge.tokenOfOwnerByIndex(user, 0);
await badge.burn(tokenId, { from: user });
const balanceOfUser = await badge.balanceOf(user);
assert.equal(balanceOfUser, 0, "Incorrect user balance");
});
it("should burn one badge with approval", async () => {
await badge.safeMint(user, { from: owner });
const tokenId = await badge.tokenOfOwnerByIndex(user, 0);
const balanceOfUserTmp = await badge.balanceOf(user);
assert.equal(balanceOfUserTmp, 1, "Incorrect user balance");
await badge.approve(executor, tokenId, { from: user });
await badge.burn(tokenId, { from: executor });
const balanceOfUser = await badge.balanceOf(user);
assert.equal(balanceOfUser, 0, "Incorrect user balance");
});
it("should revert burning one badge", async () => {
const err = `ERC721: invalid token ID.`
await expectRevert(
badge.burn(1, { from: user }),
err
);
});
it("should revert burning one badge without approval", async () => {
const err = `ERC721: caller is not token owner or approved.`
await badge.safeMint(user, { from: owner });
const tokenId = await badge.tokenOfOwnerByIndex(user, 0);
await expectRevert(
badge.burn(tokenId, { from: executor }),
err
);
});
it("should receive event for RoleGranted and RoleRevoked", async () => {
const role = await badge.MINTER_ROLE();
const receipt = await badge.grantRole(role, user, { from: owner });
expectEvent(receipt, "RoleGranted", {
account: user,
role: role,
sender: owner,
});
const receipt2 = await badge.revokeRole(role, user, { from: owner });
expectEvent(receipt2, "RoleRevoked", {
account: user,
role: role,
sender: owner,
});
});
});

125
test/nftclaimer.test.js Normal file
View File

@ -0,0 +1,125 @@
const NFTClaimer = artifacts.require("NFTClaimer");
const NFT = artifacts.require("BEBadgeV2");
const {
BN,
constants,
expectEvent,
expectRevert,
} = require("@openzeppelin/test-helpers");
contract("NFTClaimer", (accounts) => {
let tokenClaimer;
let nft;
const owner = accounts[0];
const user = accounts[1];
const singer = accounts[2];
beforeEach(async () => {
nft = await NFT.new("NFT", "NFT", 5);
tokenClaimer = await NFTClaimer.new();
await tokenClaimer.updateTokenSupport(nft.address, true);
await tokenClaimer.updateSigner(singer);
});
it("should cliam token", async () => {
const minterRole = await nft.MINTER_ROLE();
await nft.grantRole(minterRole, tokenClaimer.address);
const token = nft.address;
const chainId = await web3.eth.getChainId();
const address = tokenClaimer.address;
const startTime = Date.now() / 1000 | 0;
const saltNonce = (Math.random() * 1000) | 0;
let signStr = web3.utils.soliditySha3.apply(this,
[user, token, startTime, chainId, address, saltNonce]);
let signature = await web3.eth.sign(signStr, singer);
signature = signature.replace(/00$/, "1b").replace(/01$/, "1c");
const receipt = await tokenClaimer.claim(
token,
startTime,
saltNonce,
signature,
{ from: user }
);
const balanceOfUser = await nft.balanceOf(user);
assert.equal(balanceOfUser, 1, "Incorrect user balance");
});
it("should revert cliam token for error signature", async () => {
const minterRole = await nft.MINTER_ROLE();
await nft.grantRole(minterRole, tokenClaimer.address);
const token = nft.address;
const chainId = await web3.eth.getChainId();
const address = tokenClaimer.address;
const startTime = Date.now() / 1000 | 0;
const saltNonce = (Math.random() * 1000) | 0;
let signStr = web3.utils.soliditySha3.apply(this,
[user, token, startTime, chainId, address, saltNonce]);
let signature = await web3.eth.sign(signStr, singer);
signature = signature.replace(/00$/, "1b").replace(/01$/, "1c");
const receipt = await tokenClaimer.claim(
token,
startTime,
saltNonce,
signature,
{ from: user }
);
const err = `[BE] signature used. please send another transaction with new signature`
await expectRevert(
tokenClaimer.claim(
token,
startTime,
saltNonce,
signature,
{ from: user }
),
err
);
const balanceOfUser = await nft.balanceOf(user);
assert.equal(balanceOfUser, 1, "Incorrect user balance");
});
it("should revert cliam token for already claimed", async () => {
const minterRole = await nft.MINTER_ROLE();
await nft.grantRole(minterRole, tokenClaimer.address);
const token = nft.address;
const chainId = await web3.eth.getChainId();
const address = tokenClaimer.address;
const startTime = Date.now() / 1000 | 0;
const saltNonce = (Math.random() * 1000) | 0;
let signStr = web3.utils.soliditySha3.apply(this,
[user, token, startTime, chainId, address, saltNonce]);
let signature = await web3.eth.sign(signStr, singer);
signature = signature.replace(/00$/, "1b").replace(/01$/, "1c");
const receipt = await tokenClaimer.claim(
token,
startTime,
saltNonce,
signature,
{ from: user }
);
const startTime2 = Date.now() / 1000 | 0;
const saltNonce2 = (Math.random() * 1000) | 0;
let signStr2 = web3.utils.soliditySha3.apply(this,
[user, token, startTime2, chainId, address, saltNonce2]);
let signature2 = await web3.eth.sign(signStr2, singer);
signature2 = signature2.replace(/00$/, "1b").replace(/01$/, "1c");
const err = `NFTClaimer: already claimed`
await expectRevert(
tokenClaimer.claim(
token,
startTime2,
saltNonce2,
signature2,
{ from: user }
),
err
);
});
});