Skip to main content

Messages and queries

JSON is snake_case. Timestamps are nanosecond strings (Timestamp), amounts are Uint128 strings, assets are {"native":{"denom":"..."}} or {"cw20":{"address":"..."}}. Unknown fields are rejected. The authoritative interface is the committed JSON schema of each contract (v0.1.0).

Campaign ticket​

The API signs a CampaignTicket (ticket version v = 2); the owner submits it with the funds.

{"v":2,"chain_id":"safrochain-1","factory":"addr_safro1…","campaign_id":"6f1c2a9e-…","funding_attempt":1,
"owner":"addr_safro1…","asset":{"native":{"denom":"ibc/1E18…5C38"}},"min_funding":"1000000000",
"starts_at":"1760000000000000000","ends_at":"1762000000000000000","fee_bps":0,"min_payout":"10000",
"max_share_bps":10000,"challenge_secs":86400,"commit_window_secs":1209600,"claim_window_secs":15552000,
"escrow_code_id":2,"escrow_admin":"addr_safro1…","expires_at":"1760003600000000000"}
  • ticket is the base64 of those exact bytes; signature is the base64 of a 64-byte compact secp256k1 signature r ‖ s over sha256(ticket bytes), low-S only.
  • The factory checks, in order: no global pause → signature → JSON → v == 2 → chain id → factory address → escrow code id and admin equal its configuration → owner is the funder → not expired → campaign id [a-z0-9-]{1,64} → attempt ≥ 1 → starts_at < ends_at, ends_at in the future, start at most 365 days away, duration at most 365 days → fee ≤ max_fee_bps → max_share_bps 1–10 000 → windows within the limits → funding ≥ min_funding in exactly the ticket's asset → funding-attempt rules (attempt n + 1 only after attempt n was cancelled).

Factory​

Instantiate​

owner, operator, ticket_signer (base64 33-byte compressed public key), pauser, guardian, treasury, escrow_code_id, escrow_admin (or null = immutable instances), max_fee_bps (≤ 1 000), limits, cw20_code_checksums (≤ 20, empty = CW20 campaigns refused).

limits: min_challenge_secs/max_challenge_secs (≤ 7 d), min_commit_window_secs/ max_commit_window_secs (≤ 90 d), min_recommit_secs (≤ 14 d), min_claim_window_secs/ max_claim_window_secs (≤ 365 d), dispute_timeout_secs (≤ 14 d), max_pause_secs (≤ 7 d), max_commit_batch (≤ 2 000), max_distribute_batch (≤ 1 000). Mainnet defaults: challenge 1 h–7 d, commit window 5–90 d, re-commit ≥ 3 d, claim window 30–365 d, dispute timeout 3 d, max pause 1 d, commit batch 1 000, distribute batch 250.

Execute​

MessageWhoEffect
create_campaign { ticket, signature } + exactly one cointhe ticket's ownerverifies the ticket, records the registry entry, instantiates the escrow (instantiate2) with the funds
receive(Cw20ReceiveMsg) with {"create_campaign":{"ticket","signature"}}an approved CW20same, CW20 pool (the CW20 sender must be the owner)
update_config(ConfigUpdate)ownerrotate roles (immediate everywhere), change escrow code id or admin, max fee, limits, CW20 checksums (new campaigns only). Rotating the ticket signer, code id or admin invalidates unused tickets
pause { until }pauser (rate-limited), guardian, ownerglobal pause, at most max_pause_secs from now, never shortens one
unpause {}guardian or owner; anyone once expired (no-op)
rescue_funds { assets, recipient }ownerreturns coins or tokens sent to the factory by mistake (1–10 assets)
transfer_ownership { new_owner, expiry? }, accept_ownership {}, cancel_ownership_transfer {}owner / pending ownertwo-step ownership, no renounce
sync_campaign_owner { campaign_id }anyonere-reads an escrow's owner (escrows call it after an ownership transfer)

Queries​

QueryResponse
config {}configuration, paused_until, global_pause { start, until, kind, totals_before }
pause_state {}{ paused, paused_until, kind }
pause_totals { at }global paused nanoseconds by kind accumulated by at
campaign { campaign_id }latest attempt: { campaign_id, funding_attempt, escrow, owner, asset, created_at, code_id }
campaign_attempts { campaign_id, start_after?, limit? }every attempt of a campaign
campaigns { start_after?, limit? }, campaigns_by_owner { owner, … }latest attempts, paged (30, max 100)
ownership {}{ owner, pending_owner, pending_expiry }

Campaign escrow​

Instantiated by the factory only, with the ticket's parameters, the funding_attempt, a copy of the factory limits and, for CW20, cw20_funding.

Execute​

MessageWhoPhasePaused?Effect
top_up {} + one coin / CW20 receive {"top_up":{}}anyoneupcoming, activeblocksadds to funded_total
cancel {}owner before starts_at; guardian before finalizenot finalizednowhole balance to the owner, no fee
commit_allocations { version, entries }operatorended, committingblocks1–max_commit_batch entries { address, points }, addresses strictly increasing bytewise (also across batches), not privileged, points > 0
reset_allocations { version }operatorended, committingblocksdiscards the commitments, version + 1
finalize_allocations { version, expected_count, expected_total_points, allocation_sha256, allocation_uri }operatorended, committingblockssnapshot (balance, fee reserve, distributable, cap, treasury), opens the challenge window
dispute { reason }ownerchallengenoonce per version, 1–280 characters, never refunds
reject_allocations { reason }guardianchallenge, disputed (never after a payout)nodiscards the allocation, version + 1, back to ended, re-commit time granted (at most 2 extensions)
resolve_dispute { decision }guardiandisputedno"keep_allocation" (payouts start) or "reject_allocation" (as reject)
distribute { limit? }anyonedistributingblockspays the next entries (≤ max_distribute_batch); the call that finds nothing left completes: fee, then leftover refund
claim {}a recipientdistributing (pending entry); distributing or completed before the claim deadline (failed entry)blockspays the sender's own entry
retry_failed { addresses }anyonedistributing, completed, before the claim deadlineblocksre-sends failed payouts (1–100)
reclaim_expired {}anyoneexpirednowhole balance to the owner
sweep_leftover {}anyonecancelled, or completed after the claim deadlinenoremaining balance to the owner
refund_stray_assets { assets }anyoneanynoassets other than the pool asset to the owner (1–10)
pause { until } / unpause {}pauser (rate-limited, budget) or guardianany—instance pause; stops the escrow clock
transfer_ownership, accept_ownership, cancel_ownership_transferowner / pending ownerbefore starts_at (cancel: any)notwo-step; accept also syncs the factory registry

Every message except top_up (and create_campaign on the factory) is non-payable.

Queries​

QueryResponse
config {}parameters, funding_attempt, owner, factory, asset, created_at, limits (creation-time copy), roles { operator, pauser, guardian, treasury } (null if the factory cannot be read)
status {}campaign_id, funding_attempt, phase, funded_total, balance, version, entry_count, total_points, last_committed, finalized { at, challenge_ends_at, claim_deadline, gross_balance, max_fee, distributable, cap, treasury, allocation_sha256, allocation_uri }, dispute { raised_at, reason, expires_at, resolved_at }, paid_count, paid_total, failed_count, failed_outstanding, skipped_count, skipped_total, fee_paid, refunded_total, cursor, deadlines { starts_at, ends_at, commit_deadline, claim_deadline }, paused, paused_until, frozen_secs, pauser_pause_left_secs, completed_at, cancelled_at
allocation { address, version? }{ address, version, points, amount, status } — amount is null before finalize, projected while pending
allocations { start_after?, limit?, version? }{ version, allocations }, ascending by address (30, max 100); rejected versions stay readable
ownership {}{ owner, pending_owner, pending_expiry }

phase is one of upcoming, active, ended, committing, challenge, disputed, distributing, completed, cancelled, expired. Deadlines are real times projected from the escrow clock; re-read status {} for countdowns, since a pause moves them.

Allocation file​

allocation_sha256 is the SHA-256 (hex) of the canonical text: one <address>,<weight> line per committed entry, addresses lowercase and sorted bytewise, each line ending with \n. The contract records it but cannot check it; anyone can, by paging allocations {}. Dyma serves the file at GET /v1/discover/campaigns/:campaignId/allocation.json (schema dyma.escrow.allocation/v1; Dyma commits planned payouts as weights, weight_unit: "adjusted").