HederaDrops is one contract on Hedera. This site is an interface to it; so is anything you build — nothing here needs our permission, our API, or our indexer.
Why the contract is shaped this way — what each rule guarantees, and what it deliberately leaves open.
A drop's on-chain status decides what may touch it. The pool and the granted entries close at publish and the whitelist closes when the sale opens; entries are bought only inside the window; NFTs leave only once a seed exists.
None → Created → Published → Revealed
└──────→ Canceled (host, while entered == 0)Canceling is bounded by that guard rather than by a stage: it stays open until the first entry exists and closes for good afterwards, so no drop can be pulled out from under somebody who has paid. It is terminal — a canceled drop never reopens, and its pool leaves through claimCanceledNFTs.
The protocol's only lever over a live drop is stopped, and it reaches createDrop and addNFTs alone. Drops that already exist keep selling, revealing and paying out with the switch off.
A drop escrows N NFTs and draws over T entries, where NFT j is won by entry permute(seed, j, T). T is not what the host published — that number is the sale cap. The domain is settled at the reveal to max(cap, entered, N), which keeps it at or above the pool however few entries were offered or sold.
Entries partition by index alone — [0, granted), [granted, entered) and [entered, T)— and the last of those is written to the host inside the reveal call. The map above is a real draw across those three ranges. It is also why an undersold drop needs no special case: the unsold tail is an ordinary entry block held by the host, and the share of the pool it wins comes back through the same permutation as everyone else's.
_permute is a bijection on [0, T), so its restriction to [0, N) maps onto N distinct entries — the winning set is an N-subset, never a multiset. With the seed uniform over bytes32 that subset is uniform over all C(T, N) possibilities, giving
By linearity a block of k entries has expectation k·N/T — exact, and independent of which indices it occupies.
The zones are index ranges and nothing more. _permute takes only (seed, j, T), and _claimOne compares its output against [entryId, entryId + count) — neither reads an owner, a time, or whether the entry was paid for. Granted, sold and unsold ranges are statistically indistinguishable.
Two properties worth checking independently: injectivity fails if T < N (enforced at publish), and the cycle-walk terminates because it iterates a power-of-two domain ≥ T, revisiting no value inside one walk.
The permutation is a dozen lines, published under Drawin the reference. Every drop's result is therefore reproducible off chain from its seed alone — the outcome is checkable by anyone, not merely asserted.
Hedera produces the number, so the reveal could be left to whoever wants to send it. It is not, because whoever sends it can grind: work out what the number would be, discard the results they dislike, try again.
Handing that to an administrator would be worse — every drop on every frontend would sit behind one party. So each drop names its own revealer at publish, and three parties meet at that choice:
| Platform | decides which revealer its interface puts forward. Drops opened on this site name ours; a platform building its own names whoever it trusts. |
| Host | sets it in the publish call. Any address — the platform's default is only a default. |
| Buyer | reads it on chain before paying. Entering is accepting the choice, and declining costs nothing. |
The grace window bounds that risk rather than removing it. Until closeTime + revealGrace (7 days) the named revealer holds the reveal alone; afterwards revealSeed is permissionless, so holding a result back secures nothing and only risks someone else sending it first. Grinding is confined to one party for one window, and the window closes without anyone's intervention — which is the same rule that stops a silent revealer from freezing a drop.
That is the decentralisation argument in full: no address is wired into the contract, the choice is made per drop by parties who need not trust each other, it is visible before anyone pays, and the fallback needs nobody's permission.
An NFT leaves a pool for exactly three reasons, and each has its own function. They are separate because the right to take it out differs — a different precondition and a different recipient. One call with a mode flag would put both behind a branch and make the revert reason ambiguous.
| claimNFT(s) | It was won. Needs a seed; the index is proved against the permutation on chain. Recipient: the entry block's participant. |
| claimCanceledNFT(s) | The drop was called off. Needs status Canceled. Recipient: the host. |
| claimExpiredNFT(s) | Nobody claimed in time. Recipient: the host after 100 days, the caller after 300. |
All three funnel through one internal exit that flags the NFT before it moves, so never twice is enforced in one place rather than three. The pool only ever appends, so an index names the same NFT forever — an emptied slot stays empty even if that serial is later deposited into another drop.
The recipient is read from storage, never from the caller.That is what makes all three permissionless: a stranger can only spend gas delivering somebody else's NFT. The single exception is deliberate — after 300 days the caller becomes the recipient, because an NFT nobody is entitled to claim is one nobody has a reason to free either, and a bounty is cheaper than an escape hatch held by an administrator.
So delivery is separable from entitlement. The winner claiming their own is only the base case; the same call sent by a platform, a bot or a stranger lands in the same place, which is why a drop can be settled for its participants without any of them appearing on chain again.
One precondition sits outside all of that: the recipient must already be associated with the collection, and association is signed by that account alone. A delivery to an unassociated recipient fails with "transfer failed (associate?)"— the one part of a claim nobody can perform on the winner's behalf.
One payment splits three ways inside the entry transaction — no separate invoice, no second transfer to wait for.
| protocolFeeBps | protocol-wide, 0.5% by default, paid to the protocol wallet. |
| platformFeeBps | set by the host at publish together with the address that receives it. Zero means no platform is involved. |
| Both | snapshotted into the drop at publish — a later change to either reaches new drops only. |
The platform fee is claimed in two different places, which is what separates opening a drop from sellingit. The address recorded at publish is the drop's own platform; the address passed as the platform argument to enter is whichever interface the entry was bought through:
| enter(…, 0) or the drop's own platform | the drop's platform takes the whole platform fee |
| enter(…, someone else) | split 50/50 — the drop's platform keeps half plus the odd tinybar, the selling platform takes the rest |
The split is enforced by the contract, not by an agreement between the two interfaces, and it comes out of the fee the host already set rather than out of the buyer — the price is the price. Distribution therefore needs no permission from the drop's platform, and costs it only half of what it would otherwise never have sold.