DocsStart
Overview
plop is three components and one chain.
creator's wallet holders' wallets
│ approve + zasil (fund) ▲
▼ │ token.transfer, one per recipient
┌─────────────────────┐ kapnij(list) ┌────┴───────────┐
│ Kroplomierz │ ◄────────────────┤ keeper │ reads the registry every
│ (dropper) │ │ (executor key)│ 15 s, snapshots holders,
│ holds the supply │ Kropla event │ │ applies the rules, sends
│ caps every epoch ├────────────────► │ │ the batches, keeps a journal
└─────────┬───────────┘ └────────────────┘
│ listed in, rules stored in ▲
▼ │ eth_getLogs Transfer(token)
┌─────────────────────┐ ┌────────┴───────┐
│ Fabryka │ lista(), wpis() │ web app │ dashboard, coin page,
│ (registry) ├────────────────► │ Next.js + API │ launch flow, JSON API,
│ never holds tokens │ │ │ same rules engine as keeper
└─────────────────────┘ └────────────────┘
| Component | Where | Role |
|---|---|---|
Kroplomierz ("dropper") | kontrakty/src/Kroplomierz.sol, one instance per airdrop | Holds one token's supply. Pays out batches when its executor calls kapnij. Enforces a hard cap per epoch, a minimum interval between epochs, a pause switch and an owner-only withdrawal. |
Fabryka ("registry") | kontrakty/src/Fabryka.sol, one instance per chain | Deploys droppers (utworz), lists them, stores each dropper's split rules and logo. Never holds tokens. Permissionless. |
| Keeper | keeper/fabryka.mjs | The executor. Reads the registry, and for every dropper whose executor is its key: snapshots holders, computes the split, sends the batches, journals every step. |
| Web app | web/ (Next.js 15, viem) | Dashboard of all droppers, per-coin page with rules, holders and drop history, one-click launch, and a JSON API that other software can consume. |
Lifecycle of an airdrop#
- Create. The creator calls
Fabryka.utworz(token, executor, capPerEpoch, minInterval, logo, rules). The registry deploys a new dropper owned by the caller and lists it. In the web app this is the first of three signatures on the Add airdrop screen. - Fund. The creator approves the dropper and calls
Kroplomierz.zasil(amount). Anyone can fund any dropper; funding is a gift to the holders. The dropper's balance is alwaystoken.balanceOf(dropper); there is no internal ledger. - Drip. Every
minOdstepseconds the executor may open a new epoch by callingkapnij(epoch + 1, recipients, amounts, rulesHash, close). Large lists are split into several batches in the same epoch; all batches count towards the same cap. The dropper emitsKroplafor every batch. - Repeat until the balance is smaller than one drop. The web app then shows the coin as empty. The creator can top it up at any time (Add supply).
- Stop or take back. The owner can pause drips (
ustawPauze), change the executor, cap and interval (ustaw), or withdraw the remainder (wyplac) at any time. The rules may declare a quiet period after which the creator intends to take the rest back; the contract does not enforce it.
Who qualifies for a drop#
Qualification is computed from a snapshot of the token's holders at a block, using the rules stored in the registry:
- balance at the snapshot block must be at least
minSaldo; - the balance must have stayed at or above that threshold continuously for at least
minCzasTrzymaniaseconds (dipping below resets the clock); - contracts are skipped when
pomijajKontraktyis set (pools, routers, the bonding curve); - the dropper's owner and executor are skipped when
pomijajWlasneis set; - the dropper itself is always skipped;
- the drop is split proportionally to balance, or equally when
porownois set, with an optional per-wallet capsufitNaAdreswhose overflow is redistributed.
The exact algorithm, including rounding and ordering, is specified in Split rules. The web app and the keeper run the same code (web/lib/regula.ts), so the list shown on the coin page is the list the keeper would send.
Epochs, batches and the cap#
The dropper does not know who should receive tokens; it only limits how much can leave and how often:
maksNaEpokeis a hard cap on the sum of all batches of one epoch. Splitting a drop into a hundred calls does not raise it.minOdstepis the shortest allowed time between two epoch openings. Zero is rejected by the constructor and byustaw.kapnijwithepokaId == epoka + 1opens a new epoch (interval enforced); withepokaId == epokait continues the current one (no interval check) unless it was closed; anything else reverts.
Worst case, with a hostile executor, at most maksNaEpoke × (floor(86400 / minOdstep) + 1) base units can leave the dropper per day. The creator chooses both numbers; the Add airdrop screen shows the resulting figure before anything is signed.
Time and numbers on this chain#
Facts measured on Robinhood Chain that shape the code and the docs:
- Blocks are about 100 ms apart, produced by a single sequencer, first come first served, no public mempool and therefore no priority-fee auction. The keeper sends plain legacy transactions with
gasPricefrom the node. block.numberreturns the Ethereum L1 block number, as on every Arbitrum Orbit chain. The contracts useblock.timestampexclusively.blockTimestampineth_getLogsresults can be0x0. Every timestamp in the API is fetched from the block header instead.eth_getLogsis limited: roughly 50 000 to 100 000 blocks per query, at most 10 000 logs per response, and addresses × blocks must stay at or below 200 000. A filter without an address counts as five addresses. Over-use answers429without aRetry-Afterheader; the node recovers in about 0.7 s.- The public RPC is not an archive node. Historical
eth_getCodeand state reads need an archive endpoint; the web app keeps such an endpoint server-side and never exposes it.
Repository layout#
kontrakty/ Foundry project: Kroplomierz.sol, Fabryka.sol, tests, deploy scripts
web/ Next.js app (App Router), viem, TypeScript
lib/regula.ts split engine (pure, shared with the keeper)
lib/holdery.ts holder index built from Transfer logs
lib/fabryka.ts registry reads and rule conversions
app/api/** JSON routes, see HTTP API
components/dto.ts JSON shapes of every route
keeper/ executor: fabryka.mjs (all droppers), harmonogram.mjs (one dropper)
docs/ these pages
Glossary#
The contracts, the API and the code use Polish identifiers. This table is the bridge.
Contracts and roles#
| Identifier | Meaning |
|---|---|
Fabryka | the registry contract ("factory") |
Kroplomierz | a dropper contract ("drip meter"); one per airdrop, one token each |
kropla / Kropla | a drop; the event emitted for every batch |
kapnij | fire a drop (executor only) |
epoka | epoch: one drop, possibly several batches, capped as a whole |
wsad | a batch: one kapnij transaction |
wykonawca | executor: the only address allowed to call kapnij |
wlasciciel / owner() | owner of a dropper (two-step Ownable2Step) |
zasil / Zasilenie | fund the dropper / the funding event |
wyplac / Wyplata | owner withdrawal / its event |
ustaw / Ustawienia | owner sets executor, cap and interval / its event |
ustawPauze / wstrzymane / Wstrzymanie | pause switch / paused flag / its event |
domknieta / domkniecie | epoch closed flag / "this batch closes the epoch" |
maksNaEpoke | hard cap per epoch, base units |
minOdstep | minimum interval between epoch openings, seconds |
ostatniaKropla | timestamp at which the current epoch was opened |
wydaneWEpoce | sum of intended amounts sent in the current epoch |
stan() | (saldo, doNastepnej, epokaTeraz): balance, seconds until a new epoch may open, epoch counter |
utworz / Utworzono | create and list a dropper / the listing event |
zglos | register a dropper deployed outside the registry |
ustawReguly / ZmianaRegul | set split rules / the rules-changed event |
ustawLogo / ZmianaLogo | set logo URI / the logo-changed event |
lista, ile, wpis, czyWpisany, poTokenie | list entries, count, one entry, is-registered, droppers of a token |
Wpis | a registry entry |
przezFabryke | "created through the registry" flag on Utworzono |
utworzono | registration timestamp |
Rules#
| Identifier | Meaning |
|---|---|
Reguly / reguly | the split rules struct |
kwota | amount per drop, base units; 0 means "not set yet" |
minSaldo | minimum balance to qualify, base units |
sufitNaAdres | per-wallet cap per drop, base units; 0 means no cap |
minCzasTrzymania | minimum continuous holding time, seconds |
okresBezruchu | declared quiet period, seconds; 0 means not declared |
porowno | equal split (true) instead of proportional (false) |
pomijajKontrakty | skip contract addresses |
pomijajWlasne | skip the dropper's owner and executor |
wykluczone | manual exclusion list (off chain, keeper-side) |
podzial | split mode in the engine: proporcjonalnie or porowno |
skrotRegul | keccak256 of the canonical rules text |
skrotListy | keccak256 of abi.encode(address[] recipients, uint256[] amounts) |
przydzialy / odrzuceni / reszta | allocations / rejected holders with reasons / remainder left in the dropper |
Data#
| Identifier | Meaning |
|---|---|
migawka | holder snapshot |
holdery / saldo / odKiedy / pierwszyRaz / kontrakt | holders / balance / held-since timestamp / first-acquired timestamp / is a contract |
podaz | totalSupply |
pelna | the scan is complete; false means a gap |
powod / klopot / blad | reason / trouble note / error message |
czas / blok / doBloku / odBloku | block-header time / block number / scan upper bound / scan lower bound |
doNastepnej | seconds until a new epoch may open |
zakladka | dashboard tab: kapie dripping, pauza paused, puste empty, wszystkie all |
szukaj / strona / swiezo | search / page / bypass the server cache |
dostalem | "what I received" |
moje-tokeny | tokens held by a wallet |
obraz | image (IPFS proxy) |