DocsReference
Contracts
Two contracts, both in kontrakty/src, Solidity 0.8.24, EVM cancun, optimizer 1 000 000 runs, OpenZeppelin Ownable2Step, ReentrancyGuard and SafeERC20.
| Address on Robinhood Chain (4663) | |
|---|---|
Fabryka (registry) | 0x1CE8f5785617eaf9B6BB3c77CE7d242D86Dd0502, deployed at block 63645852 |
Kroplomierz (dropper) | one per airdrop; addresses come from Fabryka.lista() or the Utworzono event |
Both contracts use block.timestamp only. On this chain block.number is the Ethereum L1 block number, so nothing here depends on it.
The full ABI is exported in web/lib/abi.ts (ABI_FABRYKA, ABI_KROPLOMIERZ) and regenerated from Foundry artifacts with pnpm --dir web abi. Human-readable signatures are listed at the end of this page.
Fabryka (registry)#
A permissionless list of droppers plus each dropper's split rules and logo. It never holds or moves tokens: no transferFrom, no receive, no fallback. The worst a bug here can do is show a wrong list.
Structs#
struct Reguly {
uint256 kwota; // amount per drop, base units; 0 = not set yet
uint256 minSaldo; // minimum balance to qualify, base units
uint256 sufitNaAdres; // per-wallet cap per drop, base units; 0 = no cap
uint64 minCzasTrzymania; // minimum continuous holding time, seconds
uint64 okresBezruchu; // declared quiet period, seconds; 0 = not declared
bool porowno; // true = equal split; false = proportional to balance
bool pomijajKontrakty; // skip addresses with code
bool pomijajWlasne; // skip the dropper's owner and executor
}
struct Wpis {
address kroplomierz; // dropper
address token; // the ERC-20 it pays out
address wlasciciel; // owner AT REGISTRATION TIME; display only, never use for authorisation
uint64 utworzono; // block.timestamp of registration
string logo; // "" or https:// or ipfs://, at most 256 bytes
Reguly reguly;
}
The registry stores the rules but does not interpret them. Qualification and amounts are computed off chain by the split engine. okresBezruchu in particular is a declaration by the creator, not a lock: the dropper's wyplac stays unconditional.
Functions#
| Function | Access | What it does |
|---|---|---|
utworz(address token, address wykonawca, uint256 maksNaEpoke, uint256 minOdstep, string logo, Reguly reguly) returns (address kroplomierz) | anyone | Validates the logo, deploys new Kroplomierz(token, msg.sender, wykonawca, maksNaEpoke, minOdstep), records the entry, emits Utworzono (with przezFabryke = true), ZmianaRegul and ZmianaLogo. Constructor reverts of the dropper propagate unchanged (ZlyAdres for a token without code, ZerowyOdstep for a zero interval). |
zglos(address kroplomierz, string logo, Reguly reguly) | live owner() of that dropper | Registers a dropper deployed outside the registry. The address must have code, answer owner() and token(), and the token must have code. Emits the same three events with przezFabryke = false. |
ustawReguly(address kroplomierz, Reguly reguly) | live owner() | Replaces the rules. No validation beyond types; kwota == 0 is legal and means "not set". Emits ZmianaRegul. |
ustawLogo(address kroplomierz, string logo) | live owner() | Replaces the logo ("" removes it). Emits ZmianaLogo. |
ile() returns (uint256) | view | Number of entries. |
lista(uint256 od, uint256 ile) returns (Wpis[]) | view | Entries od .. od + ile - 1 in registration order. ile must be 1..100 and od < ile(), otherwise ZlyZakres. A range past the end is clamped, not rejected. |
wpis(address kroplomierz) returns (Wpis) | view | One entry. Reverts NieWpisany for an unknown address rather than returning zeros. |
czyWpisany(address kroplomierz) returns (bool) | view | Membership test. |
poTokenie(address token) returns (address[]) | view | All droppers of a token, in registration order. A token may have several. |
Authorisation always reads Kroplomierz(kroplomierz).owner() live. The stored Wpis.wlasciciel is a snapshot for display: after a two-step ownership transfer the old owner loses ustawReguly/ustawLogo immediately and the new one gains them without any registry transaction.
Events#
| Event | Emitted when |
|---|---|
Utworzono(address indexed kroplomierz, address indexed token, address indexed wlasciciel, uint64 utworzono, bool przezFabryke) | a dropper is created (utworz) or registered (zglos) |
ZmianaRegul(address indexed kroplomierz, Reguly reguly) | at registration and on every ustawReguly |
ZmianaLogo(address indexed kroplomierz, string logo) | at registration and on every ustawLogo |
Rules and logo are emitted at registration too, so an indexer gets the complete history from one event stream and never needs the initial state from a call.
Errors#
| Error | Meaning |
|---|---|
JuzWpisany(address) | already registered |
NieWpisany(address) | not registered |
NieWlasciciel(address kto, address wlasciciel) | caller is not the dropper's live owner |
ZlyAdres() | zero address or no code where code is required |
ZaDlugieLogo(uint256 dlugosc) | logo longer than 256 bytes |
ZlySchemat() | logo does not start with https:// or ipfs:// |
ZlyZakres() | lista arguments out of range |
Logo rules#
Empty, or https://…, or ipfs://…; at most 256 bytes; checked before the dropper is deployed so a bad logo does not burn the deployment gas. The web app validates the string a second time before rendering, rewrites ipfs:// to its own CID-only proxy, and falls back to a generated mark when the logo is missing or fails to load.
Kroplomierz (dropper)#
Holds one token's supply and pays it out in batches. It does not trust the executor; it limits the damage a hostile executor can do to a cap per epoch and a minimum interval, and gives the owner a pause and an unconditional exit.
Constructor#
constructor(address token_, address wlasciciel_, address wykonawca_, uint256 maks_, uint256 odstep_)
token_must have code (ZlyAdresotherwise): a typo in the token address would otherwise create a dropper that can never pay anything.wykonawca_may be the zero address; then nobody can drop until the owner callsustaw.maks_may be zero, which blocks drops more firmly than a pause.odstep_must be positive (ZerowyOdstep). With a zero interval the executor could open unlimited epochs in one block and the cap would mean nothing.ostatniaKroplastarts at zero, so the first epoch may open immediately after deployment.
Fabryka.utworz calls this constructor with wlasciciel_ = msg.sender.
State#
| Field | Type | Meaning |
|---|---|---|
token | IERC20 immutable | the token paid out |
wykonawca | address | the only address allowed to call kapnij |
wstrzymane | bool | paused: kapnij reverts; wyplac still works |
domknieta | bool | current epoch closed: further batches rejected |
maksNaEpoke | uint256 | cap on the sum of all batches in one epoch |
minOdstep | uint256 | minimum seconds between epoch openings |
epoka | uint256 | epoch counter; 0 = nothing dropped yet |
ostatniaKropla | uint256 | block.timestamp when the current epoch opened |
wydaneWEpoce | uint256 | intended amounts sent so far in the current epoch |
owner(), pendingOwner() | from Ownable2Step | owner and pending owner |
The balance is not stored: it is always token.balanceOf(address(this)). A token that charges a fee on transfer therefore does not corrupt any ledger; less simply arrives, and that is what the balance shows.
Functions#
| Function | Access | What it does |
|---|---|---|
zasil(uint256 ile) | anyone | safeTransferFrom(msg.sender, this, ile) after an approve. Emits Zasilenie. Zero reverts NicDoWyslania. A plain token.transfer to the dropper also works but emits no dropper event. |
kapnij(uint256 epokaId, address[] odbiorcy, uint256[] kwoty, bytes32 skrotRegul, bool domkniecie) | wykonawca only | Pays one batch. See the epoch rules below. Emits Kropla before transferring. |
wyplac(address doKogo, uint256 ile) | owner | Emergency exit. Works always, including while paused and mid-epoch. Emits Wyplata. |
ustaw(address wykonawca_, uint256 maks_, uint256 odstep_) | owner | Sets executor, cap and interval in one call. odstep_ == 0 reverts. Lowering the cap below wydaneWEpoce simply stops the current epoch from accepting more batches. Emits Ustawienia. |
ustawPauze(bool v) | owner | Pause or resume drops. Emits Wstrzymanie. |
transferOwnership(address) / acceptOwnership() | owner / pending owner | Two-step ownership transfer from OpenZeppelin. |
renounceOwnership() | owner | Always reverts (ZrzeczenieZabronione). An ownerless dropper with a live executor would drain itself at the cap rate with nobody able to pause or withdraw. To hand off control instead: ustaw(address(0), 0, odstep). |
stan() returns (uint256 saldo, uint256 doNastepnej, uint256 epokaTeraz) | view | Balance, seconds until a new epoch may open (0 = now), epoch counter. Arithmetic saturates so an absurd interval cannot make the read revert. |
Epoch rules in kapnij#
Let epoka be the current counter.
msg.sendermust bewykonawca(NieWykonawca), and the dropper must not be paused (Wstrzymano).odbiorcy.length == kwoty.length(NiezgodneDlugosci), non-empty (PustyWsad), no zero amount (ZerowaKwota(i)), no zero address (ZlyAdres).- Epoch selection:
epokaId == epoka + 1opens a new epoch. Requiresblock.timestamp >= ostatniaKropla + minOdstep(ZaWczesnie(now, earliest)). ResetswydaneWEpoceto the batch sum and setsostatniaKropla = block.timestamp.epokaId == epokacontinues the current epoch. No interval check. Reverts ifepoka == 0(nothing to continue) or if the epoch is closed (EpokaJuzDomknieta).- anything else reverts
ZlaEpoka(given, current).
- Cap:
wydaneWEpoce (or 0 for a new epoch) + sum(kwoty) <= maksNaEpoke, otherwisePrzekroczonySufit(afterBatch, cap). Checked arithmetic; overflow reverts. - State is written before any transfer; then
Kroplais emitted withskrotListy = keccak256(abi.encode(odbiorcy, kwoty))and the caller-suppliedskrotRegul; then the transfers run in order withsafeTransfer. domkniecie = truemarks the batch as the last one of the epoch.
The contract records intended amounts. It does not know what a fee-on-transfer token actually delivered and does not pretend to.
Events#
| Event | Emitted when |
|---|---|
Zasilenie(address indexed od, uint256 ile) | zasil |
Kropla(uint256 indexed epoka, uint256 odbiorcow, uint256 suma, bytes32 skrotListy, bytes32 skrotRegul, bool domkniecie) | every batch |
Ustawienia(address wykonawca, uint256 maksNaEpoke, uint256 minOdstep) | constructor and ustaw |
Wstrzymanie(bool wstrzymane) | ustawPauze |
Wyplata(address indexed doKogo, uint256 ile) | wyplac |
OwnershipTransferStarted, OwnershipTransferred | OpenZeppelin two-step ownership |
The recipients and amounts are not in the event; they are in the transaction calldata of kapnij, and skrotListy lets anyone check that the calldata they read is the list that was paid. See Reading the data.
Errors#
| Error | Meaning |
|---|---|
ZlyAdres() | zero address, or token without code |
NieWykonawca(address kto) | caller is not the executor |
Wstrzymano() | paused |
NiezgodneDlugosci(uint256 odbiorcow, uint256 kwot) | array length mismatch |
PustyWsad() | empty batch |
ZerowaKwota(uint256 indeks) | zero amount at index |
ZlaEpoka(uint256 podana, uint256 biezaca) | epoch id is neither current nor next |
EpokaJuzDomknieta(uint256 epoka) | batch sent to a closed epoch |
ZaWczesnie(uint256 teraz, uint256 najwczesniej) | interval not elapsed |
PrzekroczonySufit(uint256 poWsadzie, uint256 sufit) | epoch cap exceeded |
ZerowyOdstep() | interval of zero |
NicDoWyslania() | zero amount in zasil/wyplac |
ZrzeczenieZabronione() | renounceOwnership is disabled |
Trust boundaries in one table#
| Actor | Can | Cannot |
|---|---|---|
| Executor | send any list of recipients and amounts, up to maksNaEpoke per epoch, one epoch per minOdstep | exceed the cap by batching, open epochs faster than the interval, drop while paused, touch another token, change its own permissions, block the owner |
| Owner | withdraw everything at any time, pause, change executor/cap/interval, change rules and logo in the registry, transfer ownership in two steps | renounce ownership, make the dropper pay out without an executor |
| Anyone | fund a dropper, create and register droppers, read everything | change anything on someone else's dropper |
For the consequences of this model see Trust model.
Human-readable ABI#
Suitable for viem's parseAbi or cast.
Fabryka#
struct Reguly { uint256 kwota; uint256 minSaldo; uint256 sufitNaAdres; uint64 minCzasTrzymania; uint64 okresBezruchu; bool porowno; bool pomijajKontrakty; bool pomijajWlasne; }
struct Wpis { address kroplomierz; address token; address wlasciciel; uint64 utworzono; string logo; Reguly reguly; }
function utworz(address token, address wykonawca, uint256 maksNaEpoke, uint256 minOdstep, string logo, Reguly reguly) returns (address kroplomierz)
function zglos(address kroplomierz, string logo, Reguly reguly)
function ustawReguly(address kroplomierz, Reguly reguly)
function ustawLogo(address kroplomierz, string logo)
function ile() view returns (uint256)
function lista(uint256 od, uint256 ile) view returns (Wpis[])
function wpis(address kroplomierz) view returns (Wpis)
function czyWpisany(address kroplomierz) view returns (bool)
function poTokenie(address token) view returns (address[])
event Utworzono(address indexed kroplomierz, address indexed token, address indexed wlasciciel, uint64 utworzono, bool przezFabryke)
event ZmianaRegul(address indexed kroplomierz, Reguly reguly)
event ZmianaLogo(address indexed kroplomierz, string logo)
error JuzWpisany(address kroplomierz)
error NieWpisany(address kroplomierz)
error NieWlasciciel(address kto, address wlasciciel)
error ZlyAdres()
error ZaDlugieLogo(uint256 dlugosc)
error ZlySchemat()
error ZlyZakres()
Kroplomierz#
constructor(address token_, address wlasciciel_, address wykonawca_, uint256 maks_, uint256 odstep_)
function token() view returns (address)
function wykonawca() view returns (address)
function wstrzymane() view returns (bool)
function domknieta() view returns (bool)
function maksNaEpoke() view returns (uint256)
function minOdstep() view returns (uint256)
function epoka() view returns (uint256)
function ostatniaKropla() view returns (uint256)
function wydaneWEpoce() view returns (uint256)
function stan() view returns (uint256 saldo, uint256 doNastepnej, uint256 epokaTeraz)
function owner() view returns (address)
function pendingOwner() view returns (address)
function zasil(uint256 ile)
function kapnij(uint256 epokaId, address[] odbiorcy, uint256[] kwoty, bytes32 skrotRegul, bool domkniecie)
function wyplac(address doKogo, uint256 ile)
function ustaw(address wykonawca_, uint256 maks_, uint256 odstep_)
function ustawPauze(bool v)
function transferOwnership(address newOwner)
function acceptOwnership()
function renounceOwnership()
event Zasilenie(address indexed od, uint256 ile)
event Kropla(uint256 indexed epoka, uint256 odbiorcow, uint256 suma, bytes32 skrotListy, bytes32 skrotRegul, bool domkniecie)
event Ustawienia(address wykonawca, uint256 maksNaEpoke, uint256 minOdstep)
event Wstrzymanie(bool wstrzymane)
event Wyplata(address indexed doKogo, uint256 ile)
event OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner)
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner)
error ZlyAdres()
error NieWykonawca(address kto)
error Wstrzymano()
error NiezgodneDlugosci(uint256 odbiorcow, uint256 kwot)
error PustyWsad()
error ZerowaKwota(uint256 indeks)
error ZlaEpoka(uint256 podana, uint256 biezaca)
error EpokaJuzDomknieta(uint256 epoka)
error ZaWczesnie(uint256 teraz, uint256 najwczesniej)
error PrzekroczonySufit(uint256 poWsadzie, uint256 sufit)
error ZerowyOdstep()
error NicDoWyslania()
error ZrzeczenieZabronione()
Event topics and selectors#
| Signature | topics[0] / selector |
|---|---|
Utworzono(address,address,address,uint64,bool) | 0x75348e0d23f86823d7cd12353f869acbf731b5e25ac95482cfaeb942c11b244c |
ZmianaRegul(address,(uint256,uint256,uint256,uint64,uint64,bool,bool,bool)) | 0x19c025e33dc943814c1ae5443deb712510d0eb8929ceafa9c9a71e8fb41ed2d4 |
ZmianaLogo(address,string) | 0xc506a67dc1cd36eaa48d49a9b3f9893c176b818dbed3ce6a163057df48d77b27 |
Zasilenie(address,uint256) | 0x01d97b10ae37514d188ef9680fab5dbb0cbb7e58df4be1bf35042cfa405b4af7 |
Kropla(uint256,uint256,uint256,bytes32,bytes32,bool) | 0xffb1f0315c51a4f756dbace04c0f615e11acf808cef89efd5c3ae45f35bacb69 |
Ustawienia(address,uint256,uint256) | 0x3a97864a5135d5c628b21bb372484cc231779708127f78f339b5d6b053080261 |
Wstrzymanie(bool) | 0x756d12a2233d390bfdd29c7223eb2210e93ae18cd2fcd3b45a41f3d380f5911c |
Wyplata(address,uint256) | 0xec93ef6b2cb2d9cfa164f1f52786d03dffba8cfc273f0f5cc4216987a6802617 |
ERC-20 Transfer(address,address,uint256) | 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef |
utworz(...) | 0x06a9d8f1 |
zglos(...) | 0x5a6ec1df |
kapnij(...) | 0x5ebd5b0a |
zasil(uint256) | 0x1f181a16 |
stan() | 0xb801a804 |
lista(uint256,uint256) | 0xecb53825 |
Quick reads with cast#
RPC=https://rpc.mainnet.chain.robinhood.com
FABRYKA=0x1CE8f5785617eaf9B6BB3c77CE7d242D86Dd0502
# how many droppers are listed
cast call $FABRYKA "ile()(uint256)" --rpc-url $RPC
# the first page of entries, rules included
cast call $FABRYKA \
"lista(uint256,uint256)((address,address,address,uint64,string,(uint256,uint256,uint256,uint64,uint64,bool,bool,bool))[])" \
0 100 --rpc-url $RPC
# state of one dropper: balance, seconds to next epoch, epoch counter
cast call <DROPPER> "stan()(uint256,uint256,uint256)" --rpc-url $RPC