Floot is a blind drop implementation of Loot, designed to address security concerns with the smart contract design of Loot. A secondary goal is to reduce gas costs for users.
Advantages of Floot over Loot and other designs include:
- Fair and random distribution of tokens.
- Secure against frontrunning, dark pools, and manipulation by miners.
- Secure against cheating by the NFT creator.
- No contract owner and no founder allocation.
- A 31% reduction in gas cost per mint.
Limitations:
- The tokens are not revealed until the end of the token distribution.
Floot is deployed with the following constructor parameters:
guardianHash
- Hash of the seed held by the guardian as one of the inputs to randomness.guardianWindowDurationSeconds
- Period of time after the distribution, during which the guardian should provide their seed.maxDistributionDurationSeconds
- Period of time after deployment after which the distribution will end even if the max supply was not distributed.maxSupply
- The maximum number of tokens that can ever be minted.
See ./scripts/deploy.js
for some details and recommended defaults, but note that a different guardianHash
must be used for every deployment. See below for instructions on generating a new guardianHash
.
The guardian hash is used as one of the inputs to randomness, to minimize the ability of miners to independently exploit the distribution. The guardian for a distribution can be a member of the team launching the NFT, or any semi-trusted third party.
The guardian hash is the keccak hash of a random, secret 32-byte seed. Make sure the seed is generated securely with at least 16 bytes of entropy. For convenience, we can generate the seed from a password.
In JavaScript:
const password = require('crypto').randomBytes(24).toString('base64')
const seed = require('Web3').utils.soliditySha3(password)
const hash = require('Web3').utils.soliditySha3(seed)
console.log(`Guardian password: ${password}`)
console.log(`Guardian hash: ${hash}`)
The password or seed should be stored securely by the guardian. The guardian hash is public and should be passed as a constructor parameter to the Floot smart contract.
When the distribution has ended, the following function calls should be made to reveal the NFTs:
setAutomaticSeedBlockNumber();
// Must be a full gap of one block before the next call.
setAutomaticSeed();
setGuardianSeed(guardianSeed);
setFinalSeed();
Like Loot, a Floot token consists of a Bag of eight items, rendered fully on-chain as an SVG. In Loot, the items in a Bag are picked according to a “random” value which is determined by:
- The token ID (1 through 8000)
- A fixed prefix for each of the eight “slots” in the bag (
WEAPON
,CHEST
, etc.)
In Floot, we add a third value as an input to randomness:
- A global random seed generated securely after the end of the token distribution.
All minting is “blind” as the Bags are only revealed after minting has ended. The randomness of the distribution depends fully upon the process for generating the seed. Our method is inspired by the Hashmasks blind drop, but adapted to allow the content of the NFTs (the SVGs) to be generated on-chain.
The Floot blind drop design is based on the Hashmasks smart contract, which was adopted by BAYC and many other NFT projects. When used correctly, their approach ensures that the token distribution is fair and random such that even the team launching the token cannot manipulate the drop to get better or specific tokens.
It works as follows:
- Before the sale, the team computes a provenance hash which is a commitment to the exact NFT images and “original sequence” ordering of these images.
- The contract is deployed with the provenance hash set as a constant.
- When a token is sold after a certain timestamp has been reached, or the last token is sold (whichever comes first) the
startingIndexBlock
is set to the current block number. - In a later block, we set
startingIndex = blockhash(startingIndexBlock) % MAX_NFT_SUPPLY
. - Each token ID is assigned an image by the formula
(tokenId + startingIndex) % MAX_NFT_SUPPLY => Image Index From the Original Sequence
.
Assuming that startingIndex
cannot be manipulated, any token purchase made before the “reveal event” (step 3) is blind, in that the purchaser has no control over which image they receive. To manipulate startingIndex
to their benefit, an attacker would need to:
- Have prior knowledge of the exact NFT images being sold.
- Manipulate the block hash of the block containing the call to
mintNFT()
that setsstartingIndexBlock
(step 3 above).
Why are two separate txes needed to set the random index? There is an important difference between the method used here and the more naive method of referencing blockhash(block.number - 1)
as a source of randomness. The naive method is vulnerable to relatively simple attacks which make attempted mints while reverting if the attacker does not like the random number that was generated (see discussion of dark pools below). In contrast, using the two-step process requires an attacker to have significant mining resources of their own, and to actually withhold blocks in order to manipulate the result. This makes the attack extremely expensive.
We adapt the Hashmasks model described above with the following changes:
- Use an additional
guardianSeed
component. This ensures that, like Hashmasks, Floot cannot be attacked by miners on their own. Rather, attacking Floot requires collusion between the guardian and miners. - Change
startingIndexBlock = block.number
tostartingIndexBlock = block.number + 1
. Using the next block instead of the current block is a simple change which makes a miner attack significantly more difficult, since they must either:- Be willing to calculate and withhold a series of two blocks rather than one; or
- Compute a malicious block hash in the single block window of time (e.g. 13 seconds) after someone else sets the
startingIndexBlock
.
The addition of a guardian only strengthens the security properties of the system. The ideal guardian is someone who has some interest/stake in the success of the initial token distribution.
Seed generation is handled by BlindDrop.sol
and proceeds as follows:
- Prior to deploying the contract, the guardian generates a secret
guardianSeed
as a random 32-byte string. - When the smart contract is deployed, the keccak256 hash (i.e. commitment) of the guardian seed is set as an immutable value.
- After a certain timestamp is reached, or the last token is sold (whichever comes first) we set
automaticSeedBlockNumber = block.number + 1
. - In a later block, we set
automaticSeed = blockhash(automaticSeedBlockNumber)
. This begins the “guardian window” in which the guardian should submit their pre-commited seed.- If the guardian submits their seed within the window, then we set
finalSeed = automaticSeed XOR guardianSeed
. The result is random if eitherautomaticSeed
orguardianSeed
is random. - If the guardian fails to submit their seed within the specified window, then a
fallbackSeed
is computed using the same two-step method used to generate theautomaticSeed
. We then setfinalSeed = automaticSeed XOR fallbackSeed
.
- If the guardian submits their seed within the window, then we set
The purpose of fallbackSeed
is to ensure that there is no incentive for the guardian to withhold guardianSeed
.
A VRF like Chainlink's is generally a good idea, but I don't think the added cost per transaction is worth it here.
Some contracts use a “naive” source of randomness based on the block metadata. This can make it difficult or infeasible for an ordinary attacker to predict the random value produced in a particular call to the minting function.
However, since the outcome of a mint is observable on-chain, an attacker can wrap minting calls in a smart contract that reverts if a minted token does not have the desired rarity. By using dark pools (e.g. Flashbots) an attacker can reduce the cost of failed mint attempts, which may make this attack efficient in practice.
This vulnerability leaves us in a worse place than where we started, since it can skew the rarity distribution of the series as a whole.
- Loot.sol
- Masks.sol by HashMasks
- ERC721FairDistribution.sol by Chunky Cow Club Tour
- Thanks to
trestian
for useful discussion regarding attacks on naive sources of randomness.