DocsGuides

Create a dropper

How to put a token's supply on drip: from the web app in three signatures, or from code with two contracts and four transactions. Both end in the same place, a dropper listed in the registry with rules on chain and a keeper that fires the drops.

From the web app#

The Add airdrop screen (/launch) does everything a creator needs:

  1. Connect the wallet that holds the supply. It signs everything and becomes the dropper's owner.
  2. Pick a coin from the list of tokens the wallet holds (logo, symbol, balance and share of supply are read from chain), or paste a contract address.
  3. Giving away: an amount or a percentage of your balance (slider), truncated to whole tokens; drops: how many drops it is split into; every: the interval. The amount per drop and the worst case per day are shown before anything is signed.
  4. Who gets it: a live preview of the qualifying holders under the chosen rules, refreshed every 30 s from a holder snapshot. Pools, the bonding curve and your own wallet are already excluded by the default rules. More rules opens minimum balance, minimum holding time, equal split, per-wallet cap and the quiet period.
  5. Airdrop it sends three transactions in a row, each shown with its state:
    1. Fabryka.utworz(token, executor, capPerEpoch, interval, logo, rules): creates and lists the dropper. The executor defaults to the plop keeper (0x5e25b93C6454C10E4110B70DAFa76C6934cDee03); the cap per epoch is set equal to the amount per drop; the logo is prefilled from the token's logo() if it has one.
    2. token.approve(dropper, amount).
    3. Kroplomierz.zasil(amount): moves the supply into the dropper.

From that moment the keeper picks the dropper up on its next pass (every 15 s), and the coin appears on the dashboard under Dripping.

If the token already has a dropper that ran dry, the same screen refunds it instead of creating a second one: it tops the balance up and, if the rules or the cap/interval differ from what you chose, sends ustawReguly and ustaw as well.

The coin page (/moneta/<dropper>) later shows the owner an Add supply panel (approve + zasil), and an Automate the drops button when the dropper's executor is not the keeper.

From code#

1. Choose the numbers#

ParameterWhere it livesGuidance
kwota (per drop)rules, registrytotal to give away ÷ number of drops, in base units
maksNaEpokedropperat least kwota, or the drop reverts with PrzekroczonySufit. The web app uses exactly kwota.
minOdstepdropperseconds between drops; must be ≥ 1. The keeper fires as soon as it is allowed.
minSaldorulesdust filter; 0 lets any positive balance qualify
minCzasTrzymaniarulesseconds of continuous holding; 0 counts what is held at the snapshot
sufitNaAdresrulesper-wallet cap per drop, 0 for none; overflow is redistributed
porownorulesfalse (proportional) unless you specifically want an equal split, which invites sybils
pomijajKontraktyrulestrue: pools, routers and the bonding curve get nothing
pomijajWlasnerulestrue: your own wallet and the executor get nothing; without it a fresh creator holding most of the supply receives most of the first drop
okresBezruchurulesoptional declaration: after this many seconds with no drop you intend to take the rest back. Not enforced; 0 = not declared
logoregistry"", https://… or ipfs://…, ≤ 256 bytes
executordropperthe keeper's address, or your own executor (see Keeper and executor). The zero address means nobody can drop.

Worst case per day if the executor turns hostile: maksNaEpoke × (floor(86400 / minOdstep) + 1) base units, bounded by the dropper's balance. Choose values you can afford to lose.

2. Create and list the dropper#

import { createPublicClient, createWalletClient, http, parseAbi, parseUnits, custom } from 'viem'

const REGISTRY = '0x1CE8f5785617eaf9B6BB3c77CE7d242D86Dd0502'
const KEEPER   = '0x5e25b93C6454C10E4110B70DAFa76C6934cDee03'
const registryAbi = parseAbi([
  'struct Reguly { uint256 kwota; uint256 minSaldo; uint256 sufitNaAdres; uint64 minCzasTrzymania; uint64 okresBezruchu; bool porowno; bool pomijajKontrakty; bool pomijajWlasne; }',
  'function utworz(address token, address wykonawca, uint256 maksNaEpoke, uint256 minOdstep, string logo, Reguly reguly) returns (address kroplomierz)',
  'event Utworzono(address indexed kroplomierz, address indexed token, address indexed wlasciciel, uint64 utworzono, bool przezFabryke)',
])

const decimals = 18                                    // read it from the token, never assume it
const amountPerDrop = parseUnits('1000', decimals)    // 1 000 tokens per drop

const { request } = await publicClient.simulateContract({
  address: REGISTRY,
  abi: registryAbi,
  functionName: 'utworz',                              // create
  args: [
    token,            // ERC-20 address, must have code
    KEEPER,           // executor
    amountPerDrop,    // maksNaEpoke: cap per epoch
    3600n,            // minOdstep: one drop per hour
    '',               // logo
    {                 // Reguly (rules); field names are the contract's
      kwota: amountPerDrop,                       // amount per drop
      minSaldo: parseUnits('1', decimals),        // minimum balance
      sufitNaAdres: 0n,                           // per-wallet cap, 0 = none
      minCzasTrzymania: 300n,                     // minimum holding time, seconds
      okresBezruchu: 0n,                          // declared quiet period, 0 = none
      porowno: false,                             // false = proportional split
      pomijajKontrakty: true,                     // skip contracts
      pomijajWlasne: true,                        // skip owner and executor
    },
  ],
  account,
})
const hash = await walletClient.writeContract(request)
const receipt = await publicClient.waitForTransactionReceipt({ hash })

// the new dropper's address is in the Utworzono (created) event, or in the return value of the simulation
const { parseEventLogs } = await import('viem')
const [created] = parseEventLogs({ abi: registryAbi, eventName: 'Utworzono', logs: receipt.logs })
const dropper = created.args.kroplomierz

With cast:

cast send $REGISTRY \
  "utworz(address,address,uint256,uint256,string,(uint256,uint256,uint256,uint64,uint64,bool,bool,bool))" \
  $TOKEN $KEEPER 1000000000000000000000 3600 "" \
  "(1000000000000000000000,1000000000000000000,0,300,0,false,true,true)" \
  --rpc-url https://rpc.mainnet.chain.robinhood.com --private-key $PK

3. Fund it#

const erc20Abi = parseAbi(['function approve(address spender, uint256 amount) returns (bool)'])
const dropperAbi = parseAbi(['function zasil(uint256 ile)'])   // fund

const total = amountPerDrop * 24n   // 24 drops
await walletClient.writeContract({ address: token, abi: erc20Abi, functionName: 'approve', args: [dropper, total], account })
await walletClient.writeContract({ address: dropper, abi: dropperAbi, functionName: 'zasil', args: [total], account })

Anyone may call zasil on any dropper. A plain transfer to the dropper also funds it, but emits no Zasilenie event, so balance reconstructions from events will be off by that amount.

4. Confirm#

cast call $DROPPER "stan()(uint256,uint256,uint256)" --rpc-url $RPC   # balance, seconds to next epoch (0 = now), epochs so far
cast call $DROPPER "wykonawca()(address)" --rpc-url $RPC

If wykonawca() is the keeper's address, the dropper is paused-free and the balance covers at least one kwota, the first drop goes out on the keeper's next pass.

Register a dropper you deployed yourself#

A Kroplomierz deployed by hand (kontrakty/script/Wdroz.s.sol or your own deployment) is not listed until its live owner calls:

Fabryka.zglos(dropper, logo, rules)

The registry checks that the address has code, that owner() returns the caller and that token() points at a contract. The entry gets przezFabryke = false in Utworzono, which tells indexers that the dropper has history from before its registration.

Manage a dropper#

TaskCallWho
Change the rulesFabryka.ustawReguly(dropper, rules)live owner
Change the logoFabryka.ustawLogo(dropper, uri)live owner
Change executor, cap or intervalKroplomierz.ustaw(executor, cap, interval)owner
Pause / resume dropsKroplomierz.ustawPauze(true / false)owner
Take the remainder backKroplomierz.wyplac(to, amount)owner, any time
Hand the dropper to another wallettransferOwnership(new) then acceptOwnership() from the new walletowner, then new owner
Retire the dropper but keep the exitustaw(0x0, 0, interval)owner
Top upapprove + zasilanyone

Changing the rules takes effect at the next drop; the keeper reads the registry on every pass. Changing kwota without raising maksNaEpoke makes the next drop revert with PrzekroczonySufit, so change both together (the web app's Airdrop it on an existing dropper does).

Tokens that behave unusually#

  • Fee on transfer / rebasing. The dropper records intended amounts and reads its balance live. Funding arrives net of the fee; recipients receive net of the fee. The API's what I received figure is computed from the token's Transfer events, not from Kropla sums, for this reason.
  • No name(). Fine; the dashboard shows the symbol alone.
  • No symbol(), decimals() or totalSupply(). The web app refuses to draw the coin because it could not show one honest amount. The contracts do not care.
  • Several droppers for one token. Allowed by the registry (poTokenie lists them all). The web app discourages it: a token that already has a dropper shows has a dropper and links to it instead.