@mallow-labs/mallow-sweepstake-sdk
v0.1.1
Published
Kit SDK and independently verifiable draw tooling for mallow sweepstakes
Readme
@mallow-labs/mallow-sweepstake-sdk
Kit-native bindings and backend-free verification for the mallow Sweepstake program (draw algorithm v2). The package does not contain an Anchor 0.29 client or a handwritten instruction serializer.
The public surface has three layers:
- pure round, drand, transcript and shuffle functions from the package root;
- a structurally independent draw and seed implementation at
./independentfor cross-checks; - Codama-generated Kit accounts, instructions, events and PDA helpers at
./generated, derived from the vendored real Anchor IDL.
How a draw works (v2)
create_sweepstakecommits the setup identity, the terms and the drand evmnet round: the first round at or after entry close plus 30 seconds (roundForCloseTime, ceiling arithmetic; tyki'sevmnetRoundAtfloors and must not be used).- Anyone calls
settle_drawwith the 64-byte evmnet signature for that round. The program verifies the BLS signature itself, freezesn = next_indexandk = min(n, item_count), and storesseed = sha256("MALLOW_SWEEPSTAKE_SEED_V2" || program || sweepstake || candy_machine || setup_id || chain_hash || u64_le(round) || sha256(signature) || u32_le(n) || u32_le(k))(233-byte preimage). A settled seed can never be replaced; there is no Tyki request or Beacon account and no rent for either. - Anyone calls
resolve_entry. With no proof, the program evaluates a swap-or-not permutation of[0, n)(the Ethereum consensuscompute_shuffled_indexstructure, 90 rounds, exact rejection-sampled pivots, at most 4 attempts, never a modulo fallback). An entry wins iffshuffle(seed, n, index) < k, so exactlykentries win.
The permutation is pseudorandom, not an exactly uniform subset sampler.
Admission (mallow co-signs entries), delivery availability, the creator's
machine and guard authority, drand, the upgrade authority and token issuers
remain trusted. Winners can self-claim until the seven-day recovery deadline;
automatic delivery begins at day three. The guard's thirdPartySigner is the
program PDA findMintSignerPda({ sweepstake, authority, programAddress })
(seeds ["mint-signer", sweepstake, authority]), so mallow's key cannot mint
directly. Because the signer binds the creating authority, anyone else who
tries to create the sweepstake for that machine derives a different signer and
the guard check rejects them. Chain verification requires the created
mint_signer to match the creating authority.
Commands
pnpm generate
pnpm typecheck
pnpm test
pnpm build
pnpm benchmark:draw
mallow-sweepstake verify <sweepstake> --rpc <url> [--drand-mirror]
mallow-sweepstake result <sweepstake> --user <address> --rpc <url>
mallow-sweepstake settle send <sweepstake> --rpc <url> --keypair ~/.config/solana/id.json --send
mallow-sweepstake resolve send <sweepstake> --user <address> --rpc <url> --keypair ~/.config/solana/id.json --send
mallow-sweepstake verify-manifest path/to/manifest.json
mallow-sweepstake verify-manifest https://archive.example/manifest.json --rpc https://archival-rpc.example
mallow-sweepstake verify-manifest path/to/dev-manifest.json --allow-short-windows
mallow-sweepstake recover sol --program <address> --sweepstake <address> --entry <address> --user <address> --fee-destination <address> --setup-id <64-hex>
mallow-sweepstake recover resolve --program <address> --sweepstake <address> --entry <address> --setup-id <64-hex>
mallow-sweepstake recover send --program <address> --sweepstake <address> --entry <address> --user <address> --setup-id <64-hex> --rpc <url> --keypair ~/.config/solana/id.json --sendverify needs only a sweepstake address and a trusted archival RPC, with no
manifest or mallow backend. It decodes the finalized Sweepstake account
(owner, size, discriminator, version 2) and scans authenticated events back to
this generation's SweepstakeCreated. It reads the drand signature from the
authenticated DrawSettled event, verifies it locally against the pinned
evmnet key for the committed round, recomputes the seed with both transcript
implementations, and recomputes the winners with both draw implementations.
Every authenticated Resolved result must match. It works after the
Sweepstake account is closed. JSON output includes
{ok, status, signatureValid, seedMatches, n, k, winners, resolvedChecked,
missingEvidence, errors} plus hex seed, randomness, drandSignature,
chainHash and decimal round. Missing or pruned history fails with
missingEvidence and a non-zero exit. --drand-mirror also requires a public
drand mirror to return the identical signature.
result prints {index, user, isWinner, shuffledIndex} for one entrant from
the same verification. settle send fetches the committed round from public
drand mirrors (each with a 10-second timeout), verifies it locally, and submits
settle_draw with a 400,000 compute-unit limit (measured SBF maximum
283,532 CU over 20 archived rounds; theoretical four-attempt pivot bound about
368,000). resolve send reads the
finalized Sweepstake and Entry, rejects a resolved Entry before loading the
keypair, and sends the proof-free resolve_entry. Anyone may pay; --keypair
and exactly one literal --send are required. Transactions use RPC preflight
and wait for finalized confirmation. These commands default to the program
address in the generated IDL; use --program <address> for another deployment.
Commands reject unrecognized flags; result validates --user before RPC access.
--allow-short-windows accepts only the development close+600 second draw and
close+1400 second recovery deadlines, and rejects a mainnet genesis hash.
Manifests (v2)
The default manifest verifier is offline. It rejects anything but
manifestVersion: 2 and mallow-sweepstake-draw-v2, checks the PDA, terms,
round commitment and deadlines, verifies the archived drand signature
cryptographically (it never trusts an archived seed), recomputes the seed and
winners, and checks archived results. This establishes internal consistency
only. With --rpc, the CLI verifies the cluster genesis hash and program
account, then scans finalized history back to this generation's creation event
and authenticates every Entered, DrawSettled, Resolved, cancellation,
void and closure event against the manifest. A manifest with a closure
passes after its candy machine is closed; one without requires it to exist.
The RPC must retain archival
transaction history; pruned history fails closed. It must also serve v1
(SIMD-0385) transactions (Agave 4.2 or later): mallow resolves winners in
batched v1 transactions, and history is read with
maxSupportedTransactionVersion: 1.
Recovery without mallow
recover sol, recover resolve and recover principal print unsigned
instructions and never read a private key or connect to RPC.
recover send is an explicit local-key opt-in and requires --rpc,
--keypair and the literal --send. It reads finalized Sweepstake, Entry and
Clock accounts in one snapshot and validates their owner, version, generation,
canonical PDAs and the immutable Entry recipient. Before the recovery deadline,
a settled but unresolved Entry is resolved in the same transaction as the first
withdrawal. For a token entry, the SOL component (rent reserve, plus the entry
fee when refundable) finalizes first in its own transaction; if the reserve
already went out but the fee is still held at or after the recovery deadline,
it sends a fee-only withdraw_entry_sol. It then reloads and submits token
principal, which settles any still-held fee. For SOL setups, the writable
token-account placeholders are the original Entry.user and the read-only mint
and token program are the system program. Never put the system program or an
invoked program in a writable slot: the runtime demotes it to read-only and
Anchor rejects it. A production operator using multisig custody should assemble
the unsigned instructions in its normal workflow instead of exporting a key.
Principal refunds go to the entrant's canonical associated token account
when it is missing (the program creates it) or valid. A classic SPL entrant
can reassign that ATA's owner with SetAuthority; the program then refuses it.
In that case recover send (and planPrincipalRefundDestination for other
callers) creates a fresh token account owned by the original entrant for the
stored mint, in the same transaction as withdraw_entry: System
CreateAccountWithSeed with the payer as base and a random seed, then Token
InitializeAccount3. A failed withdrawal reverts the account, so no rent is
orphaned. The recovery payer funds that account's rent (about 0.002 SOL); the
entrant owns it and can close it to recover the rent. Tokens are never sent to
an account owned by anyone else. Token-2022 ATAs are ImmutableOwner and cannot
be reassigned. Manual recover principal may pass any existing token account
owned by the original entrant for the stored mint; the program checks owner,
mint and token program.
Token-2022 payouts: an entrant or treasury token account may enable
RequiredMemoTransfers. The program then CPIs the SPL Memo program before the
transfer and needs MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr as one trailing
read-only, non-signer remaining account (it fails MemoProgramRequired
otherwise). The generated builders stay raw; wrap every Token-2022
withdraw_entry, claim_prize, deliver_prize and close_sweepstake with
withMemoProgramForToken2022(instruction, sweepstake.tokenProgram). It is
idempotent, preserves every other field and meta, and returns SOL and classic
SPL instructions unchanged. getPrincipalRecoveryInstruction, recover send
and recover principal apply it automatically; withdraw_entry_sol moves no
tokens and does not need it.
Entry fees stay in escrow until terminal processing: earned by a successful mint or a resolved loser's full withdrawal before the recovery deadline, and refunded on chain for cancelled, void or expired draws and for anything still open at the recovery deadline.
decodeSweepstakeEvents(logs, expectedProgramAddress) tracks Solana invocation
depth and decodes Program data: only while that program is executing.
Benchmarks
pnpm benchmark:draw runs the canonical all-entrant draw, a single-entry
evaluation, the independent draw and one local drand verification in fresh
processes at 1,000 to 1,000,000 entrants, and requires identical winner sets.
Results are in benchmarks/. They measure local CPU and memory only, not
compute units or RPC throughput.
drand verification provenance and license attribution are in DRAND_PROVENANCE.md.
