├── .gitignore ├── README.md ├── contracts └── MintID.sol ├── hardhat.config.ts ├── package.json ├── scripts └── deploy.ts ├── test └── MintID.ts ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | 4 | # Hardhat files 5 | /cache 6 | /artifacts 7 | 8 | # TypeChain files 9 | /typechain 10 | /typechain-types 11 | 12 | # solidity-coverage files 13 | /coverage 14 | /coverage.json 15 | 16 | yarn-error.log 17 | /.openzeppelin 18 | .vscode -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NFT Contract Address: 2 | https://etherscan.io/address/0x9236cA1d6e59f8aB672269443e13669D0bD5B353 3 | 4 | # MintID NFT Description: 5 | MintID is a multifunctional asset and super equity pass of Mint Blockchain, designed to explore the NFT possibilities in various application scenarios and provide holders the ongoing value within Mint Blockchain ecosystem. 6 | 7 | # NFT Smart Contract Audit Report: 8 | https://github.com/peckshield/publications/blob/master/audit_reports/PeckShield-Audit-Report-MintID-MintGenesisNFT-v1.0.pdf 9 | -------------------------------------------------------------------------------- /contracts/MintID.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | import "erc721a-upgradeable/contracts/ERC721AUpgradeable.sol"; 5 | import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; 6 | import "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; 7 | import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; 8 | import "@openzeppelin/contracts/interfaces/IERC2981.sol"; 9 | 10 | contract MintID is 11 | ERC721AUpgradeable, 12 | IERC2981, 13 | OwnableUpgradeable, 14 | PausableUpgradeable, 15 | UUPSUpgradeable 16 | { 17 | struct MintConfig { 18 | uint64 price; 19 | uint32 startTime; 20 | uint32 endTime; 21 | } 22 | 23 | MintConfig public mintConfig; 24 | uint256 constant MAX_SUPPLY = 10000; 25 | uint8 constant maxMintPerAddress = 5; 26 | uint256 constant DENO = 1000; 27 | 28 | uint256 public royalty; 29 | address public treasuryAddress; 30 | 31 | string private _baseUri; 32 | 33 | mapping(address => uint256) public publiclist; 34 | 35 | uint256 public stakingState; 36 | 37 | mapping(address => uint256[]) public stakedAddressInfo; 38 | 39 | event TokensStaked(address indexed owner, uint256[] tokenIds); 40 | 41 | error InvalidCaller(); 42 | error MintNotStart(); 43 | error MintFinished(); 44 | error OverLimit(address minter); 45 | error OverMaxLimit(); 46 | error InsufficientBalance(address minter); 47 | error TokenNotMinted(uint256 tokenId); 48 | 49 | /// @custom:oz-upgrades-unsafe-allow constructor 50 | constructor() { 51 | _disableInitializers(); 52 | } 53 | 54 | function initialize( 55 | address _address 56 | ) public initializerERC721A initializer { 57 | __ERC721A_init("MintID", "MintID"); 58 | __UUPSUpgradeable_init(); 59 | __Pausable_init(); 60 | __Ownable_init(_msgSender()); 61 | 62 | royalty = 50; 63 | treasuryAddress = address(_address); 64 | } 65 | 66 | modifier isEOA() { 67 | if (tx.origin != msg.sender) revert InvalidCaller(); 68 | _; 69 | } 70 | 71 | modifier isSufficient() { 72 | if (block.timestamp < mintConfig.startTime) revert MintNotStart(); 73 | if (block.timestamp > mintConfig.endTime) revert MintFinished(); 74 | _; 75 | } 76 | 77 | function minted() public view returns (uint256) { 78 | return _totalMinted(); 79 | } 80 | 81 | function _startTokenId() internal view virtual override returns (uint256) { 82 | return 1; 83 | } 84 | 85 | function mint( 86 | uint8 _quantity 87 | ) external payable isEOA whenNotPaused isSufficient { 88 | address account = _msgSender(); 89 | if (msg.value < mintConfig.price * _quantity) 90 | revert InsufficientBalance(account); 91 | if (_quantity + minted() > MAX_SUPPLY) revert OverMaxLimit(); 92 | if (publiclist[account] + _quantity > maxMintPerAddress) 93 | revert OverLimit(account); 94 | publiclist[account] += _quantity; 95 | _safeMint(account, _quantity); 96 | } 97 | 98 | /** 99 | * @inheritdoc IERC2981 100 | */ 101 | function royaltyInfo( 102 | uint256 tokenId, 103 | uint256 salePrice 104 | ) external view override returns (address, uint256) { 105 | if (!super._exists(tokenId)) revert TokenNotMinted(tokenId); 106 | uint256 royaltyAmount = (salePrice * royalty) / DENO; 107 | return (treasuryAddress, royaltyAmount); 108 | } 109 | 110 | function _baseURI() internal view override returns (string memory) { 111 | return _baseUri; 112 | } 113 | 114 | function setBaseURI(string calldata _uri) external onlyOwner { 115 | _baseUri = _uri; 116 | } 117 | 118 | function setMintConfig( 119 | uint64 _price, 120 | uint32 _startTime, 121 | uint32 _endTime 122 | ) external onlyOwner { 123 | require(_endTime > _startTime, "MP: MUST(end time > Start time)"); 124 | mintConfig = MintConfig(_price, _startTime, _endTime); 125 | } 126 | 127 | function setRoyalty(uint256 _royalty) external onlyOwner { 128 | require( 129 | _royalty <= 100 && _royalty >= 0, 130 | "MP: Royalty can only be between 0 and 10%" 131 | ); 132 | royalty = _royalty; 133 | } 134 | 135 | function setTreasuryAddress(address _addr) external onlyOwner { 136 | require(_addr != address(0x0), "MP: Address not be zero"); 137 | treasuryAddress = _addr; 138 | } 139 | 140 | function withdraw() external onlyOwner { 141 | require( 142 | treasuryAddress != address(0x0), 143 | "MP: Must set withdrawal address" 144 | ); 145 | (bool success, ) = treasuryAddress.call{value: address(this).balance}( 146 | "" 147 | ); 148 | require(success, "MP: Transfer failed"); 149 | } 150 | 151 | function supportsInterface( 152 | bytes4 interfaceId 153 | ) public view override(ERC721AUpgradeable, IERC165) returns (bool) { 154 | return 155 | ERC721AUpgradeable.supportsInterface(interfaceId) || 156 | interfaceId == type(IERC2981).interfaceId; 157 | } 158 | 159 | function _authorizeUpgrade( 160 | address newImplementation 161 | ) internal override onlyOwner {} 162 | 163 | function setStakingState(uint256 _state) external onlyOwner { 164 | stakingState = _state; 165 | } 166 | 167 | function stake(uint256[] calldata tokenIds) external { 168 | require(stakingState == 1, "MP: Staking not open"); 169 | require(tokenIds.length > 0, "MP: Staking zero tokens"); 170 | address owner = _msgSender(); 171 | for (uint256 i = 0; i < tokenIds.length; ) { 172 | transferFrom(owner, address(this), tokenIds[i]); 173 | stakedAddressInfo[owner].push(tokenIds[i]); 174 | unchecked { 175 | ++i; 176 | } 177 | } 178 | emit TokensStaked(owner, tokenIds); 179 | } 180 | 181 | function stakedNum(address staker) external view returns (uint256) { 182 | return stakedAddressInfo[staker].length; 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import { HardhatUserConfig } from "hardhat/config"; 2 | import "@nomicfoundation/hardhat-toolbox"; 3 | import "@openzeppelin/hardhat-upgrades"; 4 | import "dotenv/config"; 5 | 6 | const config: HardhatUserConfig = { 7 | solidity: { 8 | version: "0.8.20", 9 | settings: { 10 | optimizer: { 11 | enabled: true, 12 | runs: 200, 13 | }, 14 | }, 15 | }, 16 | paths: { 17 | sources: "contracts", 18 | }, 19 | defaultNetwork: "localhost", 20 | networks: { 21 | localhost: { 22 | url: "http://127.0.0.1:8545", 23 | accounts: 24 | process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [], 25 | }, 26 | sepolia: { 27 | url: "https://eth-sepolia.g.alchemy.com/v2/1jrDuEnaS36cBF10eYyB8c-YNIaYb02x", 28 | accounts: 29 | process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [], 30 | }, 31 | }, 32 | etherscan: { 33 | apiKey: { 34 | sepolia: process.env.ETHERSCAN_API_KEY as string, 35 | }, 36 | }, 37 | }; 38 | 39 | export default config; 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mint-pass", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "@openzeppelin/contracts": "^5.0.0", 13 | "@openzeppelin/contracts-upgradeable": "^5.0.0", 14 | "@openzeppelin/hardhat-upgrades": "^2.4.1", 15 | "crypto-js": "^4.2.0", 16 | "erc721a-upgradeable": "^4.2.3", 17 | "hardhat": "^2.19.1" 18 | }, 19 | "devDependencies": { 20 | "@nomicfoundation/hardhat-chai-matchers": "^2.0.0", 21 | "@nomicfoundation/hardhat-ethers": "^3.0.5", 22 | "@nomicfoundation/hardhat-network-helpers": "^1.0.0", 23 | "@nomicfoundation/hardhat-toolbox": "^4.0.0", 24 | "@nomicfoundation/hardhat-verify": "^2.0.0", 25 | "@typechain/ethers-v6": "^0.5.0", 26 | "@typechain/hardhat": "^9.0.0", 27 | "@types/chai": "^4.2.0", 28 | "@types/crypto-js": "^4.2.1", 29 | "@types/mocha": ">=9.1.0", 30 | "@types/node": ">=16.0.0", 31 | "chai": "^4.2.0", 32 | "dotenv": "^16.3.1", 33 | "ethers": "^6.4.0", 34 | "hardhat-gas-reporter": "^1.0.8", 35 | "solidity-coverage": "^0.8.0", 36 | "ts-node": ">=8.0.0", 37 | "typechain": "^8.3.0", 38 | "typescript": ">=4.5.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /scripts/deploy.ts: -------------------------------------------------------------------------------- 1 | import { ethers, upgrades } from "hardhat"; 2 | 3 | async function main() { 4 | const V1contract = await ethers.getContractFactory("MintID"); 5 | console.log("Deploying V1contract..."); 6 | const v1contract = await upgrades.deployProxy(V1contract as any, ["0xc782946e205C8C9f1a307544f41eBfaFaDB23De5"], { 7 | initializer: "initialize", 8 | kind: "uups", 9 | }); 10 | await v1contract.waitForDeployment(); 11 | console.log("V1 Contract deployed to:", await v1contract.getAddress()); 12 | } 13 | 14 | // We recommend this pattern to be able to use async/await everywhere 15 | // and properly handle errors. 16 | main().catch((error) => { 17 | console.error(error); 18 | process.exitCode = 1; 19 | }); 20 | -------------------------------------------------------------------------------- /test/MintID.ts: -------------------------------------------------------------------------------- 1 | import { time, loadFixture } from "@nomicfoundation/hardhat-network-helpers"; 2 | import { anyValue } from "@nomicfoundation/hardhat-chai-matchers/withArgs"; 3 | import { expect } from "chai"; 4 | import { ethers, upgrades } from "hardhat"; 5 | 6 | // treasuryAddress 7 | const treasuryAddress = "0x8626f6940E2eb28930eFb4CeF49B2d1F2C9C1199"; 8 | 9 | const day = 60 * 60 * 24; 10 | const publicPrice = ethers.parseEther("0.28"); 11 | 12 | function getNow() { 13 | return Math.floor(Date.now() / 1000); 14 | } 15 | 16 | function generateWallet() { 17 | const wallet = ethers.Wallet.createRandom(ethers.provider); 18 | return wallet; 19 | } 20 | 21 | describe("MintID", () => { 22 | async function deployFixture() { 23 | const V1contract = await ethers.getContractFactory("MintID"); 24 | const [owner, otherAccount] = await ethers.getSigners(); 25 | const v1contract = await upgrades.deployProxy(V1contract as any, [treasuryAddress], { 26 | initializer: "initialize", 27 | kind: "uups", 28 | }); 29 | const contract = await v1contract.waitForDeployment(); 30 | 31 | // generate wallets for test 32 | const publicUser = generateWallet(); 33 | await owner.sendTransaction({ 34 | to: publicUser.address, 35 | value: ethers.parseEther("2"), 36 | }); 37 | const wlUser = generateWallet(); 38 | await owner.sendTransaction({ 39 | to: wlUser.address, 40 | value: ethers.parseEther("2"), 41 | }); 42 | 43 | return { 44 | contract, 45 | treasuryAddress, 46 | owner, 47 | otherAccount, 48 | publicUser, 49 | wlUser, 50 | }; 51 | } 52 | 53 | async function setRightContract(contract: any) { 54 | // set config 55 | const startDate = getNow() - day; 56 | const endDate = getNow() + day; 57 | await contract.setMintConfig(publicPrice, startDate, endDate); 58 | } 59 | 60 | describe("Deployment", () => { 61 | it("Set right treasuryAddress", async () => { 62 | const { contract, treasuryAddress } = await loadFixture(deployFixture); 63 | await expect(await contract.treasuryAddress()).to.equal(treasuryAddress); 64 | }); 65 | 66 | it("Set right owner", async () => { 67 | const { contract, owner } = await loadFixture(deployFixture); 68 | await expect(await contract.owner()).to.equal(owner.address); 69 | }); 70 | }); 71 | 72 | describe("Mint condition", () => { 73 | it("Not start mint", async () => { 74 | const { contract, publicUser } = await loadFixture(deployFixture); 75 | await contract.setMintConfig(publicPrice, getNow() + day, getNow() + 2 * day); 76 | 77 | await expect( 78 | contract.mint(1, { 79 | value: publicPrice, 80 | }) 81 | ).to.be.revertedWithCustomError(contract, "MintNotStart"); 82 | }); 83 | it("Already finish mint", async () => { 84 | const { contract, publicUser } = await loadFixture(deployFixture); 85 | await contract.setMintConfig(publicPrice, getNow() - 2 * day, getNow() - day); 86 | 87 | await expect( 88 | contract.mint(1, { 89 | value: publicPrice, 90 | }) 91 | ).to.be.revertedWithCustomError(contract, "MintFinished"); 92 | }); 93 | }); 94 | 95 | describe("Mint", async () => { 96 | it("Should mint 5 items", async () => { 97 | const { contract, publicUser } = await loadFixture(deployFixture); 98 | await setRightContract(contract); 99 | const contractCaller = contract.connect(publicUser); 100 | await (contractCaller as any).mint(5, { 101 | value: publicPrice * BigInt(5), 102 | }); 103 | await expect(await contract.balanceOf(publicUser.address)).to.equal(5); 104 | await expect(await contract.publiclist(publicUser.address)).to.equal(5); 105 | await expect(await contract.minted()).to.equal(5); 106 | await expect(await ethers.provider.getBalance(await contract.getAddress())).to.equal(publicPrice * BigInt(5)); 107 | }); 108 | it("Should't mint 6 items", async () => { 109 | const { contract, publicUser } = await loadFixture(deployFixture); 110 | await setRightContract(contract); 111 | const contractCaller = contract.connect(publicUser); 112 | await expect( 113 | (contractCaller as any).mint(6, { 114 | value: publicPrice * BigInt(6), 115 | }) 116 | ) 117 | .to.be.revertedWithCustomError(contract, "OverLimit") 118 | .withArgs(publicUser.address); 119 | }); 120 | }); 121 | 122 | describe("Royalty", async () => { 123 | it("Set wrong royalty beacause out of range", async () => { 124 | const { contract, publicUser } = await loadFixture(deployFixture); 125 | await expect(contract.setRoyalty(101)).to.be.revertedWith("MP: Royalty can only be between 0 and 10%"); 126 | }); 127 | it("Set right royalty", async () => { 128 | const { contract } = await loadFixture(deployFixture); 129 | await contract.setRoyalty(100); 130 | await expect(await contract.royalty()).to.be.equal(100); 131 | }); 132 | it("TokenId is not exist when get royalty info", async () => { 133 | const { contract } = await loadFixture(deployFixture); 134 | await expect(contract.royaltyInfo(101, ethers.parseEther("2"))) 135 | .to.be.revertedWithCustomError(contract, "TokenNotMinted") 136 | .withArgs(101); 137 | }); 138 | it("Get right royalty info", async () => { 139 | const { contract, publicUser } = await loadFixture(deployFixture); 140 | await setRightContract(contract); 141 | const contractCaller = contract.connect(publicUser); 142 | await (contractCaller as any).mint(5, { 143 | value: publicPrice * BigInt(5), 144 | }); 145 | await contract.setRoyalty(100); 146 | const value = ethers.parseEther("1"); 147 | const [address, amount] = await contract.royaltyInfo(1, value); 148 | await expect(amount).to.be.equal(value / BigInt(10)); 149 | await expect(address).to.be.equal(treasuryAddress); 150 | }); 151 | }); 152 | 153 | describe("Withdrawl", async () => { 154 | it("Public wallet can not withdraw", async () => { 155 | const { contract, publicUser, owner } = await loadFixture(deployFixture); 156 | await setRightContract(contract); 157 | const contractCaller = contract.connect(publicUser); 158 | await expect((contractCaller as any).withdraw()) 159 | .to.be.revertedWithCustomError(contract, "OwnableUnauthorizedAccount") 160 | .withArgs(publicUser.address); 161 | }); 162 | it("Owner withdraw ETH to treasure address", async () => { 163 | const { contract, publicUser, owner } = await loadFixture(deployFixture); 164 | await setRightContract(contract); 165 | const usedValue = publicPrice * BigInt(5); 166 | const contractCaller = contract.connect(publicUser); 167 | await (contractCaller as any).mint(5, { 168 | value: usedValue, 169 | }); 170 | 171 | const beforeBanlance = await ethers.provider.getBalance(treasuryAddress); 172 | await contract.withdraw(); 173 | const afterBanlance = await ethers.provider.getBalance(treasuryAddress); 174 | await expect(afterBanlance - beforeBanlance).to.be.equal(usedValue); 175 | await expect(await ethers.provider.getBalance(contract.getAddress())).to.be.equal(0); 176 | }); 177 | }); 178 | 179 | describe("Contract Upgrade", async () => { 180 | it("public wallet call upgrade function", async () => { 181 | const { contract, publicUser, owner } = await loadFixture(deployFixture); 182 | const V2Contract = await ethers.getContractFactory("MintID"); 183 | const v2 = await V2Contract.deploy(); 184 | const data = contract.interface.encodeFunctionData("setTreasuryAddress", [owner.address]); 185 | const publicCaller = contract.connect(publicUser); 186 | await expect((publicCaller as any).upgradeToAndCall(await v2.getAddress(), data)) 187 | .to.be.revertedWithCustomError(contract, "OwnableUnauthorizedAccount") 188 | .withArgs(publicUser.address); 189 | }); 190 | it("Upgrade successsfully", async () => { 191 | const { contract, publicUser, owner } = await loadFixture(deployFixture); 192 | const v2Contract = await ethers.getContractFactory("MintID"); 193 | contract.abi; 194 | const upgradeContract = await upgrades.upgradeProxy(contract, v2Contract, { 195 | kind: "uups", 196 | call: { 197 | fn: "setTreasuryAddress", 198 | args: [owner.address], 199 | }, 200 | }); 201 | await expect(await upgradeContract.treasuryAddress()).to.be.equal(owner.address); 202 | }); 203 | }); 204 | 205 | describe("staking", async () => { 206 | it("should pass", async () => { 207 | const { contract, publicUser, wlUser } = await loadFixture(deployFixture); 208 | await setRightContract(contract); 209 | const contractCaller = contract.connect(publicUser); 210 | const contractCallerWL = contract.connect(wlUser); 211 | 212 | await (contractCaller as any).mint(5, { 213 | value: publicPrice * BigInt(5), 214 | }); 215 | 216 | await expect(await contract.stakingState()).to.be.equal(0); 217 | 218 | await expect((contractCaller as any).stake([1])).to.be.revertedWith("MP: Staking not open"); 219 | 220 | await contract.setStakingState(1); 221 | 222 | await expect(await contract.stakingState()).to.be.equal(1); 223 | 224 | await expect((contractCaller as any).stake([])).to.revertedWith("MP: Staking zero tokens"); 225 | 226 | await expect(await (contractCaller as any).stakedNum(publicUser.address)).to.be.equal(0); 227 | 228 | await (contractCaller as any).stake([1]); 229 | 230 | await expect(await contract.stakedAddressInfo(publicUser.address, 0)).to.be.equal(1); 231 | 232 | await expect(await (contractCaller as any).stakedNum(publicUser.address)).to.be.equal(1); 233 | 234 | await (contractCaller as any).approve(wlUser, 2); 235 | 236 | await expect((contractCallerWL as any).stake([2])).to.be.revertedWithCustomError(contract, "TransferFromIncorrectOwner"); 237 | 238 | await (contractCaller as any).stake([2, 4, 5]); 239 | 240 | await expect(await contract.stakedAddressInfo(publicUser.address, 3)).to.be.equal(5); 241 | 242 | await expect(await (contractCaller as any).stakedNum(publicUser.address)).to.be.equal(4); 243 | }); 244 | }); 245 | }); 246 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "resolveJsonModule": true 10 | } 11 | } 12 | --------------------------------------------------------------------------------