Floot = Fair Loot

--

The Floot smart contracts are available on GitHub with an Apache license. The goal of Floot is to share better practices for randomness design in NFTs.

Floot is a blind drop implementation of Loot, designed to address security concerns with the design of Loot and many of its derivative projects. 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.

Common pitfalls with on-chain randomness

Problems with block.timestamp / block.basefee / blockhash / etc.

It’s well-known that on-chain randomness is tricky to get right. 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 reproducible on-chain, this method is vulnerable to “postselection:” an attacker can decide whether or not to mint, conditional on the outcome of the mint. This is done by wrapping 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.

How about VRF?

A VRF like Chainlink’s could be used to avoid the exploit described above, if the smart contract is carefully designed to protect against postselection. I don’t think the added cost per transaction of the VRF oracle is worth it, however, for our use case.

Floot blind drop design

Overview

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.

Analysis of Hashmasks on-chain randomness

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 design 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:

  1. 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.
  2. The contract is deployed with the provenance hash set as a constant.
  3. 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.
  4. In a later block, we set startingIndex = blockhash(startingIndexBlock) % MAX_NFT_SUPPLY.
  5. 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 the HashmasksstartingIndex to their benefit, an attacker would need to:

  1. Have prior knowledge of the exact NFT images being sold.
  2. Manipulate the block hash of the block containing the call to mintNFT() that sets startingIndexBlock (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 “postselection” attacks above). 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.

Floot on-chain randomness

We adapt the Hashmasks model described above with the following changes:

First, we 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.

Second, we change startingIndexBlock = block.number to startingIndexBlock = 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.
  • 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:

  1. Prior to deploying the contract, the guardian generates a secret guardianSeed as a random 32-byte string.
  2. When the smart contract is deployed, the keccak256 hash (i.e. commitment) of the guardian seed is set as an immutable value.
  3. After a certain timestamp is reached, or the last token is sold (whichever comes first) we set automaticSeedBlockNumber = block.number + 1.
  4. 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 either automaticSeed or guardianSeed 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 the automaticSeed. We then set finalSeed = automaticSeed XOR fallbackSeed.

The purpose of fallbackSeed is to ensure that there is no incentive for the guardian to withhold guardianSeed.

Other optimizations

  • Cheaper minting: Floot uses ERC721EnumerableOptimized.sol, an optimized version of OpenZeppelin’s ERC721Enumerable.sol that can be used with NFTs that are minted serially and which do not support burning. This results in a ~31% reduction in the minting cost of Floot, with no loss of functionality.

Next steps

Floot is open-source under an Apache-2.0 license. You are welcome to use the code in your own projects.

The last few days have seen Loot-derived projects continue to proliferate using flawed smart contract designs. While a blind drop model has its own tradeoffs, I think it’s a promising step towards the fair distribution of on-chain generative NFTs like Loot.

If you have feedback, questions, or are otherwise interested in Floot, please DM me on Twitter.

References

--

--