Skip to main content

How it works

Droppie has three moving parts:

  • Contracts — one plain DroppieDrop escrow per campaign, deployed straight from the creator's wallet, plus DroppieERC20 for no-code tokens. No factories, nothing pre-deployed. See the contracts reference.
  • SDK@droppie/sdk builds Merkle trees, parses CSVs, generates proofs, and ships the contract ABIs and creation bytecode. The web app and your scripts use the exact same code.
  • Web app — a Next.js front end that orchestrates everything client-side. Your recipient list never touches a Droppie server; trees are built in your browser.

The create flow

Key properties:

  • Your wallet deploys the contract. The drop's constructor runs in a transaction you sign, and the deploying wallet becomes creator. No factory sits in the middle, and nothing is pre-deployed — which is exactly why Droppie works on any configured EVM chain.
  • Two quick transactions: deploy, then fund. Funding is a plain ERC-20 transfer into the drop — no approve step. Until the transfer lands the drop shows as underfunded and the app gates claims; a claim against an underfunded drop reverts harmlessly without consuming claim state.
  • One escrow per campaign. Each drop is its own contract with its own token balance, and every parameter except the metadata string is a Solidity immutable. A bug or sweep in one campaign can never touch another's funds.
  • Only the root goes on-chain. However large the list, the contract stores a single 32-byte Merkle root. The full list lives in the distribution JSON at metadataURI.

The claim flow

The claim page resolves the distribution JSON from the drop's metadataURIipfs:// URIs via public gateways, plain https:// URLs, or inline data: URIs. If none of those load, the recipient can upload the file manually; proofs are always computed locally.

The Merkle tree

Droppie uses OpenZeppelin's StandardMerkleTree with the leaf encoding ["address", "uint256"] — one leaf per (recipient, amount) pair.

Leaf hashing

Each leaf is double-hashed:

bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(recipient, amount))));

Why hash twice? The inner keccak256(abi.encode(...)) is 32 bytes — the same size as an internal tree node. Without the outer hash, a crafted 64-byte "amount + address" blob could be confused with a concatenated pair of internal nodes, enabling a second-preimage attack where an internal node is presented as a leaf. Double-hashing makes leaves and internal nodes structurally distinct. This is the OpenZeppelin standard, and the on-chain side verifies with the same scheme:

if (!MerkleProof.verify(proof, merkleRoot, leaf)) revert InvalidProof();

Amounts are raw wei

The amount in each leaf is the raw token amount (already multiplied by the token's decimals). The CSV you upload uses human units; the SDK converts using decimals() before building the tree. 100 tokens with 18 decimals becomes 100000000000000000000 in the leaf. Proof generation must use the exact same raw values — this is why claim pages read amounts from the distribution JSON rather than asking the user.

Proofs

A proof for a leaf is the list of sibling hashes on the path from that leaf to the root — log2(n) hashes, so even a million-recipient tree needs only ~20 hashes (~640 bytes of calldata) per claim. The contract recomputes the root from the leaf and the proof; if it matches the stored merkleRoot, the claim is genuine. It is computationally infeasible to forge a proof for an (address, amount) pair that was not in the original tree.

Why funds sit in the drop contract

When you fund your drop, the budget moves from your wallet into the drop contract itself — not into a shared pool, and never into a Droppie-controlled wallet:

  • Non-custodial. Droppie (the app, the site, the team) has no key that can move campaign funds. You deploy the contract, you send the funding transfer, and the only outflows the contract allows are valid claims and the creator's sweep.
  • Isolation. One campaign = one contract = one balance. Nothing is commingled.
  • Verifiable solvency. Recipients can check on-chain that the drop holds enough tokens before bothering to claim — and the app does exactly that, flagging underfunded drops instead of letting claims fail.
  • Deterministic wind-down. After endTime passes, the creator — and only the creator — can sweep whatever remains. If the drop has no deadline (endTime == 0), the creator can sweep at any time; see the sweep semantics before trusting a deadline-less drop.

The distribution JSON

The distribution file is the published snapshot of the recipient list — the off-chain half of the drop. Its metadataURI is stored on-chain by the drop contract. Schema (version 2):

type DistributionJson = {
version: 2;
chainId: number; // chain the drop lives on
token: `0x${string}`; // ERC-20 being distributed
merkleRoot: `0x${string}`; // must match the on-chain root
totalAmount: string; // sum of all amounts, raw wei, bigint as string
recipients: {
address: `0x${string}`;
amount: string; // raw wei, bigint as string
}[];
createdAt: string; // ISO 8601 timestamp
};

Example:

{
"version": 2,
"chainId": 8453,
"token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"merkleRoot": "0x2f9a5d3e8c41b7a90d6f4e2b1c8a7d5e3f6b9c0a1d2e3f4a5b6c7d8e9f0a1b2c",
"totalAmount": "392500000000000000000",
"recipients": [
{ "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", "amount": "100000000000000000000" },
{ "address": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", "amount": "250500000000000000000" },
{ "address": "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", "amount": "42000000000000000000" }
],
"createdAt": "2026-07-06T12:00:00.000Z"
}

Amounts are strings because they're bigints that exceed JavaScript's safe-integer range. Anyone holding this file can independently rebuild the tree with loadTreeFromDistribution and verify that the recomputed root matches the one on-chain — the file cannot lie about who gets what.

Trust model, summarized

PropertyEnforced by
Only listed addresses can claimMerkle proof verification on-chain
Each address claims exactly oncehasClaimed mapping
Exact amounts, no more, no lessAmount is part of the hashed leaf
Funds can't be redirectedclaim always transfers to the proven account
Creator can't change the list after launchRoot is a Solidity immutable, fixed at deployment
Droppie can't touch fundsEscrow holds tokens; no admin functions exist

Next: dig into the contract reference or the SDK.