按审计要求, 修改202403活动相关合约

This commit is contained in:
CounterFire2023 2024-03-03 13:48:10 +08:00
parent 663eea2ac8
commit cf3bcd96c7
10 changed files with 13254 additions and 11763 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -3259,7 +3259,7 @@
} }
}, },
"schemaVersion": "3.4.16", "schemaVersion": "3.4.16",
"updatedAt": "2024-01-12T05:44:22.587Z", "updatedAt": "2024-02-27T05:13:36.919Z",
"networkType": "ethereum", "networkType": "ethereum",
"devdoc": { "devdoc": {
"kind": "dev", "kind": "dev",

File diff suppressed because one or more lines are too long

View File

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

View File

@ -11,8 +11,6 @@ contract HasSignature is Ownable {
bytes32 hash, bytes32 hash,
bytes memory signature bytes memory signature
) public pure { ) public pure {
require(signer != address(0), "[BE] invalid signer");
require(signature.length == 65, "[BE] invalid signature length");
bytes32 ethSignedMessageHash = ECDSA.toEthSignedMessageHash(hash); bytes32 ethSignedMessageHash = ECDSA.toEthSignedMessageHash(hash);
address recovered = ECDSA.recover(ethSignedMessageHash, signature); address recovered = ECDSA.recover(ethSignedMessageHash, signature);

View File

@ -2,72 +2,37 @@
pragma solidity 0.8.10; pragma solidity 0.8.10;
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol"; import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Burnable.sol"; import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Burnable.sol";
import "@openzeppelin/contracts/access/AccessControl.sol"; import "@openzeppelin/contracts/access/Ownable.sol";
contract BEBadgeV2 is AccessControl, ERC721Enumerable, ERC721Burnable { contract BEBadgeV2 is Ownable, ERC721Enumerable, ERC721Burnable {
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
string private _baseTokenURI = ""; string private _baseTokenURI = "";
uint256 public immutable supplyLimit; uint256 public immutable supplyLimit;
uint256 private _tokenIndex; uint256 private _tokenIndex;
uint256 public maxBatchSize = 500; mapping(address => bool) public minters;
// ============ Events ============ // ============ Events ============
event MetaAddressUpdated(address indexed metaAddress); event MetaAddressUpdated(address indexed metaAddress);
event BatchLimitUpdated(uint256 indexed maxBatchSize); event MinterUpdated(address indexed minter, bool support);
constructor( constructor(
string memory _name, string memory _name,
string memory _symbol, string memory _symbol,
uint256 _supplyLimt uint256 _supplyLimt
) ERC721(_name, _symbol) { ) ERC721(_name, _symbol) {
_setRoleAdmin(MINTER_ROLE, DEFAULT_ADMIN_ROLE);
_setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
_setupRole(MINTER_ROLE, msg.sender);
supplyLimit = _supplyLimt; 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. * @dev Safely mints a new token and assigns it to the specified address.
* Only the account with the MINTER_ROLE can call this function. * Only the account with the MINTER_ROLE can call this function.
* * tokenId begin with 1.
* @param to The address to which the newly minted token will be assigned. * @param _to The address to which the newly minted token will be assigned.
*/ */
function safeMint(address to) external onlyRole(MINTER_ROLE) returns (uint256){ function safeMint(address _to) external onlyMinter returns (uint256){
require( require(_tokenIndex < supplyLimit, "Exceeds the total supply");
(supplyLimit == 0) || (totalSupply() < supplyLimit),
"Exceeds the total supply"
);
uint256 tokenId = ++_tokenIndex; uint256 tokenId = ++_tokenIndex;
_safeMint(to, tokenId); _safeMint(_to, tokenId);
return tokenId; return tokenId;
} }
@ -76,7 +41,7 @@ contract BEBadgeV2 is AccessControl, ERC721Enumerable, ERC721Burnable {
*/ */
function updateBaseURI( function updateBaseURI(
string calldata baseTokenURI string calldata baseTokenURI
) external onlyRole(DEFAULT_ADMIN_ROLE) { ) external onlyOwner() {
_baseTokenURI = baseTokenURI; _baseTokenURI = baseTokenURI;
} }
@ -84,20 +49,6 @@ contract BEBadgeV2 is AccessControl, ERC721Enumerable, ERC721Burnable {
return _baseTokenURI; 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}. * @dev See {IERC165-_beforeTokenTransfer}.
@ -120,9 +71,25 @@ contract BEBadgeV2 is AccessControl, ERC721Enumerable, ERC721Burnable {
public public
view view
virtual virtual
override(ERC721, AccessControl, ERC721Enumerable) override(ERC721, ERC721Enumerable)
returns (bool) returns (bool)
{ {
return super.supportsInterface(interfaceId); return super.supportsInterface(interfaceId);
} }
/**
* @dev Updates the minters mapping to allow or disallow a specific address to mint tokens.
* Only the contract owner can call this function.
* @param _address The address to update the minting permission for.
* @param _support A boolean indicating whether the address should be allowed to mint tokens or not.
*/
function updateMinters(address _address, bool _support) external onlyOwner {
minters[_address] = _support;
emit MinterUpdated(_address, _support);
}
modifier onlyMinter() {
require(minters[_msgSender()], "Address does not have the minter permission");
_;
}
} }

View File

@ -0,0 +1,52 @@
const base = require("../scripts/base");
const NFTClaimer = artifacts.require("activity/NFTClaimer");
const NFT = artifacts.require("tokens/erc721/BEBadgeV2");
module.exports = async function (deployer, network, accounts) {
const config = require(`../config/config_${network}`);
let cfgs = base.loadData({ network });
// await deployer.deploy(NFT, 'BEBadgeV2', 'BEBadgeV2', 500);
// const nftInstance = await NFT.deployed();
// if (nftInstance) {
// console.log("NFT successfully deployed.");
// console.log("address: " + nftInstance.address);
// }
// base.updateArray({
// name: "BEBadgeV2",
// type: "erc721",
// json: "assets/contracts/BEBadgeV2.json",
// address: nftInstance.address,
// network,
// });
// await deployer.deploy(NFTClaimer);
// const claimInstance = await NFTClaimer.deployed();
// if (claimInstance) {
// console.log("ClaimToken successfully deployed.");
// console.log("address: " + claimInstance.address);
// }
// base.updateArray({
// name: "NFTClaimer",
// type: "logic",
// json: "assets/contracts/NFTClaimer.json",
// address: claimInstance.address,
// network,
// });
const nftAddress = cfgs.find((c) => c.name === "BEBadgeV2").address
const nftInstance = await NFT.at(nftAddress);
const claimerAddress = cfgs.find((c) => c.name === "NFTClaimer").address
const claimInstance = await NFTClaimer.at(claimerAddress);
await claimInstance.updateTokenSupport(nftInstance.address, true);
console.log("updateTokenSupport successfully deployed.");
await claimInstance.updateSigner(config.admins.admin);
console.log("updateSigner successfully deployed.");
await claimInstance.updateStartTime((Date.now() / 1000 | 0) - 3600 * 24);
await claimInstance.updateEndTime((Date.now() / 1000 | 0) + 3600 * 24 * 10);
console.log("update start time and end time successfully deployed.");
await nftInstance.grantRole(await nftInstance.MINTER_ROLE(), claimInstance.address);
console.log("grantRole successfully deployed.");
};

14
out/sepolia_dev.json Normal file
View File

@ -0,0 +1,14 @@
[
{
"name": "BEBadgeV2",
"type": "erc721",
"json": "assets/contracts/BEBadgeV2.json",
"address": "0x09F0dFFA584B1277D7c4E44265a6b5D03303Fc99"
},
{
"name": "NFTClaimer",
"type": "logic",
"json": "assets/contracts/NFTClaimer.json",
"address": "0xe4112A9490765463471a9a73050328153C509B0b"
}
]

View File

@ -20,29 +20,24 @@ contract("NFTClaimer", (accounts) => {
beforeEach(async () => { beforeEach(async () => {
nft = await NFT.new("NFT", "NFT", 5); nft = await NFT.new("NFT", "NFT", 5);
tokenClaimer = await NFTClaimer.new(); tokenClaimer = await NFTClaimer.new(nft.address);
await tokenClaimer.updateTokenSupport(nft.address, true);
await tokenClaimer.updateSigner(singer); await tokenClaimer.updateSigner(singer);
await tokenClaimer.updateStartTime(start); await tokenClaimer.updateStartTime(start);
await tokenClaimer.updateEndTime(end); await tokenClaimer.updateEndTime(end);
}); });
it("should cliam token", async () => { it("should cliam token", async () => {
const minterRole = await nft.MINTER_ROLE(); await nft.updateMinters(tokenClaimer.address, true);
await nft.grantRole(minterRole, tokenClaimer.address);
const token = nft.address; const token = nft.address;
const chainId = await web3.eth.getChainId(); const chainId = await web3.eth.getChainId();
const address = tokenClaimer.address; const address = tokenClaimer.address;
const startTime = Date.now() / 1000 | 0;
const saltNonce = (Math.random() * 1000) | 0; const saltNonce = (Math.random() * 1000) | 0;
let signStr = web3.utils.soliditySha3.apply(this, let signStr = web3.utils.soliditySha3.apply(this,
[user, token, startTime, chainId, address, saltNonce]); [user, token, chainId, address, saltNonce]);
let signature = await web3.eth.sign(signStr, singer); let signature = await web3.eth.sign(signStr, singer);
signature = signature.replace(/00$/, "1b").replace(/01$/, "1c"); signature = signature.replace(/00$/, "1b").replace(/01$/, "1c");
const receipt = await tokenClaimer.claim( const receipt = await tokenClaimer.claim(
token,
startTime,
saltNonce, saltNonce,
signature, signature,
{ from: user } { from: user }
@ -51,31 +46,46 @@ contract("NFTClaimer", (accounts) => {
const balanceOfUser = await nft.balanceOf(user); const balanceOfUser = await nft.balanceOf(user);
assert.equal(balanceOfUser, 1, "Incorrect user balance"); assert.equal(balanceOfUser, 1, "Incorrect user balance");
}); });
it("should revert cliam token for error signature", async () => { it("should revert cliam token for no minter permission", async () => {
const minterRole = await nft.MINTER_ROLE();
await nft.grantRole(minterRole, tokenClaimer.address);
const token = nft.address; const token = nft.address;
const chainId = await web3.eth.getChainId(); const chainId = await web3.eth.getChainId();
const address = tokenClaimer.address; const address = tokenClaimer.address;
const startTime = Date.now() / 1000 | 0;
const saltNonce = (Math.random() * 1000) | 0; const saltNonce = (Math.random() * 1000) | 0;
let signStr = web3.utils.soliditySha3.apply(this, let signStr = web3.utils.soliditySha3.apply(this,
[user, token, startTime, chainId, address, saltNonce]); [user, token, chainId, address, saltNonce]);
let signature = await web3.eth.sign(signStr, singer);
signature = signature.replace(/00$/, "1b").replace(/01$/, "1c");
const err = `Address does not have the minter permission`
await expectRevert(
tokenClaimer.claim(
saltNonce,
signature,
{ from: user }
),
err
);
});
it("should revert cliam token for error signature", async () => {
await nft.updateMinters(tokenClaimer.address, true);
const token = nft.address;
const chainId = await web3.eth.getChainId();
const address = tokenClaimer.address;
const saltNonce = (Math.random() * 1000) | 0;
let signStr = web3.utils.soliditySha3.apply(this,
[user, token, chainId, address, saltNonce]);
let signature = await web3.eth.sign(signStr, singer); let signature = await web3.eth.sign(signStr, singer);
signature = signature.replace(/00$/, "1b").replace(/01$/, "1c"); signature = signature.replace(/00$/, "1b").replace(/01$/, "1c");
const receipt = await tokenClaimer.claim( const receipt = await tokenClaimer.claim(
token,
startTime,
saltNonce, saltNonce,
signature, signature,
{ from: user } { from: user }
); );
const err = `[BE] signature used. please send another transaction with new signature` const err = `NFTClaimer: already claimed.`
await expectRevert( await expectRevert(
tokenClaimer.claim( tokenClaimer.claim(
token,
startTime,
saltNonce, saltNonce,
signature, signature,
{ from: user } { from: user }
@ -88,37 +98,30 @@ contract("NFTClaimer", (accounts) => {
}); });
it("should revert cliam token for already claimed", async () => { it("should revert cliam token for already claimed", async () => {
const minterRole = await nft.MINTER_ROLE(); await nft.updateMinters(tokenClaimer.address, true);
await nft.grantRole(minterRole, tokenClaimer.address);
const token = nft.address; const token = nft.address;
const chainId = await web3.eth.getChainId(); const chainId = await web3.eth.getChainId();
const address = tokenClaimer.address; const address = tokenClaimer.address;
const startTime = Date.now() / 1000 | 0;
const saltNonce = (Math.random() * 1000) | 0; const saltNonce = (Math.random() * 1000) | 0;
let signStr = web3.utils.soliditySha3.apply(this, let signStr = web3.utils.soliditySha3.apply(this,
[user, token, startTime, chainId, address, saltNonce]); [user, token, chainId, address, saltNonce]);
let signature = await web3.eth.sign(signStr, singer); let signature = await web3.eth.sign(signStr, singer);
signature = signature.replace(/00$/, "1b").replace(/01$/, "1c"); signature = signature.replace(/00$/, "1b").replace(/01$/, "1c");
const receipt = await tokenClaimer.claim( const receipt = await tokenClaimer.claim(
token,
startTime,
saltNonce, saltNonce,
signature, signature,
{ from: user } { from: user }
); );
const startTime2 = Date.now() / 1000 | 0;
const saltNonce2 = (Math.random() * 1000) | 0; const saltNonce2 = (Math.random() * 1000) | 0;
let signStr2 = web3.utils.soliditySha3.apply(this, let signStr2 = web3.utils.soliditySha3.apply(this,
[user, token, startTime2, chainId, address, saltNonce2]); [user, token, chainId, address, saltNonce2]);
let signature2 = await web3.eth.sign(signStr2, singer); let signature2 = await web3.eth.sign(signStr2, singer);
signature2 = signature2.replace(/00$/, "1b").replace(/01$/, "1c"); signature2 = signature2.replace(/00$/, "1b").replace(/01$/, "1c");
const err = `NFTClaimer: already claimed` const err = `NFTClaimer: already claimed`
await expectRevert( await expectRevert(
tokenClaimer.claim( tokenClaimer.claim(
token,
startTime2,
saltNonce2, saltNonce2,
signature2, signature2,
{ from: user } { from: user }
@ -127,24 +130,20 @@ contract("NFTClaimer", (accounts) => {
); );
}); });
it("should revert cliam token for activity not begin", async () => { it("should revert cliam token for activity not begin", async () => {
const minterRole = await nft.MINTER_ROLE(); await nft.updateMinters(tokenClaimer.address, true);
await tokenClaimer.updateStartTime(Date.now() / 1000 | 0 + 3600); await tokenClaimer.updateStartTime(Date.now() / 1000 | 0 + 3600);
await nft.grantRole(minterRole, tokenClaimer.address);
const token = nft.address; const token = nft.address;
const chainId = await web3.eth.getChainId(); const chainId = await web3.eth.getChainId();
const address = tokenClaimer.address; const address = tokenClaimer.address;
const startTime = Date.now() / 1000 | 0;
const saltNonce = (Math.random() * 1000) | 0; const saltNonce = (Math.random() * 1000) | 0;
let signStr = web3.utils.soliditySha3.apply(this, let signStr = web3.utils.soliditySha3.apply(this,
[user, token, startTime, chainId, address, saltNonce]); [user, token, chainId, address, saltNonce]);
let signature = await web3.eth.sign(signStr, singer); let signature = await web3.eth.sign(signStr, singer);
signature = signature.replace(/00$/, "1b").replace(/01$/, "1c"); signature = signature.replace(/00$/, "1b").replace(/01$/, "1c");
const err = `NFTClaimer: not started` const err = `NFTClaimer: not started`
await expectRevert( await expectRevert(
tokenClaimer.claim( tokenClaimer.claim(
token,
startTime,
saltNonce, saltNonce,
signature, signature,
{ from: user } { from: user }
@ -153,24 +152,20 @@ contract("NFTClaimer", (accounts) => {
); );
}); });
it("should revert cliam token for activity already end", async () => { it("should revert cliam token for activity already end", async () => {
const minterRole = await nft.MINTER_ROLE(); await nft.updateMinters(tokenClaimer.address, true);
await tokenClaimer.updateEndTime((Date.now() / 1000 | 0) - 3600); await tokenClaimer.updateEndTime((Date.now() / 1000 | 0) - 3600);
await nft.grantRole(minterRole, tokenClaimer.address);
const token = nft.address; const token = nft.address;
const chainId = await web3.eth.getChainId(); const chainId = await web3.eth.getChainId();
const address = tokenClaimer.address; const address = tokenClaimer.address;
const startTime = Date.now() / 1000 | 0;
const saltNonce = (Math.random() * 1000) | 0; const saltNonce = (Math.random() * 1000) | 0;
let signStr = web3.utils.soliditySha3.apply(this, let signStr = web3.utils.soliditySha3.apply(this,
[user, token, startTime, chainId, address, saltNonce]); [user, token, chainId, address, saltNonce]);
let signature = await web3.eth.sign(signStr, singer); let signature = await web3.eth.sign(signStr, singer);
signature = signature.replace(/00$/, "1b").replace(/01$/, "1c"); signature = signature.replace(/00$/, "1b").replace(/01$/, "1c");
const err = `NFTClaimer: already ended` const err = `NFTClaimer: already ended`
await expectRevert( await expectRevert(
tokenClaimer.claim( tokenClaimer.claim(
token,
startTime,
saltNonce, saltNonce,
signature, signature,
{ from: user } { from: user }