The Picaroons is a new NFT collection by Alex Lucas, with support from Pranksy. Holders of certain NFTs by Alex were able to mint up to 2 tokens, then during public mint anyone could mint 2 tokens (and previous minters could mint 2 more). The 10k collection quickly sold out during public mint, and is now only available on secondary marketplaces.
An interesting thing about this contract is the extra effort into preventing bots from minting directly from the contract. They maintained an allow list of addresses in their web app instead of putting the snapshot into the contract, and minting required a signature generated by their web app. Let’s dig into the contract code…
Mint
function mint(bytes32 hash, bytes memory signature, uint256 tokenQuantity, uint256 maxAmountAllowed) external payable {
require(saleLive, "SALE_CLOSED");
// Is the signature valid
require(matchAddresSigner(hash, signature), "DIRECT_MINT_DISALLOWED");
// Verify if the keccak256 hash matches the hash generated by the hashTransaction function
require(hashTransaction(msg.sender, tokenQuantity, maxAmountAllowed) == hash, "HASH_FAIL");
// Out of stock check
require(totalSupply() + tokenQuantity <= TOTAL_SUPPLY, "OUT_OF_STOCK");
// Is the address allow more mint more tokens?
require(amountMintedList[msg.sender] + tokenQuantity <= maxAmountAllowed, "EXCEED_ALLOC");
require(PRICE * tokenQuantity <= msg.value, "INSUFFICIENT_ETH");
for(uint256 i = 0; i < tokenQuantity; i++) {
amountMintedList[msg.sender]++;
_safeMint(msg.sender, totalSupply() + 1);
}
}
The mint function requires the following:
- Sale isn’t closed
- The
hash
argument was signed (signature
) by a known signing address - The
hash
argument can be verified with the other function arguments - There are tokens available to mint
- You have not minted too much
- You sent enough ETH
Once those checks pass, it’s a simple mint loop, incrementing a counter for how many you have minted. The interesting parts are requirements 2 & 3.
Match Address Signer
address private _signerAddress = 0xF5F6C1B8F13F2A41Ce8474AAD3Dd5050364eef1f;
...
function matchAddresSigner(bytes32 hash, bytes memory signature) private view returns(bool) {
return _signerAddress == hash.recover(signature);
}
The _signerAddress
is defined as a static variable. In order to generate a verifiable signature
for the hash
, the private key associated with this signing address must exist in their web app. Because this address is static, with no way to change it, they hopefully had some good app and server security to prevent any leaking of the private key. Assuming no one else has access to the private key, the signature
can only be generated by their web app, and therefore no one, including bots, could mint directly from the contract. All minting actions had to go through their web app, which can be protected from bots using more traditional methods.
How did they secure their web app from bots?
- They required a captcha before a signature was generated, which prevents most web bots.
- Before the public mint, only addresses on an allow list based on NFT ownership were able to mint.
This allow list was generated from a snapshot ~2 weeks before minting was enabled. But unlike many allow list snapshots, it wasn’t put into the contract. Instead they must have made a database for their web app to check against, which was only used during the pre-sale.
How did they make a signature in a web app that could be verified within the contract?
Libraries like web3.js provide a way to sign data in javascript using a private key. The library also provides message hashing. So by using a library like web3.js in a server-side web app, you can do the following:
- Encode function arguments into a message
- Hash that message
- Sign the hash
The hash and signature can then be returned from the web app to the browser, then passed to the contract, thereby preventing direct contract minting.
Hash Transaction
This function reconstructs the hashed message that was signed. It does this by encoding the arguments, then hashing them with solidity’s standard hashing algorithm, keccak256.
function hashTransaction(address sender, uint256 qty, uint256 maxAmountAllowed) private pure returns(bytes32) {
bytes32 hash = keccak256(abi.encodePacked(
"\x19Ethereum Signed Message:\n32",
keccak256(abi.encodePacked(sender, qty, maxAmountAllowed)))
);
return hash;
}
That’s about it for the minting. Let’s see what Slither says…
Slither Analysis
Slither is a python tool for static analysis of Solidity contracts. You can use it to get a quick summary of the contract code, and then look for any deeper issues.
$ slither 0x545f0a45Ba06C7C5b1a5Fb0b29008462ceEA07b7 --print human-summary

No significant issues found. Nearly all the issues are in the dependency contracts, which are pretty standard, and the only issues Slither finds for ThePicaroons
contract are cosmetic.
Gas Efficiency
The contract uses ERC721Enumerable. While it’s a somewhat common extension to ERC721, and it adds some useful functionality, it definitely increases gas costs for minting and transfers. Azuki did a deep analysis of this and created ERC721A to solve this gas issue without losing key functionality. Using ERC721A would have been a nice improvement to the Picaroons contract, but the contract is otherwise very simple and efficient.
Provenance Hash
One final detail in the contract is the use of a NFT Provenance Hash. This can be used to verify that the reveal after mint was defined before minting began, and not manipulated after mint but before reveal. The fourth transaction on the contract, right after public mint was enabled, was to set the provenance hash to e4b58ca66f8bf42902acff422cb634ff8c6d012627a71f195071c725558670b9
. While they don’t give a nice breakdown on their website for verifying the metadata of the tokens, like BAYC does, it’s still a good thing to do and provides that verification capability to anyone that wants to do the work.
/// Set the smart-contract proof as an hash of all NFT metadata hashes
/// @param hash of all NFTs metadata hashes
function setProvenanceHash(string calldata hash) external onlyOwner notLocked {
proof = hash;
}