Skip to main content

Contracts overview

The Droppie protocol is two small contracts, built with Foundry, Solidity ^0.8.24, and OpenZeppelin v5:

ContractRole
DroppieDropOne airdrop campaign — escrows the tokens, verifies Merkle proofs, pays claims
DroppieERC20"Token Alchemist" — a no-code ERC-20 with optional mint/burn

Direct-deploy architecture

There are no factories, no proxies, and no pre-deployed infrastructure. Every campaign is its own plain contract, deployed straight from the creator's wallet:

  • The deployer is the creator. The wallet that sends the deployment transaction becomes creator — the only address allowed to sweep. There is no creator parameter and no third party in the loop; you own the contract from block one.
  • Parameters are immutable. token, merkleRoot, startTime, and endTime are Solidity immutables baked into the runtime code at deployment; metadataURI (a dynamic string) is set in the constructor and has no setter. There is no initialize, no admin, no upgrade path — what you deploy is what runs forever.
  • Funding is a separate, plain transfer. After deployment the creator sends token.transfer(drop, totalAmount) — no approve, no transferFrom. Until the transfer lands the drop is simply underfunded (details).
  • Any EVM chain works. Because nothing needs to be pre-deployed, Droppie runs on every configured chain out of the box. The creation bytecode the app deploys ships inside @droppie/sdk as droppieDropBytecode and droppieErc20Bytecode.

Every campaign is fully isolated: one drop = one contract = one token balance. A bug or sweep in one campaign can never touch another's funds.

DroppieDrop constructor

constructor(
IERC20 token_,
bytes32 merkleRoot_,
uint64 startTime_, // 0 = immediate
uint64 endTime_, // 0 = no deadline
string memory metadataURI_ // distribution JSON (ipfs://, https://, data:)
)
ParameterDescription
token_The ERC-20 being distributed. Must be non-zero.
merkleRoot_Root of the recipients tree (leaf scheme). Must be non-zero.
startTime_Unix timestamp claims open. 0 means claims open immediately.
endTime_Unix timestamp claims close. 0 means no deadline. If non-zero, must lie strictly after both startTime_ and the current block timestamp.
metadataURI_Where the distribution JSON lives.

Reverts with ZeroAddress, ZeroMerkleRoot, or InvalidTimeWindow if validation fails. See the full DroppieDrop reference for claim, sweep, views, events, and errors.

DroppieERC20 — the Token Alchemist

The Token Alchemist (/app/tokens in the app) deploys production-grade ERC-20s without writing Solidity. It's a single OpenZeppelin v5-based contract with 18 decimals and two immutable feature flags — pick mintable and/or burnable at deploy time. Like the drop, it's deployed directly by your wallet; it modernizes the legacy four-variant token deployer into one parameterized contract.

constructor(
string memory name,
string memory symbol,
uint256 premintWei, // RAW wei — 1 token = 10^18
address owner_, // mint authority; receives the premint
bool mintable_,
bool burnable_
)
ParameterDescription
name / symbolStandard ERC-20 metadata.
premintWeiInitial supply in raw wei, minted to owner_. May be zero.
owner_Token owner (mint authority). The app passes your connected address.
mintable_Whether the owner may mint after deployment. Immutable.
burnable_Whether holders may burn. Immutable.
Premint is raw wei

premintWei is the raw amount, already scaled by 18 decimals — the frontend does the multiplication. This fixes the legacy deployer's unit asymmetry, where some variants took whole tokens and others took wei. To premint 1,000,000 tokens, pass 1000000 * 10^18.

Functions

FunctionAccessBehavior
mint(address to, uint256 amount)onlyOwnerMints amount (raw wei) to to. Reverts NotMintable() if the token was deployed with mintable_ = false.
burn(uint256 amount)Anyone (own balance)OZ ERC20Burnable semantics. Reverts NotBurnable() if burnable_ = false.
burnFrom(address account, uint256 amount)Requires allowanceOZ ERC20Burnable semantics. Reverts NotBurnable() if burnable_ = false.
Standard ERC-20transfer, approve, transferFrom, balanceOf, allowance, totalSupply, name, symbol, decimals (always 18)

Errors

error NotMintable();
error NotBurnable();

The flags are fixed at deployment. A token deployed with mintable_ = false has a hard supply cap at its premint — no owner, including the deployer, can ever mint more.

Deploying outside the app

The app deploys via deployContract with the SDK's bytecode, but nothing about the contracts is app-specific — forge create works just as well:

cd packages/contracts
forge build

# deploy a drop: token, merkle root, startTime, endTime, metadataURI
forge create src/DroppieDrop.sol:DroppieDrop \
--rpc-url $RPC_URL \
--private-key $CREATOR_KEY \
--broadcast \
--constructor-args \
0xYourTokenAddress \
0xYourMerkleRoot \
0 0 \
"ipfs://.../distribution.json"

# fund it — one plain transfer, no approve
cast send 0xYourTokenAddress \
"transfer(address,uint256)" 0xTheNewDropAddress $TOTAL_AMOUNT \
--rpc-url $RPC_URL --private-key $CREATOR_KEY

The wallet behind $CREATOR_KEY becomes the drop's creator. Deploying a token works the same way with src/DroppieERC20.sol:DroppieERC20 and its six constructor args.

Security posture

  • Contracts follow OpenZeppelin v5 primitives (SafeERC20, MerkleProof, ERC20Burnable, Ownable).
  • The full Foundry test suite covers claim happy paths, double-claim and wrong-proof reverts, window enforcement, sweep authorization and timing, underfunded-drop behavior, campaign isolation, token flags, and fuzzing on amounts.
  • The contracts have not undergone a third-party audit. See the FAQ before committing significant value.