How to Airdrop NFTs (At a Fraction of the Cost)

Last Updated:
August 11, 2022
Airdrop NFTs Feature Image

When you run an airdrop with Metacommerce, you benefit from three major gas saving techniques that allow creators to run cost-effective airdrop campaigns based on previous token-holdings.

Introducing: The Metacommerce Airdrop

When you run an airdrop with Metacommerce, you benefit from three major gas saving techniques that allow creators to run cost-effective airdrop campaigns based on previous token-holdings.

Building on the shoulders of giants

Many thanks to innovators in the space, most notably CHANCE and 0xInuarashi, whose findings provided a backbone for the development of the Metacommerce Airdrop. 

In this article we will walk through the gas saving techniques employed by the Metacommerce Airdrop.

1. Clone Factory Proxy-Standard


In the traditional method of creating airdrops, the creator needs to deploy both the implementation and storage of every new token. This is expensive if the creator seeks to run a series of airdrops. 

However, we notice that the specifications in each airdrop contract all have the same base functionality but different storage states. In order to save gas costs, the Metacommerce Airdrop deploys an airdrop template implementation with all the base functions and the creator just initialises a storage instance that points to that template through the clone factory. 

Metacommerce deploys a single template contract with all the required function implementations at a cost of around 2,829,008 units of gas. This is a cost that is covered, in advance, by Metacommerce and there are no recurring costs until the template contract needs an upgrade. This will never be a cost that the creator pays.

The creator, on the other hand, only needs to spend around 351817 units of gas to initialise the contract storage. That is ~ 8x less than what they would normally need to spend to deploy a contract.

2. Phantom Airdrop w/ EIP2309

MerkleTree Airdropping is one of the popular standards for allowing loyal token-holders to mint tokens out of a newly deployed airdrop-contract (aka write their ownership to storage). To give some benchmarks, the costs total to around 1,351,155 + N * 73,904 units of gas for contract deployment and minting. Where N Is the total number of token-holders that are permissioned to receive the airdrop. This cost can accumulate quickly with larger token-owner cohorts. In addition, the cost of receiving tokens is levied on the loyal token holder.

We can do better. 

Upon the initial drop, the parent-token ownership mapping is exactly the same as the child-token ownership mapping. The mappings diverge when owners in the child-token start transferring away their airdropped tokens. We take advantage of this fact and change the ownerOf(tokenId) function in the EIP721-standard to redirect the ownerOf(tokenId) call in the child to read the parent mapping only if that tokenId has not been moved yet (through the transferFrom function). If it has been moved then we know that the ownership states have diverged and the ownership of a specific tokenId has been written to the child-contract’s local storage.

A consequence of this method is that if an owner in the parent-token contract decides to transfer their asset elsewhere without explicitly writing to storage in the child-contract through the transferFrom function, they subsequently also transfer away their airdropped token.

To notify marketplaces that an airdrop has occurred and have the NFT show-up in owners’ wallets, we leverage EIP2309 by emitting the ConsecutiveTransfer event over all the tokenIds that the child-contract references in the parent-contract. 

Heavy credit goes to CHANCE’s article on mimetic airdrops which you can read here.

3. ERC721A and Bitmaps

Inspired by ERC721A written by Azuki, we can reduce the cost of writing to storage significantly by representing NFT metadata structs with bitmaps.

 

We can represent the ownership mapping as (uint256 => uint256) instead of (uint256 => address).This allows us to pack any other relevant metadata into the bitmap mapping, instead of having to initialise another mapping for things like i.e  isBurned (uint256 => bool), or any other metadata that needs to be associated with the tokenId.

The same goes for any additional metadata with regards to the owner, like their balance, the number of tokens minted, burned, etc.  We can pack all that information in one (address => uint256) mapping. 

Representing data through bitmaps allows us to achieve significant gas savings on writes. Through this, we were able to include the feature of running airdrop campaigns on a subset of token-holders instead of the entire collection, in a cost-effective manner.

The naive implementation for this feature would be to pass in a list of uint256 tokenIds that are a subset of parent tokenIds. Then perform a check in the ownerOf function to see if the queried tokenId belongs in that list. However, if we have a subset of hundreds of tokens to airdrop out of a collection of thousands, performing hundreds of uint256 writes to set the whitelist would negate the gas-savings from the previous sections.

Instead, we can represent the list of tokenIds as bit positions on a uint256 data-type, where each bit position corresponds to a tokenId; a 0 means that tokenId should not be airdropped, and 1 means it should. To illustrate an example, if the parent collection has a size of 10,000 tokenIds, and assuming they are enumerated, we can encode selection data on all 10,000 tokens with just 10,000 / 256  = ~39 uint256 data-types. 

To create a selection of tokenIds: [1,5,100], we flip the bits of each corresponding tokenId, and represent that as a uint256.


In this case the bitmask list will be just 1, but if we were to include tokenId 1000. We need to append 3 more uint256 that correspond to tokenIds ranges:

[256 to 511, 512 to 767, 768 to 1023], then flip bit 232 in the 768 to 1023 uint256.

setAidropBitmask

     * This function creates a whitelist of tokenIds to be airdropped to

     */

    function setAirdropBitmask(uint256[] memory _bitmask) public onlyOwner {

        require(isSelectiveAirdrop, "Contract is not selective airdrop mode.");

        for (uint256 i = 0; i < _bitmask.length; i++) {

            _airdropBitmask[i] = _bitmask[i];

        }

    }

This function will set the whitelist. Notice, we can pass any sized bitmap, however this can get costly with extremely large tokenIds. However, most collections are under-enumerated and have less than 10,000 tokenIds.

isValidTokenId

 function isValidTokenId(uint256 tokenId) public view returns (bool) {

        require(isSelectiveAirdrop, "Contract is not selective airdrop mode.");

        uint256 depth = tokenId / 256;

        uint256 layerBitmask = _airdropBitmask[depth];

        uint256 bitMask = 1 << (tokenId % 256);

        return ((layerBitmask | bitMask) == layerBitmask);

    }

A check to validate that a tokenId bit has been set to 1.

ownerOf

function ownerOf(uint256 tokenId) public view virtual returns (address) {

        require(_startTokenId() <= tokenId && tokenId < ERC721AStorage.layout()._currentIndex, "ERC721: approved query for nonexistent token");

        // If the address has been written to storage use the stored address

        if (isSelectiveAirdrop) {

            require(isSelectiveAirdrop && isValidTokenId(tokenId), "ERC721: approved query for nonexistent token");

        }

Adding this require will revert all calls that query for a tokenId that is not whitelisted, masking out the tokenId’s ownership from the parent contract.

Ready to airdrop at a fraction of the cost?

Sign up to the Metacommerce ORM beta to get access to our gas saving airdrop technology.

Get In-depth Web3 and NFT insights weekly.
Subscribe to our newsletter

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.