Create a fair NFT mint on Ethereum with Solidity and NextJS #2: Smart Contract setup
Papers, please
In the last post, we've setup a request to our back-end as a requirement of the mint process.
Indeed, we fetched the hash, signature and nonce generated from the back-end and we sent them to the smart contract through the buy
method.
Let's see how we're going to process those informations.
Verify the signer identity
First of all let's add a field that contains the address of the transaction signer to our smart contract. As we saw in the previous post, this address is an account that we own and with which we sign the transaction in our NextJS backend.
address private _signerAddress = 0x5260818d61ff27B7d0db3A96310246C041F7191e; // So we can modify the signer address on each drop to enhance security function setSignerAddress(address addr) external onlyOwner { _signerAddress = addr; }
Then we create a function called matchAddresSigner
to check if the address that signed the hash is the same as the one we set in the contract.
function matchAddresSigner(bytes32 hash, bytes memory signature) private view returns(bool) { return _signerAddress == hash.recover(signature); }
Finally, we can use this function as a require in our buy
method.
require(matchAddresSigner(hash, signature), "DIRECT_MINT_DISALLOWED");
Prevent double minting
Now that we verified the signer identity, we can prevent the same mint from being executed twice. We'll use a simple string-to-boolean map to store the used nonces.
mapping(string => bool) private _usedNonces;
Then require the nonce to be unique inside our buy
function.
require(!_usedNonces[nonce], "HASH_USED");
Check hash validity
Now that we've verified the signer identity and the nonce, we can check if the hash is valid. To do so, we'll create a function that recreates the hash from the transaction and compare it to the one we received from the sender.
function hashTransaction(address sender, uint256 qty, string memory nonce) private pure returns(bytes32) { bytes32 hash = keccak256(abi.encodePacked( "\x19Ethereum Signed Message:\n32", keccak256(abi.encodePacked(sender, qty, nonce))) ); return hash; }
Once again, we'll add it to our buy
function as a require.
require(hashTransaction(msg.sender, tokenQuantity, nonce) == hash, "HASH_FAIL");
The final buy
method
Now that we've verified the signer identity, the nonce and the hash, the buy
function is pretty much classic, here's the full function:
function buy(bytes32 hash, bytes memory signature, string memory nonce, uint256 tokenQuantity) external payable { require(saleLive, "SALE_CLOSED"); require(!presaleLive, "ONLY_PRESALE"); // Check if the address signer is the same as the _signerAddress, preventing direct minting. require(matchAddresSigner(hash, signature), "DIRECT_MINT_DISALLOWED"); // This is required to prevent someone from re-using the same signature and hash combination to mint again. require(!_usedNonces[nonce], "HASH_USED"); // Verify if the keccak256 hash matches the hash generated by the hashTransaction function require(hashTransaction(msg.sender, tokenQuantity, nonce) == hash, "HASH_FAIL"); // Out of stock check require(totalSupply() < TOTAL_SUPPLY, "OUT_OF_STOCK"); require(publicAmountMinted + tokenQuantity <= DISTRIBUTED_TO_PUBLIC, "EXCEED_PUBLIC"); require(tokenQuantity <= MAX_PER_MINT, "EXCEED_MAX"); require(PRICE * tokenQuantity <= msg.value, "INSUFFICIENT_ETH"); for(uint256 i = 0; i < tokenQuantity; i++) { publicAmountMinted++; _safeMint(msg.sender, totalSupply() + 1); } // Add the nonce to the list of already used nonce _usedNonces[nonce] = true; }
In the final post, we'll see how to add some fair minting logic to our smart contract with the whitelisting method.
Check out the GitHub repository for the full source.