add contracts for nft and nft claim activity
This commit is contained in:
parent
b656d3ae3b
commit
a8ab7b4e3a
26048
build/contracts/BEBadgeV2.json
Normal file
26048
build/contracts/BEBadgeV2.json
Normal file
File diff suppressed because one or more lines are too long
2985
build/contracts/IClaimAbleNFT.json
Normal file
2985
build/contracts/IClaimAbleNFT.json
Normal file
File diff suppressed because it is too large
Load Diff
16070
build/contracts/NFTClaimer.json
Normal file
16070
build/contracts/NFTClaimer.json
Normal file
File diff suppressed because one or more lines are too long
116
contracts/activity/NFTClaimer.sol
Normal file
116
contracts/activity/NFTClaimer.sol
Normal 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);
|
||||
}
|
||||
}
|
128
contracts/tokens/erc721/BEBadgeV2.sol
Normal file
128
contracts/tokens/erc721/BEBadgeV2.sol
Normal 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);
|
||||
}
|
||||
}
|
@ -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
109
test/badgev2.test.js
Normal 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
125
test/nftclaimer.test.js
Normal 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
|
||||
);
|
||||
});
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user