Docs

Drop design document.

This is a live document that explains what the thirdweb Drop smart contracts are, how they work and can be used, and why they are written the way they are.

The document is written for technical and non-technical readers. To ask further questions about any of thirdweb's Drop, please visit our support site or create a github issue.

Background

The thirdweb Drop contracts are distribution mechanisms for tokens. This distribution mechanism is offered for ERC20, ERC721 and ERC1155 tokens, as DropERC20, DropERC721 and DropERC1155.

The Drop contracts are meant to be used when the goal of the contract creator is for an audience to come in and claim tokens within certain restrictions e.g. — ‘only addresses in an allowlist can mint tokens', or ‘minters must pay x amount of price in y currency to mint', etc.

The Drop contracts let the contract creator establish phases (periods of time), where each phase can specify multiple such restrictions on the minting of tokens during that period of time. We refer to such a phase as a ‘claim condition'.

Why we're building Drop

We've observed that there are largely three distinct contexts under which one mints tokens —

  • Minting tokens for yourself on a contract you own. E.g. a person wants to mint their Twitter profile picture as an NFT.
  • Having an audience mint tokens on a contract you own.
    • The nature of tokens to be minted by the audience is pre-determined by the contract admin. E.g. a 10k NFT drop where the contents of the NFTs to be minted by the audience is already known and determined by the contract admin before the audience comes in to mint NFTs.
    • The nature of tokens to be minted by the audience is not pre-determined by the contract admin. E.g. a course 'certificate' dynamically generated with the name of the course participant, to be minted by the course participant at the time of course completion.

The thirdweb Token contracts serve the cases described in (1) and 2(ii).

The thirdweb Drop contracts serve the case described in 2(i). They are written to give a contract creator granular control over restrictions around an audience minting tokens from the same contract (or ‘collection', in the case of NFTs) over an extended period of time.

Technical Details

The distribution mechanism of Drop is as follows — A contract admin establishes a series of 'claim conditions'. A 'claim condition' is a period of time in which accounts can mint tokens on the respective Drop contract, within a set of restrictions defined by the ‘claim condition'.

Claim Conditions

The following makes up a claim condition —

struct ClaimCondition {
uint256 startTimestamp;
uint256 maxClaimableSupply;
uint256 supplyClaimed;
uint256 quantityLimitPerWallet;
bytes32 merkleRoot;
uint256 pricePerToken;
address currency;
}
ParametersTypeDescription
startTimestampuint256The unix timestamp after which the claim condition applies. The same claim condition applies until the startTimestamp of the next claim condition.
maxClaimableSupplyuint256The maximum total number of tokens that can be claimed under the claim condition.
supplyClaimeduint256At any given point, the number of tokens that have been claimed under the claim condition.
quantityLimitPerWalletuint256The maximum number of tokens that can be claimed by a wallet under a given claim condition.
merkleRootbytes32The allowlist of addresses that can claim tokens under the claim condition.

(Optional) The allowlist may specify quantity limits, price and currency for addresses in the list, overriding these values under that claim condition.

The parameters that make up a claim condition can be composed in different ways to create specific restrictions around a mint. For example, a single claim condition where:

  • quantityLimitPerWallet = 5
  • merkleRoot = bytes32(0)

creates restrictions around a mint, where (1) a wallet can mint at most 5 tokens and (2) all wallets are subject to general claim condition limits, without any overrides.

A Drop contract lets a contract admin establish a series of claim conditions, at once. Since each claim condition specifies a startTime, a contract admin can establish a series of claim conditions, ordered by their start time, to specify different set of restrictions around minting, during different periods of time.

At any moment, there is only one active claim condition, and an account attempting to mint tokens on the respective Drop contract successfully or unsuccessfully, based on whether the account passes the restrictions defined by that moment's active claim condition.

A Drop contract natively keeps track of claim conditions set by a contract admin in a ‘claim conditions list', which looks as follows —

struct ClaimConditionList {
uint256 currentStartId;
uint256 count;
mapping(uint256 => ClaimCondition) conditions;
mapping(uint256 => mapping(address => uint256)) supplyClaimedByWallet;
}
ParameterDescription
currentStartIdThe uid for the first claim condition amongst the current set of claim conditions. The uid for each next claim condition is one more than the previous claim condition's uid.
countThe total number of claim conditions in the list of claim conditions.
conditionsThe claim conditions at a given uid. Claim conditions are ordered in an ascending order by their startTimestamp.
supplyClaimedByWalletMap from a claim condition uid and account to the supply claimed by that account.

Allowlist as an override list

As mentioned above, an allowlist can specify different conditions for addresses in the list. This way, it serves as an override over general/open restrictions for non-allowlisted addresses. In this allowlist or override-list, an admin can set any/all of these three:

  • quantity limit
  • price
  • currency

If a value is not set for any of these, then the value specified in general claim condition will be used. However, currency override will be considered only when a price override is set too, and not without it.

IMPORTANT: The allowlist should not contain an address more than once in the same merkle tree. Multiple instances for the same address (with different/same quantity, price etc.) may not function as expected and may lead to unexpected behavior during claim. (More details in Limitations section).

Setting claim conditions

In all Drop contracts, a contract admin specifies the following when setting claim conditions:

ParameterTypeDescription
conditionsClaimCondition[]Claim conditions in ascending order by startTimestamp.
resetClaimEligibilityboolWhether to reset supplyClaimedByWallet values when setting new claim conditions.

When setting claim conditions, any existing set of claim conditions stored in ClaimConditionsList are overwritten with the new claim conditions specified in conditions.

The claim conditions specified in conditions are expected to be in ordered in ascending order, by their ‘start time'. As a result, only one claim condition is active during at any given time.

Each of the claim conditions specified in conditions is assigned a unique integer ID. The UID of the first condition in conditions is stored as the ClaimConditionList.currentStartId and each next claim condition's UID is one more than the previous condition's UID.

Claim conditions

The resetClaimEligibility boolean flag determines what UIDs are assigned to the claim conditions specified in conditions. Since ClaimConditionList.supplyClaimedByWallet is indexed by the UID of claim conditions, this gives a contract admin more granular control over the restrictions that claim conditions can express. We now illustrate this with an example:

Let's say an existing claim condition C1 specifies the following restrictions:

  • quantityLimitPerWallet = 1
  • merkleRoot = bytes32(0)
  • pricePerToken = 0.1 ether
  • currency = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE (i.e. native token of the chain e.g ether for Ethereum mainnet)

At a high level, C1 expresses the following restrictions on minting — any address can claim at most one token, ever, by paying 0.1 ether in price.

Let's say the contract admin wants to increase the price per token from 0.1 ether to 0.2 ether, while ensuring that wallets that have already claimed tokens are not able to claim tokens again. Essentially, the contract admin now wants to instantiate a claim condition C2 with the following restrictions:

  • quantityLimitPerWallet = 1
  • merkleRoot = bytes32(0)
  • pricePerToken = 0.2 ether
  • currency = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE (i.e. native token of the chain e.g ether for Ethereum mainnet)

To go from C1 to C2 while ensuring that wallets that have already claimed tokens are not able to claim tokens again, the contract admin will set claim conditions while specifying resetClaimEligibility == false. As a result, the C2 will be assigned the same UID as C1. Since ClaimConditionList.supplyClaimedByWallet is indexed by the UID of claim conditions, the information of the quantity of tokens claimed by the wallet during C1 will not be lost. And so, wallets that claimed tokens during C1 will now be ineligible to claim tokens during C2 because of the following check:

// pseudo-code
supplyClaimedByWallet = claimCondition.supplyClaimedByWallet[conditionId][claimer];
require(quantityToClaim + supplyClaimedByWallet <= quantityLimitPerWallet);

EIPs supported / implemented

The distribution mechanism for tokens expressed by thirdweb's Drop is implemented for ERC20, ERC721 and ERC1155 tokens, as DropERC20, DropERC721 and DropERC1155.

There are a few key differences between the three implementations —

  • DropERC20 is written for the distribution of completely fungible, ERC20 tokens. On the other hand, DropERC721 and DropERC1155 are written for the distribution of NFTs, which requires ‘lazy minting' i.e. defining the content of the NFTs before an audience comes in to mint them during a claim condition.
  • Both DropERC20 and DropERC721 maintain a global, contract-wide ClaimConditionsList which stores the claim conditions under which tokens can be minted. The DropERC1155 contract, on the other hand, maintains a ClaimConditionList for every integer token ID that an NFT can assume. And so, a contract admin can set up claim conditions per NFT i.e. per token ID, in the DropERC1155 contract.

Limitations

Sybil attacks

The distribution mechanism of thirdweb's Drop contracts is vulnerable to sybil attacks. That is, despite the various ways in which restrictions can be applied to the minting of tokens, some restrictions that claim conditions can express target wallets and not persons.

For example, the restriction quantityLimitPerWallet expresses the max quantity a wallet can claim during the respective claim condition. A sophisticated actor may generate multiple wallets to claim tokens in a way that undermine such restrictions, when viewing such restrictions as restrictions on unique persons, and not wallets.

Allowlist behavior

When specifying allowlist of addresses, and quantities, price, etc. for those addresses, contract admins must ensure that they don't list an address more than once in the same merkle tree.

For e.g. admin wishes to grant user X permission to mint 2 tokens at 0.25 ETH, and 4 tokens at 0.5 ETH. In this case, the contract design will not permit the user X to claim 6 tokens with different prices as desired. Instead, the user may be limited to claiming just 2 tokens or 4 tokens based on their order of claiming.

To avoid such pitfalls, an address should be listed only once per merkle tree or allowlist.