├── .gitignore ├── foundry.toml ├── .gitmodules ├── src ├── IMintableERC721.sol ├── CappedHelper.sol ├── mocks │ ├── SimpleNFT.sol │ └── CappedNFT.sol ├── CappedMinter.sol └── SimpleMinter.sol ├── .github └── workflows │ └── test.yml ├── test ├── SimpleMinter.t.sol └── CappedMinter.t.sol └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | cache/ 2 | out/ 3 | 4 | # Personal packages for LSP 5 | package.json 6 | package-lock.json 7 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [default] 2 | src = 'src' 3 | out = 'out' 4 | libs = ['lib', 'node_modules'] 5 | 6 | # See more config options https://github.com/foundry-rs/foundry/tree/master/config 7 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | [submodule "lib/openzeppelin-contracts"] 5 | path = lib/openzeppelin-contracts 6 | url = https://github.com/OpenZeppelin/openzeppelin-contracts 7 | -------------------------------------------------------------------------------- /src/IMintableERC721.sol: -------------------------------------------------------------------------------- 1 | // @title Interface based on our target contract 2 | interface IMintableERC721 { 3 | function mint(uint numberOfTokens) external payable; 4 | function transferFrom(address from, address to, uint256 tokenId) external; 5 | function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256); 6 | } 7 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: workflow_dispatch 4 | 5 | env: 6 | FOUNDRY_PROFILE: ci 7 | 8 | jobs: 9 | check: 10 | strategy: 11 | fail-fast: true 12 | 13 | name: Foundry project 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | with: 18 | submodules: recursive 19 | 20 | - name: Install Foundry 21 | uses: foundry-rs/foundry-toolchain@v1 22 | with: 23 | version: nightly 24 | 25 | - name: Run Forge build 26 | run: | 27 | forge --version 28 | forge build --sizes 29 | id: build 30 | 31 | - name: Run Forge tests 32 | run: | 33 | forge test -vvv 34 | id: test 35 | -------------------------------------------------------------------------------- /src/CappedHelper.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "openzeppelin-contracts/contracts/token/ERC721/utils/ERC721Holder.sol"; 5 | import "forge-std/console.sol"; 6 | import "./IMintableERC721.sol"; 7 | 8 | /// @title NFT mass minter 9 | /// @author 0xMouseLess 10 | /// @notice This contract is used to mint NFTs in batches 11 | contract CappedHelper is ERC721Holder { 12 | 13 | /// @dev NFT mint price 14 | uint128 immutable PRICE_PER_NFT = 0.05 ether; 15 | 16 | /// @dev Max mints per call 17 | uint256 immutable MAX_MINT_PER_ADDRESS = 5; 18 | 19 | /// @notice All minting logic happens here 20 | constructor(IMintableERC721 target) payable { 21 | target.mint{value: PRICE_PER_NFT*MAX_MINT_PER_ADDRESS}(MAX_MINT_PER_ADDRESS); 22 | 23 | // NOTE: THIS IS NOT EFFICIENT, JUST FOR DEMO PURPOSES 24 | // **CAN BE REPLACED WITH A MORE EFFICIENT SOLUTION** 25 | 26 | uint256 counter = MAX_MINT_PER_ADDRESS; 27 | 28 | // Transfer nfts out one by one 29 | while(counter > 0) { 30 | uint tokenId = target.tokenOfOwnerByIndex(address(this), 0); 31 | target.transferFrom(address(this), msg.sender, tokenId); 32 | counter--; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/mocks/SimpleNFT.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "openzeppelin-contracts/contracts/token/ERC721/ERC721.sol"; 5 | import "openzeppelin-contracts/contracts/access/Ownable.sol"; 6 | import "openzeppelin-contracts/contracts/token/ERC721/extensions/ERC721Enumerable.sol"; 7 | 8 | contract SimpleNFT is ERC721, ERC721Enumerable, Ownable { 9 | bool public saleIsActive = false; 10 | 11 | uint256 public constant MAX_SUPPLY = 10000; 12 | uint256 public constant MAX_PUBLIC_MINT = 5; 13 | uint256 public constant PRICE_PER_TOKEN = 0.05 ether; 14 | 15 | constructor() ERC721("SimpleNFT", "SimpleNFT") { 16 | } 17 | 18 | function _beforeTokenTransfer(address from, address to, uint256 tokenId) internal override(ERC721, ERC721Enumerable) { 19 | super._beforeTokenTransfer(from, to, tokenId); 20 | } 21 | 22 | function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721, ERC721Enumerable) returns (bool) { 23 | return super.supportsInterface(interfaceId); 24 | } 25 | 26 | function setSaleState(bool newState) public onlyOwner { 27 | saleIsActive = newState; 28 | } 29 | 30 | function mint(uint numberOfTokens) public payable { 31 | uint256 ts = totalSupply(); 32 | require(saleIsActive, "Sale must be active to mint tokens"); 33 | require(numberOfTokens <= MAX_PUBLIC_MINT, "Exceeded max token purchase"); 34 | require(ts + numberOfTokens <= MAX_SUPPLY, "Purchase would exceed max tokens"); 35 | require(PRICE_PER_TOKEN * numberOfTokens <= msg.value, "Ether value sent is not correct"); 36 | 37 | for (uint256 i = 0; i < numberOfTokens; i++) { 38 | _safeMint(msg.sender, ts + i); 39 | } 40 | } 41 | 42 | function withdraw() public onlyOwner { 43 | uint balance = address(this).balance; 44 | payable(msg.sender).transfer(balance); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/CappedMinter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "openzeppelin-contracts/contracts/access/Ownable.sol"; 5 | import "openzeppelin-contracts/contracts/token/ERC721/utils/ERC721Holder.sol"; 6 | import "./IMintableERC721.sol"; 7 | import "./CappedHelper.sol"; 8 | 9 | /// @title NFT mass minter 10 | /// @author 0xMouseLess 11 | /// @notice Exploiting NFT drops that don't track number of mints per address 12 | contract CappedMinter is Ownable, ERC721Holder { 13 | 14 | /// @dev NFT mint address 15 | IMintableERC721 targetNFT; 16 | 17 | /// @dev NFT mint price 18 | uint128 immutable PRICE_PER_NFT = 0.05 ether; 19 | 20 | /// @dev Max mints per call 21 | uint256 immutable MAX_MINT_PER_ADDRESS = 5; 22 | 23 | /// @notice Setting up contract 24 | /// @param _targetAddress Address of the NFT we want to mint 25 | constructor(address _targetAddress) Ownable() { 26 | targetNFT = IMintableERC721(_targetAddress); 27 | } 28 | 29 | // @notice Repeatedly calls the mint function from the Doodles contract 30 | // @param numMints The number of times we want to call the mint function 31 | function massMint(uint numMints) external payable onlyOwner { 32 | for(uint i = 0; i < numMints; i++) { 33 | new CappedHelper{value: PRICE_PER_NFT*MAX_MINT_PER_ADDRESS}(targetNFT); 34 | } 35 | // Return if there are any overflow 36 | payable(owner()).transfer(address(this).balance); 37 | } 38 | 39 | // @notice Method to transfer minted NFTs to contract owner 40 | // @param _tokenIds The IDs of the tokens we want to withdraw 41 | // @dev This function saves gas and should be called when minting is over and gas is low 42 | function withdraw(uint[] calldata _tokenIds) external onlyOwner { 43 | for(uint i=0; i<_tokenIds.length; i++) { 44 | targetNFT.transferFrom(address(this), owner(), _tokenIds[i]); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/mocks/CappedNFT.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "openzeppelin-contracts/contracts/token/ERC721/ERC721.sol"; 5 | import "openzeppelin-contracts/contracts/access/Ownable.sol"; 6 | import "openzeppelin-contracts/contracts/token/ERC721/extensions/ERC721Enumerable.sol"; 7 | 8 | contract CappedNFT is ERC721, ERC721Enumerable, Ownable { 9 | bool public saleIsActive = false; 10 | 11 | uint256 public constant MAX_SUPPLY = 10000; 12 | uint256 public constant MAX_PUBLIC_MINT = 5; 13 | uint256 public constant PRICE_PER_TOKEN = 0.05 ether; 14 | 15 | mapping(address => uint256) public minted; 16 | 17 | constructor() ERC721("CappedNFT", "CappedNFT") { 18 | } 19 | 20 | function _beforeTokenTransfer(address from, address to, uint256 tokenId) internal override(ERC721, ERC721Enumerable) { 21 | super._beforeTokenTransfer(from, to, tokenId); 22 | } 23 | 24 | function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721, ERC721Enumerable) returns (bool) { 25 | return super.supportsInterface(interfaceId); 26 | } 27 | 28 | function setSaleState(bool newState) public onlyOwner { 29 | saleIsActive = newState; 30 | } 31 | 32 | function mint(uint numberOfTokens) public payable { 33 | uint256 ts = totalSupply(); 34 | require(saleIsActive, "Sale must be active to mint tokens"); 35 | require(minted[msg.sender] + numberOfTokens <= MAX_PUBLIC_MINT, "Exceeded account mint limit"); 36 | require(ts + numberOfTokens <= MAX_SUPPLY, "Purchase would exceed max tokens"); 37 | require(PRICE_PER_TOKEN * numberOfTokens <= msg.value, "Ether value sent is not correct"); 38 | 39 | minted[msg.sender] += numberOfTokens; 40 | for (uint256 i = 0; i < numberOfTokens; i++) { 41 | _safeMint(msg.sender, ts + i); 42 | } 43 | } 44 | 45 | function withdraw() public onlyOwner { 46 | uint balance = address(this).balance; 47 | payable(msg.sender).transfer(balance); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/SimpleMinter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "openzeppelin-contracts/contracts/access/Ownable.sol"; 5 | import "openzeppelin-contracts/contracts/token/ERC721/utils/ERC721Holder.sol"; 6 | import "forge-std/console.sol"; 7 | import "./IMintableERC721.sol"; 8 | 9 | /// @title NFT mass minter 10 | /// @author 0xMouseLess 11 | /// @notice Exploiting NFT drops that don't track number of mints per address 12 | contract SimpleMinter is Ownable, ERC721Holder { 13 | 14 | /// @notice NFT mint address 15 | IMintableERC721 targetNFT; 16 | 17 | /// @dev NFT mint price 18 | uint128 immutable PRICE_PER_NFT = 0.05 ether; 19 | 20 | /// @dev Max mints per call 21 | uint256 immutable MAX_MINT_PER_ADDRESS = 5; 22 | 23 | /// @notice Setting up contract 24 | /// @param _targetAddress Address of the NFT we want to mint 25 | constructor(address _targetAddress) Ownable() { 26 | targetNFT = IMintableERC721(_targetAddress); 27 | } 28 | 29 | // @notice Repeatedly calls the mint function from the Doodles contract 30 | // @param numMints The number of times we want to call the mint function 31 | function massMint(uint numMints) external payable onlyOwner { 32 | require(numMints % MAX_MINT_PER_ADDRESS == 0, "numMints must be a multiple of MAX_MINT_PER_ADDRESS"); 33 | uint mintedSoFar = 0; 34 | uint costPerMint = PRICE_PER_NFT*MAX_MINT_PER_ADDRESS; 35 | while(mintedSoFar < numMints) { 36 | targetNFT.mint{value: costPerMint}(MAX_MINT_PER_ADDRESS); 37 | mintedSoFar += MAX_MINT_PER_ADDRESS; 38 | } 39 | // Return if there are any overflow 40 | payable(owner()).transfer(address(this).balance); 41 | } 42 | 43 | // @notice Method to transfer minted NFTs to contract owner 44 | // @param _tokenIds The IDs of the tokens we want to withdraw 45 | // @dev This function saves gas and should be called when minting is over and gas is low 46 | function withdraw(uint[] calldata _tokenIds) external onlyOwner { 47 | for(uint i=0; i<_tokenIds.length; i++) { 48 | targetNFT.transferFrom(address(this), owner(), _tokenIds[i]); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /test/SimpleMinter.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Test.sol"; 5 | import "forge-std/Vm.sol"; 6 | import "../src/SimpleMinter.sol"; 7 | import "../src/mocks/SimpleNFT.sol"; 8 | 9 | /// @title Test Simple Minter contract 10 | /// @author 0xMouseLess 11 | contract SimpleMinterTest is Test { 12 | 13 | /// @notice Our mass minting contract 14 | SimpleMinter simpleMinter; 15 | 16 | /// @notice The nft we are minting 17 | SimpleNFT simpleNFT; 18 | 19 | /// @notice Mock EOA 20 | address minterEoa = 0xc0FFee0000000000000000000000000000000000; 21 | /// @notice NFT Contract Owner 22 | address nftOwner = 0x00000000003b3cc22aF3aE1EAc0440BcEe416B40; 23 | 24 | /// @notice Setup tests 25 | function setUp() public { 26 | // Deploy mock nft contract from mock owner 27 | vm.prank(nftOwner); 28 | simpleNFT = new SimpleNFT(); 29 | // Setup all calls to happen from mock eoa 30 | startHoax(minterEoa, 100 ether); 31 | // Setup minting contract 32 | simpleMinter = new SimpleMinter(address(simpleNFT)); 33 | } 34 | 35 | /// @notice Test massMint function when minting is live 36 | function testMintActive() public { 37 | // Enforce that the sale is active 38 | if(!simpleNFT.saleIsActive()) { 39 | changePrank(nftOwner); 40 | simpleNFT.setSaleState(true); 41 | } 42 | changePrank(minterEoa); 43 | // Number of nfts that we will mint 44 | uint numToMint = 30; 45 | // Price per nft mint 46 | uint256 pricePerNft = simpleNFT.PRICE_PER_TOKEN(); 47 | 48 | // Testing mint function 49 | simpleMinter.massMint{value: pricePerNft*numToMint}(numToMint); 50 | assertEq(numToMint, simpleNFT.balanceOf(address(simpleMinter)), "Unable to mass mint NFTs"); 51 | 52 | // Calculating ids of the nfts we minted 53 | uint256[] memory mintIds = new uint256[](numToMint); 54 | for (uint256 i; i < numToMint; ++i) { 55 | mintIds[i] = simpleNFT.tokenOfOwnerByIndex(address(simpleMinter), i); 56 | } 57 | 58 | // Testing withdraw function to **withdraw all** 59 | simpleMinter.withdraw(mintIds); 60 | assertEq(0, simpleNFT.balanceOf(address(simpleMinter)), "Minter contract is still holding NFTs"); 61 | assertEq(numToMint, simpleNFT.balanceOf(minterEoa), "EOA did not receive all NFTs from Minter contract"); 62 | } 63 | 64 | /// @notice Test massMint function when minting is not live 65 | function testMintNotActiveRevert() public { 66 | // Enforce that the sale is not active 67 | if(simpleNFT.saleIsActive()) { 68 | changePrank(nftOwner); 69 | simpleNFT.setSaleState(false); 70 | } 71 | changePrank(minterEoa); 72 | // Number of nfts that we will mint 73 | uint numToMint = 30; 74 | // Price per nft mint 75 | uint256 pricePerNft = simpleNFT.PRICE_PER_TOKEN(); 76 | 77 | // Test if next function call results in a revert 78 | vm.expectRevert(bytes("Sale must be active to mint tokens")); 79 | 80 | // Making low level call to catch transaction status (can also be done with normal call) 81 | bytes memory callData = abi.encodeWithSignature("massMint(uint256)", numToMint); 82 | (bool status, ) = address(simpleMinter).call{value: pricePerNft*numToMint}(callData); 83 | 84 | assertTrue(status, "expectedRevert: call did not revert"); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /test/CappedMinter.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-Licenje-Identifier: UNLICENSED 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Test.sol"; 5 | import "forge-std/Vm.sol"; 6 | import "forge-std/console.sol"; 7 | import "../src/CappedMinter.sol"; 8 | import "../src/CappedHelper.sol"; 9 | import "../src/mocks/CappedNFT.sol"; 10 | import "../src/IMintableERC721.sol"; 11 | 12 | /// @title Test Simple Minter contract 13 | /// @author 0xMouseLess 14 | contract CappedMinterTest is Test { 15 | 16 | /// @dev The nft we are minting 17 | CappedNFT cappedNFT; 18 | 19 | /// @dev Mass minter contract 20 | CappedMinter cappedMinter; 21 | 22 | /// @dev Mock EOA 23 | address minterEoa = 0xc0FFee0000000000000000000000000000000000; 24 | /// @dev NFT Contract Owner 25 | address nftOwner = 0x00000000003b3cc22aF3aE1EAc0440BcEe416B40; 26 | 27 | /// @dev NFT mint price 28 | uint128 immutable PRICE_PER_NFT = 0.05 ether; 29 | 30 | /// @dev Max mints per call 31 | uint256 immutable MAX_MINT_PER_ADDRESS = 5; 32 | 33 | /// @notice Setup tests 34 | function setUp() public { 35 | // Deploy mock nft contract from mock owner 36 | vm.prank(nftOwner); 37 | cappedNFT = new CappedNFT(); 38 | // Setup all calls to happen from mock eoa 39 | startHoax(minterEoa, 100 ether); 40 | // Setup minting contract 41 | cappedMinter = new CappedMinter(address(cappedNFT)); 42 | } 43 | 44 | /// @notice Test massMint function when minting is not live 45 | function testMintNotActiveRevert() public { 46 | // Enforce that the sale is active 47 | if(cappedNFT.saleIsActive()) { 48 | changePrank(nftOwner); 49 | cappedNFT.setSaleState(false); 50 | } 51 | changePrank(minterEoa); 52 | 53 | uint helperInstances = 10; 54 | uint totalMinted = helperInstances * MAX_MINT_PER_ADDRESS; 55 | 56 | // Test if next function call results in a revert 57 | vm.expectRevert(bytes("Sale must be active to mint tokens")); 58 | cappedMinter.massMint{value: totalMinted*PRICE_PER_NFT}(helperInstances); 59 | } 60 | 61 | /// @notice Test main massMint function when minting is live 62 | function testMintActive() public { 63 | // Enforce that the sale is active 64 | if(!cappedNFT.saleIsActive()) { 65 | changePrank(nftOwner); 66 | cappedNFT.setSaleState(true); 67 | } 68 | changePrank(minterEoa); 69 | 70 | uint helperInstances = 10; 71 | uint totalMinted = helperInstances * MAX_MINT_PER_ADDRESS; 72 | 73 | // Testing mint function 74 | cappedMinter.massMint{value: totalMinted*PRICE_PER_NFT}(helperInstances); 75 | assertEq(totalMinted, cappedNFT.balanceOf(address(cappedMinter)), "Unable to mass mint NFTs"); 76 | 77 | // Calculating ids of the nfts we minted 78 | uint256[] memory mintIds = new uint256[](totalMinted); 79 | for (uint256 i; i < totalMinted; ++i) { 80 | mintIds[i] = cappedNFT.tokenOfOwnerByIndex(address(cappedMinter), i); 81 | } 82 | 83 | // Testing withdraw function to **withdraw all** 84 | cappedMinter.withdraw(mintIds); 85 | assertEq(0, cappedNFT.balanceOf(address(cappedMinter)), "Minter contract is still holding NFTs"); 86 | assertEq(totalMinted, cappedNFT.balanceOf(minterEoa), "EOA did not receive all NFTs from Minter contract"); 87 | } 88 | 89 | //////////////////////// 90 | /// HELPER FUNCTIONS /// 91 | //////////////////////// 92 | 93 | /// @notice Test minting more than account limit 94 | function testHelperExceedMintLimitRevert() public { 95 | // Enforce that the sale is active 96 | if(!cappedNFT.saleIsActive()) { 97 | changePrank(nftOwner); 98 | cappedNFT.setSaleState(true); 99 | } 100 | changePrank(minterEoa); 101 | 102 | // Mint from our EOA for this test 103 | cappedNFT.mint{value: PRICE_PER_NFT*MAX_MINT_PER_ADDRESS}(MAX_MINT_PER_ADDRESS); 104 | 105 | // Expect a revert as we are minting from the same addr twice 106 | vm.expectRevert(bytes("Exceeded account mint limit")); 107 | cappedNFT.mint{value: PRICE_PER_NFT*MAX_MINT_PER_ADDRESS}(MAX_MINT_PER_ADDRESS); 108 | } 109 | 110 | /// @notice Test to check if the helper contract is able to mint 111 | function testHelperMintingActive() public { 112 | // Enforce that the sale is active 113 | if(!cappedNFT.saleIsActive()) { 114 | changePrank(nftOwner); 115 | cappedNFT.setSaleState(true); 116 | } 117 | changePrank(minterEoa); 118 | CappedHelper cappedHelper = new CappedHelper{value: PRICE_PER_NFT*MAX_MINT_PER_ADDRESS}(IMintableERC721(address(cappedNFT))); 119 | assertEq(MAX_MINT_PER_ADDRESS, cappedNFT.balanceOf(minterEoa), "msg.sender did not receive all nfts from minter contract"); 120 | assertEq(0, cappedNFT.balanceOf(address(cappedHelper)), "minter contract still holds some nfts"); 121 | } 122 | 123 | /// @notice Test when minting is not live 124 | function testHelperMintingNotActiveRevert() public { 125 | // Enforce that the sale is not active 126 | if(cappedNFT.saleIsActive()) { 127 | changePrank(nftOwner); 128 | cappedNFT.setSaleState(false); 129 | } 130 | changePrank(minterEoa); 131 | 132 | // Test if next function call results in a revert 133 | vm.expectRevert(bytes("Sale must be active to mint tokens")); 134 | new CappedHelper{value: PRICE_PER_NFT*MAX_MINT_PER_ADDRESS}(IMintableERC721(address(cappedNFT))); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Using Smart Contracts To Mass Mint NFT Drops 2 | 3 | Practical examples on how to **mass mint NFT drops in a single transaction** using smart contracts. 4 | 5 | Drops vary significantly per NFT collection, this repo explores 3 differnt cases and how to approach them. 6 | 7 | #### The 3 mint types that this repo explores are: 8 | - NFT that perform no mint checks 9 | - NFT that caps the mint amount per address 10 | - NFT that does not allow smart contracts to mint 11 | 12 | ## Usage 13 | 14 | Install this foundry project 15 | ```shell 16 | git clone https://github.com/mouseless-eth/NFT-Mass-Minting 17 | cd NFT-Mass-Minting 18 | forge install 19 | ``` 20 | 21 | Run the tests 22 | ```shell 23 | forge test 24 | ``` 25 | 26 | #### Notice :shipit: 27 | This repo is made only for **educational purposes**. The contracts included have been rewritten to only contain the **bare minimum needed to perfom a mass mint**. All gas saving alpha has been stripped away to improve the contracts readibility. If you want to take these to production, I highly suggest investing time into gas golfing and fine tuning your contracts. 28 | 29 | ## Mint Type One: No Checks 30 | These type of contract's have no sanity checks and only allow users to mint **up to** a certain number of NFTs per transation. We can easily create a custom contract loops and repeatedly call the NFT's mint function. 31 | 32 | Example of a NFT mint function that does not have any sanity checks 33 | ```solidity 34 | function mint(uint numberOfTokens) public payable { 35 | uint256 ts = totalSupply(); 36 | require(saleIsActive, "Sale must be active to mint tokens"); 37 | require(numberOfTokens <= MAX_PUBLIC_MINT, "Exceeded max token purchase"); 38 | require(ts + numberOfTokens <= MAX_SUPPLY, "Purchase would exceed max tokens"); 39 | require(PRICE_PER_TOKEN * numberOfTokens <= msg.value, "Ether value sent is not correct"); 40 | 41 | for (uint256 i = 0; i < numberOfTokens; i++) { 42 | _safeMint(msg.sender, ts + i); 43 | } 44 | ``` 45 | > [src/SimpleMinter.sol](./src/SimpleMinter.sol) contains an example of a mass minter for this type of drops 46 | 47 | ## Mint Type Two: Capped Mint Amt Per Address 48 | These type of contracts are more sophisticated as they track the amount minted by an address using a `mapping(address=>uint256)` which results in a **capped mint amout per address**. We can still mass mint these types of drops by using a factory design pattern to deploy multiple new contracts (new address) that do the minting. 49 | 50 | Example of a NFT mint function that caps the mint amount per address (take note of the `minted` mapping) 51 | ```solidity 52 | function mint(uint numberOfTokens) public payable { 53 | uint256 ts = totalSupply(); 54 | require(saleIsActive, "Sale must be active to mint tokens"); 55 | require(minted[msg.sender] + numberOfTokens <= MAX_PUBLIC_MINT, "Exceeded account mint limit"); 56 | require(ts + numberOfTokens <= MAX_SUPPLY, "Purchase would exceed max tokens"); 57 | require(PRICE_PER_TOKEN * numberOfTokens <= msg.value, "Ether value sent is not correct"); 58 | 59 | minted[msg.sender] += numberOfTokens; 60 | for (uint256 i = 0; i < numberOfTokens; i++) { 61 | _safeMint(msg.sender, ts + i); 62 | } 63 | } 64 | ``` 65 | > [src/CappedMinter.sol](./src/CappedMinter.sol) contains an example of a mass minter for this type of drop 66 | 67 | ###### Relevant contract files 68 | ``` 69 | src 70 | ├── CappedMinter.sol // Factory contract to create n 'CappedHelper' instances 71 | ├── CappedHelper.sol // Contract that handles minting 72 | ├── IMintableERC721.sol 73 | ├── mocks 74 | │   ├── CappedNFT.sol 75 | test 76 | ├── CappedMinter.t.sol 77 | ``` 78 | 79 | ## Mint Type Three: Minter Cannot Be A Smart Contract 80 | These type of mints make sure that only [EOA](https://ethdocs.org/en/latest/contracts-and-transactions/account-types-gas-and-transactions.html) are allowed to call the mint function. This is enforced through the following check `require(msg.sender == tx.origin)`. Because of this we cannot mass mint using a custom smart contract, but there is a work around. 81 | 82 | Example of a NFT mint function that only allows EOAs to mint 83 | ```solidity 84 | function mint(uint numberOfTokens) public payable { 85 | uint256 ts = totalSupply(); 86 | require(saleIsActive, "Sale must be active to mint tokens"); 87 | require(msg.sender == tx.origin, "Contracts are not allowed to mint"); // take note of this line 88 | require(numberOfTokens <= MAX_PUBLIC_MINT, "Exceeded max token purchase"); 89 | require(ts + numberOfTokens <= MAX_SUPPLY, "Purchase would exceed max tokens"); 90 | require(PRICE_PER_TOKEN * numberOfTokens <= msg.value, "Ether value sent is not correct"); 91 | 92 | for (uint256 i = 0; i < numberOfTokens; i++) { 93 | _safeMint(msg.sender, ts + i); 94 | } 95 | } 96 | ``` 97 | These type of drops need to be executed using a scripting languge such as **javascript**, **rust**, **python3** 98 | 99 | #### Setup before the mint 100 | 1) Generate private keys to use 101 | 2) Seed all generated accounts with enough to pay for the total mint price 102 | 3) Each account creates a signed transation to mint the max amount from the NFT contract, save these txs in a data structure (don't broadcast the txs) 103 | 4) Sniff mempool to check if the drop is live 104 | 105 | #### Execution when minting is live 106 | 5) Place all signed txs in a [flashbots bundle](https://docs.flashbots.net/flashbots-auction/searchers/advanced/understanding-bundles) 107 | 6) Once the bundle has been mined, programmatically move all minted NFTs to a single address 108 | 109 | This works as each we are not using a smart contract and each `tx.origin` is unique. Using a flashbots bundle ensures all txs are **mined in the same block**. 110 | 111 | 112 | *⚠️ Keeping this implementation closed sourced for now but the steps above outline how to mass mint these type of drops* 113 | 114 | --------------------------------------------------------------------------------