增加erc20空投合约

This commit is contained in:
CounterFire2023 2024-08-16 15:42:28 +08:00
parent 77b430fce1
commit a8adc5929e
10 changed files with 1004 additions and 687 deletions

View File

@ -0,0 +1,128 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import {Pausable} from "@openzeppelin/contracts/security/Pausable.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {HasSignature} from "../core/HasSignature.sol";
import {TimeChecker} from "../utils/TimeChecker.sol";
contract TokenClaim is HasSignature, ReentrancyGuard, Pausable, TimeChecker {
using SafeERC20 for IERC20;
uint256 public immutable _CACHED_CHAIN_ID;
address public immutable _CACHED_THIS;
address public verifier;
mapping(address token => address wallet) public erc20Wallets;
// store user's claimed amount
mapping(address user => mapping(address token => uint256 amount)) public claimedAmount;
event EventERC20Wallet(address erc20, address wallet);
event EventVerifierUpdated(address indexed verifier);
event EventTokenClaimed(
address indexed user,
address indexed token,
address passport,
uint256 amount,
uint256 nonce
);
constructor(address _wallet, address _token, address _verifier, uint256 _duration) TimeChecker(_duration) {
_CACHED_CHAIN_ID = block.chainid;
_CACHED_THIS = address(this);
erc20Wallets[_token] = _wallet;
verifier = _verifier;
}
/**
* @dev update verifier address
*/
function updateVerifier(address _verifier) external onlyOwner {
require(_verifier != address(0), "TokenClaimer: address can not be zero");
verifier = _verifier;
emit EventVerifierUpdated(_verifier);
}
/**
* @dev update pause state
*/
function pause() external onlyOwner {
_pause();
}
/**
* @dev update unpause state
*/
function unpause() external onlyOwner {
_unpause();
}
/**
* @dev update ERC20 wallet
*/
function updateERC20Wallet(address erc20, address wallet) external onlyOwner {
require(erc20Wallets[erc20] != wallet, "TokenClaimer: ERC20 wallet not changed");
erc20Wallets[erc20] = wallet;
emit EventERC20Wallet(erc20, wallet);
}
/**
* @dev claim CEC with signature
*/
function claim(
address passport,
address token,
uint256 amount,
uint256 signTime,
uint256 saltNonce,
bytes calldata signature
) external signatureValid(signature) timeValid(signTime) nonReentrant whenNotPaused {
require(passport != address(0), "TokenClaimer: passport address can not be zero");
require(erc20Wallets[token] != address(0), "TokenClaimer: token is not supported");
require(amount > 0, "TokenClaimer: amount is zero");
address user = _msgSender();
bytes32 criteriaMessageHash = getMessageHash(
user,
passport,
token,
amount,
_CACHED_THIS,
_CACHED_CHAIN_ID,
signTime,
saltNonce
);
checkSigner(verifier, criteriaMessageHash, signature);
_useSignature(signature);
claimedAmount[user][token] += amount;
IERC20(token).safeTransferFrom(erc20Wallets[token], user, amount);
emit EventTokenClaimed(user, token, passport, amount, saltNonce);
}
function getMessageHash(
address _user,
address _passport,
address _token,
uint256 _amount,
address _contract,
uint256 _chainId,
uint256 _signTime,
uint256 _saltNonce
) public pure returns (bytes32) {
bytes memory encoded = abi.encodePacked(
_user,
_passport,
_token,
_amount,
_contract,
_chainId,
_signTime,
_saltNonce
);
return keccak256(encoded);
}
}

View File

@ -0,0 +1,29 @@
import { HardhatRuntimeEnvironment } from "hardhat/types";
import { DeployFunction } from "hardhat-deploy/types";
import { updateArray } from "../scripts/utils"
const deployNFTClaim: DeployFunction =
async function (hre: HardhatRuntimeEnvironment) {
const provider = hre.ethers.provider;
const from = await (await provider.getSigner()).getAddress();
const config = require(`../config/config_${hre.network.name}`);
const { mallFeeAddress, paymentTokens, verifier } = config.market
const ret = await hre.deployments.deploy("TokenClaim", {
from,
args: [mallFeeAddress, paymentTokens[0], verifier, 3600],
log: true,
});
console.log("==TokenClaim addr=", ret.address);
updateArray({
name: "TokenClaim",
type: "logic",
json: "assets/contracts/TokenClaim.json",
address: ret.address,
network: hre.network.name,
});
};
deployNFTClaim.tags = ["TokenClaim"];
export default deployNFTClaim;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -4,6 +4,9 @@
"@openzeppelin/contracts/access/Ownable.sol": { "@openzeppelin/contracts/access/Ownable.sol": {
"content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v4.9.0) (access/Ownable.sol)\n\npragma solidity ^0.8.0;\n\nimport \"../utils/Context.sol\";\n\n/**\n * @dev Contract module which provides a basic access control mechanism, where\n * there is an account (an owner) that can be granted exclusive access to\n * specific functions.\n *\n * By default, the owner account will be the one that deploys the contract. This\n * can later be changed with {transferOwnership}.\n *\n * This module is used through inheritance. It will make available the modifier\n * `onlyOwner`, which can be applied to your functions to restrict their use to\n * the owner.\n */\nabstract contract Ownable is Context {\n address private _owner;\n\n event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);\n\n /**\n * @dev Initializes the contract setting the deployer as the initial owner.\n */\n constructor() {\n _transferOwnership(_msgSender());\n }\n\n /**\n * @dev Throws if called by any account other than the owner.\n */\n modifier onlyOwner() {\n _checkOwner();\n _;\n }\n\n /**\n * @dev Returns the address of the current owner.\n */\n function owner() public view virtual returns (address) {\n return _owner;\n }\n\n /**\n * @dev Throws if the sender is not the owner.\n */\n function _checkOwner() internal view virtual {\n require(owner() == _msgSender(), \"Ownable: caller is not the owner\");\n }\n\n /**\n * @dev Leaves the contract without owner. It will not be possible to call\n * `onlyOwner` functions. Can only be called by the current owner.\n *\n * NOTE: Renouncing ownership will leave the contract without an owner,\n * thereby disabling any functionality that is only available to the owner.\n */\n function renounceOwnership() public virtual onlyOwner {\n _transferOwnership(address(0));\n }\n\n /**\n * @dev Transfers ownership of the contract to a new account (`newOwner`).\n * Can only be called by the current owner.\n */\n function transferOwnership(address newOwner) public virtual onlyOwner {\n require(newOwner != address(0), \"Ownable: new owner is the zero address\");\n _transferOwnership(newOwner);\n }\n\n /**\n * @dev Transfers ownership of the contract to a new account (`newOwner`).\n * Internal function without access restriction.\n */\n function _transferOwnership(address newOwner) internal virtual {\n address oldOwner = _owner;\n _owner = newOwner;\n emit OwnershipTransferred(oldOwner, newOwner);\n }\n}\n" "content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v4.9.0) (access/Ownable.sol)\n\npragma solidity ^0.8.0;\n\nimport \"../utils/Context.sol\";\n\n/**\n * @dev Contract module which provides a basic access control mechanism, where\n * there is an account (an owner) that can be granted exclusive access to\n * specific functions.\n *\n * By default, the owner account will be the one that deploys the contract. This\n * can later be changed with {transferOwnership}.\n *\n * This module is used through inheritance. It will make available the modifier\n * `onlyOwner`, which can be applied to your functions to restrict their use to\n * the owner.\n */\nabstract contract Ownable is Context {\n address private _owner;\n\n event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);\n\n /**\n * @dev Initializes the contract setting the deployer as the initial owner.\n */\n constructor() {\n _transferOwnership(_msgSender());\n }\n\n /**\n * @dev Throws if called by any account other than the owner.\n */\n modifier onlyOwner() {\n _checkOwner();\n _;\n }\n\n /**\n * @dev Returns the address of the current owner.\n */\n function owner() public view virtual returns (address) {\n return _owner;\n }\n\n /**\n * @dev Throws if the sender is not the owner.\n */\n function _checkOwner() internal view virtual {\n require(owner() == _msgSender(), \"Ownable: caller is not the owner\");\n }\n\n /**\n * @dev Leaves the contract without owner. It will not be possible to call\n * `onlyOwner` functions. Can only be called by the current owner.\n *\n * NOTE: Renouncing ownership will leave the contract without an owner,\n * thereby disabling any functionality that is only available to the owner.\n */\n function renounceOwnership() public virtual onlyOwner {\n _transferOwnership(address(0));\n }\n\n /**\n * @dev Transfers ownership of the contract to a new account (`newOwner`).\n * Can only be called by the current owner.\n */\n function transferOwnership(address newOwner) public virtual onlyOwner {\n require(newOwner != address(0), \"Ownable: new owner is the zero address\");\n _transferOwnership(newOwner);\n }\n\n /**\n * @dev Transfers ownership of the contract to a new account (`newOwner`).\n * Internal function without access restriction.\n */\n function _transferOwnership(address newOwner) internal virtual {\n address oldOwner = _owner;\n _owner = newOwner;\n emit OwnershipTransferred(oldOwner, newOwner);\n }\n}\n"
}, },
"@openzeppelin/contracts/security/Pausable.sol": {
"content": "// SPDX-License-Identifier: MIT\n// OpenZeppelin Contracts (last updated v4.7.0) (security/Pausable.sol)\n\npragma solidity ^0.8.0;\n\nimport \"../utils/Context.sol\";\n\n/**\n * @dev Contract module which allows children to implement an emergency stop\n * mechanism that can be triggered by an authorized account.\n *\n * This module is used through inheritance. It will make available the\n * modifiers `whenNotPaused` and `whenPaused`, which can be applied to\n * the functions of your contract. Note that they will not be pausable by\n * simply including this module, only once the modifiers are put in place.\n */\nabstract contract Pausable is Context {\n /**\n * @dev Emitted when the pause is triggered by `account`.\n */\n event Paused(address account);\n\n /**\n * @dev Emitted when the pause is lifted by `account`.\n */\n event Unpaused(address account);\n\n bool private _paused;\n\n /**\n * @dev Initializes the contract in unpaused state.\n */\n constructor() {\n _paused = false;\n }\n\n /**\n * @dev Modifier to make a function callable only when the contract is not paused.\n *\n * Requirements:\n *\n * - The contract must not be paused.\n */\n modifier whenNotPaused() {\n _requireNotPaused();\n _;\n }\n\n /**\n * @dev Modifier to make a function callable only when the contract is paused.\n *\n * Requirements:\n *\n * - The contract must be paused.\n */\n modifier whenPaused() {\n _requirePaused();\n _;\n }\n\n /**\n * @dev Returns true if the contract is paused, and false otherwise.\n */\n function paused() public view virtual returns (bool) {\n return _paused;\n }\n\n /**\n * @dev Throws if the contract is paused.\n */\n function _requireNotPaused() internal view virtual {\n require(!paused(), \"Pausable: paused\");\n }\n\n /**\n * @dev Throws if the contract is not paused.\n */\n function _requirePaused() internal view virtual {\n require(paused(), \"Pausable: not paused\");\n }\n\n /**\n * @dev Triggers stopped state.\n *\n * Requirements:\n *\n * - The contract must not be paused.\n */\n function _pause() internal virtual whenNotPaused {\n _paused = true;\n emit Paused(_msgSender());\n }\n\n /**\n * @dev Returns to normal state.\n *\n * Requirements:\n *\n * - The contract must be paused.\n */\n function _unpause() internal virtual whenPaused {\n _paused = false;\n emit Unpaused(_msgSender());\n }\n}\n"
},
"@openzeppelin/contracts/security/ReentrancyGuard.sol": { "@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" "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"
}, },
@ -34,15 +37,12 @@
"@openzeppelin/contracts/utils/Strings.sol": { "@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" "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/TokenClaim.sol": {
"content": "// SPDX-License-Identifier: MIT\npragma solidity 0.8.19;\n\nimport {ReentrancyGuard} from \"@openzeppelin/contracts/security/ReentrancyGuard.sol\";\nimport {Pausable} from \"@openzeppelin/contracts/security/Pausable.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {SafeERC20} from \"@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol\";\nimport {HasSignature} from \"../core/HasSignature.sol\";\nimport {TimeChecker} from \"../utils/TimeChecker.sol\";\n\ncontract TokenClaim is HasSignature, ReentrancyGuard, Pausable, TimeChecker {\n using SafeERC20 for IERC20;\n\n uint256 public immutable _CACHED_CHAIN_ID;\n address public immutable _CACHED_THIS;\n address public verifier;\n\n mapping(address token => address wallet) public erc20Wallets;\n \n // store user's claimed amount\n mapping(address user => mapping(address token => uint256 amount)) public claimedAmount;\n\n event EventERC20Wallet(address erc20, address wallet);\n \n event EventVerifierUpdated(address indexed verifier);\n event EventTokenClaimed(\n address indexed user, \n address indexed token, \n address passport, \n uint256 amount, \n uint256 nonce\n );\n\n constructor(address _wallet, address _token, address _verifier, uint256 _duration) TimeChecker(_duration) {\n _CACHED_CHAIN_ID = block.chainid;\n _CACHED_THIS = address(this);\n erc20Wallets[_token] = _wallet;\n verifier = _verifier;\n }\n\n /**\n * @dev update verifier address\n */\n function updateVerifier(address _verifier) external onlyOwner {\n require(_verifier != address(0), \"TokenClaimer: address can not be zero\");\n verifier = _verifier;\n emit EventVerifierUpdated(_verifier);\n }\n\n /**\n * @dev update pause state\n */\n function pause() external onlyOwner {\n _pause();\n }\n\n /**\n * @dev update unpause state\n */\n function unpause() external onlyOwner {\n _unpause();\n }\n\n /**\n * @dev update ERC20 wallet\n */\n function updateERC20Wallet(address erc20, address wallet) external onlyOwner {\n require(erc20Wallets[erc20] != wallet, \"TokenClaimer: ERC20 wallet not changed\");\n erc20Wallets[erc20] = wallet;\n emit EventERC20Wallet(erc20, wallet);\n }\n\n /**\n * @dev claim CEC with signature\n */\n\n function claim(\n address passport,\n address token,\n uint256 amount,\n uint256 signTime,\n uint256 saltNonce,\n bytes calldata signature\n ) external signatureValid(signature) timeValid(signTime) nonReentrant whenNotPaused {\n require(passport != address(0), \"TokenClaimer: passport address can not be zero\");\n require(erc20Wallets[token] != address(0), \"TokenClaimer: token is not supported\");\n require(amount > 0, \"TokenClaimer: amount is zero\");\n address user = _msgSender();\n bytes32 criteriaMessageHash = getMessageHash(\n user,\n passport,\n token,\n amount,\n _CACHED_THIS,\n _CACHED_CHAIN_ID,\n signTime,\n saltNonce\n );\n checkSigner(verifier, criteriaMessageHash, signature);\n _useSignature(signature);\n claimedAmount[user][token] += amount;\n IERC20(token).safeTransferFrom(erc20Wallets[token], user, amount);\n emit EventTokenClaimed(user, token, passport, amount, saltNonce);\n }\n\n function getMessageHash(\n address _user,\n address _passport,\n address _token,\n uint256 _amount,\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 _passport,\n _token,\n _amount,\n _contract,\n _chainId,\n _signTime,\n _saltNonce\n );\n return keccak256(encoded);\n }\n}"
},
"contracts/core/HasSignature.sol": { "contracts/core/HasSignature.sol": {
"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" "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"
}, },
"contracts/mall/GameItemMall.sol": {
"content": "// SPDX-License-Identifier: MIT\npragma solidity 0.8.19;\n\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {SafeERC20} from \"@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol\";\nimport {ReentrancyGuard} from \"@openzeppelin/contracts/security/ReentrancyGuard.sol\";\nimport {HasSignature} from \"../core/HasSignature.sol\";\nimport {TimeChecker} from \"../utils/TimeChecker.sol\";\nimport {MallBase} from \"./MallBase.sol\";\n\n/**\n * @title GameItemMall\n * @dev GameItemMall is a contract for managing centralized game items sale,\n * allowing users to buy item in game.\n */\ncontract GameItemMall is MallBase, ReentrancyGuard, HasSignature, TimeChecker {\n using SafeERC20 for IERC20;\n\n mapping(uint256 itemId => address user) public orderIdUsed;\n\n event ItemSoldOut(\n address indexed buyer,\n address indexed passport,\n uint256 indexed orderId,\n address currency,\n uint256 amount\n );\n\n constructor(address _currency, address _feeToAddress, address _verifier, uint256 _duration) \n TimeChecker(_duration)\n MallBase(_currency, _feeToAddress, _verifier){\n }\n\n function buy(\n address passport,\n uint256 orderId,\n address currency,\n uint256 amount,\n uint256 signTime,\n uint256 saltNonce,\n bytes calldata signature\n ) external nonReentrant signatureValid(signature) timeValid(signTime) {\n require(passport != address(0), \"passport address can not be zero\");\n // check if orderId is used\n require(orderIdUsed[orderId] == address(0), \"orderId is used\");\n // check if currency is supported\n require(erc20Supported[currency], \"currency is not supported\");\n // check if amount is valid\n require(amount > 0, \"amount is zero\");\n address buyer = _msgSender();\n bytes32 criteriaMessageHash = getMessageHash(\n buyer,\n passport,\n orderId,\n currency,\n amount,\n _CACHED_THIS,\n _CACHED_CHAIN_ID,\n signTime,\n saltNonce\n );\n checkSigner(verifier, criteriaMessageHash, signature);\n IERC20 paymentContract = IERC20(currency);\n _useSignature(signature);\n orderIdUsed[orderId] = buyer;\n paymentContract.safeTransferFrom(buyer, feeToAddress, amount);\n emit ItemSoldOut(buyer, passport, orderId, currency, amount);\n }\n\n function getMessageHash(\n address _buyer,\n address _passport,\n uint256 _orderId,\n address _currency,\n uint256 _amount,\n address _contract,\n uint256 _chainId,\n uint256 _signTime,\n uint256 _saltNonce\n ) public pure returns (bytes32) {\n bytes memory encoded = abi.encodePacked(\n _buyer,\n _passport,\n _orderId,\n _currency,\n _amount,\n _contract,\n _chainId,\n _signTime,\n _saltNonce\n );\n return keccak256(encoded);\n }\n}"
},
"contracts/mall/MallBase.sol": {
"content": "// SPDX-License-Identifier: MIT\npragma solidity 0.8.19;\n\nimport {Ownable} from \"@openzeppelin/contracts/access/Ownable.sol\";\n\nabstract contract MallBase is Ownable {\n address public verifier;\n // Address to receive transaction fee\n address public feeToAddress;\n\n uint256 public immutable _CACHED_CHAIN_ID;\n address public immutable _CACHED_THIS;\n\n mapping(address token => bool status) public erc20Supported;\n event AddERC20Suppout(address erc20);\n event RemoveERC20Suppout(address erc20);\n event VerifierUpdated(address indexed verifier);\n event FeeToAddressUpdated(address indexed feeToAddress);\n\n constructor(address _currency, address _feeToAddress, address _verifier) {\n _CACHED_CHAIN_ID = block.chainid;\n _CACHED_THIS = address(this);\n verifier = _verifier;\n erc20Supported[_currency] = true;\n feeToAddress = _feeToAddress;\n }\n\n function addERC20Support(address erc20) external onlyOwner {\n require(erc20 != address(0), \"ERC20 address can not be zero\");\n erc20Supported[erc20] = true;\n emit AddERC20Suppout(erc20);\n }\n\n function removeERC20Support(address erc20) external onlyOwner {\n erc20Supported[erc20] = false;\n emit RemoveERC20Suppout(erc20);\n }\n\n /**\n * @dev update verifier address\n */\n function updateVerifier(address _verifier) external onlyOwner {\n require(_verifier != address(0), \"address can not be zero\");\n verifier = _verifier;\n emit VerifierUpdated(_verifier);\n }\n\n function setFeeToAddress(address _feeToAddress) external onlyOwner {\n require(\n _feeToAddress != address(0),\n \"fee received address can not be zero\"\n );\n feeToAddress = _feeToAddress;\n emit FeeToAddressUpdated(_feeToAddress);\n }\n}\n"
},
"contracts/utils/TimeChecker.sol": { "contracts/utils/TimeChecker.sol": {
"content": "// SPDX-License-Identifier: MIT\npragma solidity 0.8.19;\nimport {Ownable} from \"@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(uint256 _duration) {\n duration = _duration;\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 {Ownable} from \"@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(uint256 _duration) {\n duration = _duration;\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"
} }

View File

@ -65,6 +65,24 @@ const config: HardhatUserConfig = {
accounts: process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [], accounts: process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [],
}, },
}, },
sourcify: {
enabled: false,
},
etherscan: {
apiKey: {
bsc_test: "BUWD4T1ENMK9JUTNVQD4YBDMNRNINEWSUN"
},
customChains: [
{
network: "bsc_test",
chainId: 97,
urls: {
apiURL: "https://api-testnet.bscscan.com/api",
browserURL: "https://testnet.bscscan.com"
}
}
]
}
}; };
export default config; export default config;

View File

@ -16,5 +16,11 @@
"type": "logic", "type": "logic",
"json": "assets/contracts/GameItemMall.json", "json": "assets/contracts/GameItemMall.json",
"address": "0xaE08adb5278B107D2501e7c61907e41FEf3887D7" "address": "0xaE08adb5278B107D2501e7c61907e41FEf3887D7"
},
{
"name": "TokenClaim",
"type": "logic",
"json": "assets/contracts/TokenClaim.json",
"address": "0xc058411B15E544291765F15B13c88582b7bceaD0"
} }
] ]

View File

@ -16,8 +16,11 @@
"deploy:nftlock:main": "hardhat deploy --tags NFTLockMain --network sepolia_test --reset", "deploy:nftlock:main": "hardhat deploy --tags NFTLockMain --network sepolia_test --reset",
"deploy:testtoken": "hardhat deploy --tags TestToken --network imtbl_test --reset", "deploy:testtoken": "hardhat deploy --tags TestToken --network imtbl_test --reset",
"deploy:airdrop": "hardhat deploy --tags AirdropToken --network imtbl_test --reset", "deploy:airdrop": "hardhat deploy --tags AirdropToken --network imtbl_test --reset",
"deploy:tokenclaim": "hardhat deploy --tags TokenClaim --network bsc_test --reset",
"deploy:gameitemmall": "hardhat deploy --tags GameItemMall --network imtbl_test --reset", "deploy:gameitemmall": "hardhat deploy --tags GameItemMall --network imtbl_test --reset",
"solhint": "solhint --config ./.solhint.json" "solhint": "solhint --config ./.solhint.json",
"show_verify_list": "npx hardhat verify --list-networks",
"verify_sample": "npx hardhat verify --network bsc_test --constructor-args ./verify/tokenclaim.js 0xee0044BF2ACEf7C3D7f6781d8f5DC4d2Dd1CE64c"
}, },
"author": "", "author": "",
"license": "ISC", "license": "ISC",

105
test/testTokenClaim.ts Normal file
View File

@ -0,0 +1,105 @@
import { expect } from 'chai'
import hre from "hardhat";
import {
getBytes,
solidityPackedKeccak256,
} from 'ethers'
import {
loadFixture,
} from "@nomicfoundation/hardhat-toolbox/network-helpers";
describe('TokenClaim', function() {
async function deployOneContract() {
const [owner, otherAccount] = await hre.ethers.getSigners();
const verifier = owner.address;
const OperatorAllowlist = await hre.ethers.getContractFactory("OperatorAllowlist");
const operatorAllowlist = await OperatorAllowlist.deploy(owner.address);
const 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);
const amount = '10000000000000000000000';
await ft.mint(owner.address, amount);
const TokenClaim = await hre.ethers.getContractFactory("TokenClaim");
const claimer = await TokenClaim.deploy( verifier, ft.target, verifier, 3600);
// @ts-ignore
await ft.connect(owner).approve(claimer.target, amount);
const chainId = hre.network.config.chainId
await operatorAllowlist.grantRegistrarRole(owner.address)
await operatorAllowlist.addAddressToAllowlist([claimer.target])
return { claimer, owner, otherAccount, verifier, ft, chainId };
}
describe("Deployment", function () {
it('should deploy TokenClaim with the correct verifier', async function() {
const { claimer, verifier } = await loadFixture(deployOneContract);
expect(await claimer.verifier()).to.equal(verifier);
});
it('should deploy TokenClaim with the correct FT address', async function() {
const { claimer, ft, owner } = await loadFixture(deployOneContract);
expect(await claimer.erc20Wallets(ft.target)).to.equal(owner.address);
});
})
describe("Claim", function () {
it('should claim token success', async function() {
const { claimer, ft, owner, chainId, otherAccount } = await loadFixture(deployOneContract);
const amount = 100;
// @ts-ignore
// await ft.connect(owner).approve(claimer.target, '10000000000000000000000');
const nonce = (Math.random() * 1000) | 0;
const now = (Date.now() / 1000) | 0;
let localMsgHash = solidityPackedKeccak256(["address","address", "address", "uint256", "address", "uint256", "uint256", "uint256"],
[otherAccount.address, otherAccount.address, ft.target, amount, claimer.target, chainId, now, nonce]);
const signature = await owner.signMessage(getBytes(localMsgHash));
// @ts-ignore
await expect(claimer.connect(otherAccount).claim(otherAccount.address, ft.target, amount, now, nonce, signature))
.to.emit(claimer, "EventTokenClaimed")
.withArgs(otherAccount.address, ft.target, otherAccount.address, amount, nonce);
expect(await ft.balanceOf(otherAccount.address)).to.equal(amount);
});
it('should revert claim token for signature used', async function() {
const { claimer, ft, owner, chainId, otherAccount } = await loadFixture(deployOneContract);
const amount = 100;
const nonce = (Math.random() * 1000) | 0;
const now = (Date.now() / 1000) | 0;
let localMsgHash = solidityPackedKeccak256(["address","address", "address", "uint256", "address", "uint256", "uint256", "uint256"],
[otherAccount.address, otherAccount.address, ft.target, amount, claimer.target, chainId, now, nonce]);
const signature = await owner.signMessage(getBytes(localMsgHash));
//@ts-ignore
await claimer.connect(otherAccount).claim(otherAccount.address, ft.target, amount, now, nonce, signature)
// @ts-ignore
await expect(claimer.connect(otherAccount).claim(otherAccount.address, ft.target, amount, now, nonce, signature)).to.be.revertedWith("signature used. please send another transaction with new signature");
});
it('should revert claim token for timeout', async function() {
const { claimer, ft, owner, chainId, otherAccount } = await loadFixture(deployOneContract);
const amount = 100;
// @ts-ignore
// await ft.connect(owner).approve(claimer.target, '10000000000000000000000');
const nonce = (Math.random() * 1000) | 0;
const now = (Date.now() / 1000 - 3601) | 0;
let localMsgHash = solidityPackedKeccak256(["address","address", "address", "uint256", "address", "uint256", "uint256", "uint256"],
[otherAccount.address, otherAccount.address, ft.target, amount, claimer.target, chainId, now, nonce]);
const signature = await owner.signMessage(getBytes(localMsgHash));
// @ts-ignore
await expect(claimer.connect(otherAccount).claim(otherAccount.address, ft.target, amount, now, nonce, signature)).to.be.revertedWith("expired, please send another transaction with new signature");
});
it('should revert claim token for token not support', async function() {
const { claimer, ft, owner, chainId, otherAccount } = await loadFixture(deployOneContract);
await claimer.updateERC20Wallet(ft.target, '0x0000000000000000000000000000000000000000');
const amount = 100;
const nonce = (Math.random() * 1000) | 0;
const now = (Date.now() / 1000) | 0;
let localMsgHash = solidityPackedKeccak256(["address","address", "address", "uint256", "address", "uint256", "uint256", "uint256"],
[otherAccount.address, otherAccount.address, ft.target, amount, claimer.target, chainId, now, nonce]);
const signature = await owner.signMessage(getBytes(localMsgHash));
// @ts-ignore
await expect(claimer.connect(otherAccount).claim(otherAccount.address, ft.target, amount, now, nonce, signature)).to.be.revertedWith("TokenClaimer: token is not supported");
});
})
})

6
verify/tokenclaim.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = [
'0x50A8e60041A206AcaA5F844a1104896224be6F39',
'0x8f34a7b59841bc87f7d80f9858490cc1412d50fb',
'0x50A8e60041A206AcaA5F844a1104896224be6F39',
3600
];