Skip to content

About

Drip token faucet program

Resources

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Repository files navigation

DripToken

Production-quality, intentionally simple SPL token faucet on Solana (Anchor).

Users claim a fixed amount of a configured SPL token. The program enforces a per-user cooldown and/or a rolling 24-hour daily limit, supports both mint and transfer modes, and is designed for clarity, safety, and easy auditing.

Design philosophy: minimal moving parts, fixed-size accounts, state-before-effects, no unbounded growth, descriptive errors and events.


Architecture Overview

The program (drip_token) maintains two persistent account types:

Account Seeds Purpose
Config [b"config"] Singleton global faucet configuration
UserState [b"user", user.key().as_ref()] Per-user rate-limit state (init_if_needed)

Token movement occurs in one of two modes (set in Config):

  • Mint mode (mode = 0) – Config PDA is the mint authority; claim calls token_interface::mint_to.
  • Transfer mode (mode = 1) – Config PDA owns a vault token account; claim calls token_interface::transfer_checked. Vault balance must be strictly greater than claim_amount.

All critical UserState updates happen before any token CPI. Both cooldown and daily limit are enforced when their respective values are > 0. A global paused flag can disable claims.

An admin helper mint_to_vault exists to fund the vault by minting into it (admin-only).

Primary token path uses anchor_spl::token_interface (Token-2022 compatible). Users receive tokens into their Associated Token Account for the configured mint.

High-level claim flow

User signs claim
       │
       ▼
Load Config + UserState (init_if_needed)
       │
       ▼
paused == false ?
       │
       ▼
Cooldown elapsed? (if cooldown_seconds > 0)
       │
       ▼
Daily window check / reset (if daily_limit > 0)
       │
       ▼
Update UserState (last_claim_ts, claimed_today, last_day_ts)
       │
       ▼
Mint or Transfer (exactly config.claim_amount)
       │
       ▼
Emit ClaimEvent

Account Diagrams

Text diagram

Config PDA  seeds = ["config"]
┌─────────────────────────────────────────────────────────────┐
│ admin: Pubkey                                               │
│ mint: Pubkey          (immutable after initialize)          │
│ vault: Pubkey         (Pubkey::default() allowed in Mint)   │
│ claim_amount: u64                                           │
│ cooldown_seconds: u64 (0 = disabled)                        │
│ daily_limit: u64      (0 = disabled)                        │
│ mode: u8              (0 = Mint, 1 = Transfer)              │
│ paused: bool                                                │
│ bump: u8                                                    │
└─────────────────────────────────────────────────────────────┘

UserState PDA  seeds = ["user", user]
┌─────────────────────────────────────────────────────────────┐
│ last_claim_ts: i64                                          │
│ claimed_today: u64                                          │
│ last_day_ts: i64      (start of current rolling 24 h window)│
│ bump: u8                                                    │
└─────────────────────────────────────────────────────────────┘

Vault (Transfer mode only)
  Token account whose authority = Config PDA, mint = Config.mint

Mermaid – account relationships

erDiagram
    CONFIG ||--o{ USER_STATE : "rate-limits"
    CONFIG ||--o| VAULT : "owns (Transfer mode)"
    CONFIG ||--|| MINT : "mint authority (Mint mode) / referenced"
    USER ||--|| USER_STATE : "one per user"
    USER ||--|| USER_ATA : "receives tokens"
    CONFIG {
        pubkey admin
        pubkey mint
        pubkey vault
        u64 claim_amount
        u64 cooldown_seconds
        u64 daily_limit
        u8 mode
        bool paused
        u8 bump
    }
    USER_STATE {
        i64 last_claim_ts
        u64 claimed_today
        i64 last_day_ts
        u8 bump
    }
Loading

Mermaid – claim sequence (happy path)

sequenceDiagram
    participant U as User
    participant P as drip_token
    participant C as Config PDA
    participant S as UserState PDA
    participant T as Token Program

    U->>P: claim()
    P->>C: load
    P->>S: load / init_if_needed
    P->>P: check paused, cooldown, daily limit
    P->>S: update last_claim_ts, claimed_today, last_day_ts
    alt Mint mode
        P->>T: mint_to(user ATA, claim_amount)
    else Transfer mode
        P->>T: transfer_checked(vault → user ATA, claim_amount)
    end
    P-->>U: ClaimEvent
Loading

Instructions

Instruction Admin only? Description
initialize Yes Creates Config PDA. Sets admin, mint, claim_amount, limits, mode, paused. Mint becomes immutable.
claim No User claims exactly config.claim_amount. Enforces pause + both limits. Updates UserState before CPI.
update_config Yes Updates claim_amount, cooldown_seconds, daily_limit, mode, paused, admin. Cannot change mint.
set_vault Yes Sets or replaces the vault token account. New vault must match mint and have Config PDA as authority.
close_user_state No User closes own UserState and recovers rent. Allowed only when rate-limit state is fully clean.
mint_to_vault Yes Admin helper: mints a caller-supplied amount into the vault (Config PDA signs as mint authority).

Account requirements (summary)

initialize

  • Signer: future admin
  • Config PDA (init)
  • Mint account
  • System program

claim

  • Signer: user
  • Config PDA
  • UserState PDA (init_if_needed)
  • User’s ATA (init_if_needed where appropriate)
  • Vault token account (always required by current constraints, even in Mint mode)
  • Mint
  • Token program / Associated Token program / System program as needed

update_config

  • Signer: current config.admin
  • Config PDA

set_vault

  • Signer: current config.admin
  • Config PDA
  • New vault token account (must be for config.mint, authority = Config PDA)

close_user_state

  • Signer: user (owner of the PDA)
  • UserState PDA
  • Conditions: claimed_today == 0 and cooldown fully elapsed (or cooldown_seconds == 0)

mint_to_vault

  • Signer: current config.admin
  • Config PDA
  • Vault (must already be authorised by Config PDA)
  • Mint
  • Token program

Security Considerations & Invariants

The program assumes hostile clients. Key invariants that must always hold:

  1. Singleton Config – seeds = [b"config"].
  2. Unique UserState – seeds = [b"user", user.key().as_ref()].
  3. State-before-effects – UserState is fully updated before any mint or transfer CPI inside claim.
  4. Fixed claim amount – claim never uses a client-supplied amount; always config.claim_amount.
  5. Pause is absolute for claims – paused == true rejects every claim.
  6. Dual limit enforcement – both cooldown and daily limit are checked when > 0; a claim must satisfy both.
  7. Rolling 24-hour window – exactly 86_400 seconds from last_day_ts.
  8. Mode safety
    • Mint: Config PDA must be mint authority.
    • Transfer: vault authority = Config PDA and balance strictly greater than claim_amount.
  9. Mint immutability – set only in initialize; never changed afterwards.
  10. Admin-only mutations – only the stored admin can call admin instructions.
  11. No debt / no over-mint on claim – Transfer mode rejects on insufficient vault; Mint mode relies on mint authority + supply rules.
  12. No account reallocation – fixed-size accounts only.
  13. Clean close only – close_user_state requires claimed_today == 0 and fully expired cooldown (prevents close + re-init bypass).
  14. Fixed update_config surface – only the listed fields are mutable; mint and bump are immutable.
  15. mint_to_vault is admin-only – operational helper, not part of the user claim path.

Full threat model, residual risks, and error-code mapping live in 03-security-model.md.

Trust assumptions

  • Solana Clock sysvar is trusted for time-based limits.
  • Admin key compromise is out of scope for the initial design (recommended production posture: multisig or renounce after audit).
  • No on-chain governance in v1.

Events

Event Emitted by Fields
ClaimEvent claim user, amount, timestamp, mode
UserStateClosed close_user_state user, timestamp

Events are the primary interface for indexers and explorers. Schema is intentionally minimal and stable.


Errors

Code Meaning
FaucetPaused Claim (or mint_to_vault) attempted while paused
CooldownNotElapsed Cooldown still active
DailyLimitExceeded Claim would exceed daily limit
ArithmeticOverflow checked_add / checked_sub failed
InvalidMode Mode is not 0 or 1
InsufficientVaultBalance Transfer mode: vault balance ≤ claim_amount
InvalidMint Provided mint does not match Config
InvalidVault Vault invalid for mint / wrong authority
Unauthorized Caller is not the current admin
AlreadyInitialized Config already exists
CannotCloseWithClaimedToday UserState still has claimed_today > 0
CannotCloseDuringCooldown Cooldown has not fully expired

All failure paths return distinct, descriptive ErrorCode variants. No panics in instruction handlers.


Configuration Reference

Field Type Meaning
admin Pubkey Authority for admin instructions
mint Pubkey SPL token mint (immutable after initialize)
vault Pubkey Vault token account (Transfer mode). May be default in Mint mode
claim_amount u64 Exact tokens delivered per successful claim
cooldown_seconds u64 Minimum seconds between claims (0 = disabled)
daily_limit u64 Max tokens claimable per rolling 24 h window (0 = disabled)
mode u8 0 = Mint, 1 = Transfer
paused bool Emergency switch – true disables claims
bump u8 Config PDA bump

Limit logic

  • Claim allowed only when paused == false and (cooldown elapsed or disabled) and (daily limit not exceeded or disabled).
  • Daily window is a rolling 86_400-second period measured from last_day_ts.
  • Even when daily_limit == 0, the implementation still writes claimed_today = claim_amount (the field is unused for enforcement in that case).

Building & Testing

Development is expected in GitHub Codespaces (or any environment with the Solana / Anchor toolchain).

# Install / update toolchain as needed (Codespaces usually pre-configured)
avm use latest          # or pin a specific Anchor version
anchor --version

# Build
anchor build

# Run the full TypeScript test suite (local validator)
anchor test

# Run a specific test file if desired
anchor test --skip-local-validator -- --grep "cooldown"

Test coverage expectations (see 05-testing.md):

  • Happy path (Mint + Transfer)
  • Cooldown, daily limit, dual limits, day-boundary edge cases
  • Pause
  • Admin authority checks
  • Wrong mint / vault / insufficient vault balance
  • init_if_needed and first-time users
  • Clean-close rules for close_user_state
  • Clock sysvar manipulation for deterministic time tests
  • Trident fuzzing harness (when configured under trident/)

Helper utilities live under tests/utils/ (setup.ts, time.ts, accounts.ts).


Deployment

Current scope: localnet → devnet only.
Mainnet is intentionally out of scope for the foreseeable future.

Upgrade authority (devnet)

  • Keep a single controlled upgrade authority key while iterating on devnet.
  • Record the key location and the Program ID after every deploy.
  • If the program is later promoted, the preferred production posture is to renounce upgrade authority (immutable) or move it to a multisig. That decision is deferred.

Operational scripts

All day-to-day commands live under scripts/.
See scripts/README.md for the full reference. Summary:

Script Purpose
./scripts/build.sh Clean anchor build
./scripts/deploy-devnet.sh Deploy / upgrade to devnet
ts-node scripts/initialize.ts … Create Config PDA
ts-node scripts/setup-mint-mode.ts … Transfer mint authority → Config PDA
ts-node scripts/setup-transfer-mode.ts … Create vault + set_vault (+ optional fund)
ts-node scripts/health-check.ts Print Config (+ optional UserState)
ts-node scripts/update-config.ts … Change claim amount / limits / mode / admin / pause
ts-node scripts/pause.ts / --unpause Convenience pause helpers
ts-node scripts/claim.ts Smoke-test claim

Environment overrides: CLUSTER, RPC_URL, PROGRAM_ID, ANCHOR_WALLET.

First-time devnet checklist

# 0. Prerequisites
#    - Funded wallet on devnet
#    - yarn install && anchor build

# 1. Build
./scripts/build.sh

# 2. Deploy
./scripts/deploy-devnet.sh
#    → note the Program ID printed by Anchor
#    → optionally update sdk/src/constants.ts and Anchor.toml [programs.devnet]

# 3. Create (or reuse) an SPL mint
spl-token create-token --decimals 6 --url devnet
#    → MINT=<address>

# 4. Initialize Config (admin = current wallet)
ts-node scripts/initialize.ts \
  --mint $MINT \
  --claim-amount 1000000 \
  --cooldown 60 \
  --daily-limit 5000000 \
  --mode 0          # 0 = Mint, 1 = Transfer

# 5a. Mint mode – give Config PDA mint authority
ts-node scripts/setup-mint-mode.ts --mint $MINT

# 5b. Transfer mode alternative
# ts-node scripts/setup-transfer-mode.ts --mint $MINT --fund 100000000

# 6. Verify
ts-node scripts/health-check.ts

# 7. Smoke-test claim
ts-node scripts/claim.ts

Day-to-day operations

  • Pause / unpause
    ts-node scripts/pause.ts or ts-node scripts/pause.ts --unpause
    (or call update-config.ts with the desired paused value)

  • Change parameters (claim amount, cooldown, daily limit, mode, admin)
    Always via update_config – use scripts/update-config.ts.
    The mint is immutable after initialize.

  • Vault management (Transfer mode)
    Monitor vault balance. Re-fund with a normal token transfer or with
    mint_to_vault (admin-only, Config PDA must be mint authority).
    No automated refill in v1.

  • Key management
    Loss of the admin key means loss of ability to pause or change config.
    Keep the admin key secure even on devnet.

Anchor.toml notes

After the first successful devnet deploy, add:

[programs.devnet]
drip_token = "<PROGRAM_ID_FROM_DEPLOY>"

Keep [programs.localnet] for local testing.
The SDK defaults to the localnet Program ID; override with PROGRAM_ID env var or by updating sdk/src/constants.ts when working against devnet.


Client Usage

A minimal TypeScript client lives under sdk/.

import { AnchorProvider, Program } from "@coral-xyz/anchor";
import { Connection, PublicKey } from "@solana/web3.js";
import { getConfig } from "./sdk/src/config";
import { getUserState } from "./sdk/src/user";
import { claim } from "./sdk/src/claim";
import { initialize, updateConfig, setVault, mintToVault } from "./sdk/src/admin";
import { DEFAULT_PROGRAM_ID } from "./sdk/src/constants";
import { mapProgramError } from "./sdk/src/errors";

// Fetch global config
const config = await getConfig(connection, program, programId);

// Fetch user state (null if never claimed)
const userState = await getUserState(connection, program, user, programId);

// Claim (provider wallet must sign as the user)
try {
  const result = await claim(provider, program, user);
  console.log("claimed", result.amount, "mode", result.mode);
} catch (e) {
  console.error(mapProgramError(e));
}

Public helpers

Function Purpose
getConfig Fetch & deserialize Config PDA
getUserState Fetch UserState (or null)
claim Build + send claim instruction
initialize Admin – create Config
updateConfig Admin – update mutable fields
setVault Admin – set/replace vault
mintToVault Admin – mint into vault
mapProgramError Human-readable error messages

PDA derivation, seeds, and mode constants are centralized in sdk/src/constants.ts and sdk/src/pdas.ts. They must stay in sync with on-chain constants.rs.


Project Layout (reference)

programs/drip_token/
├── Cargo.toml
└── src/
    ├── lib.rs
    ├── state.rs
    ├── errors.rs
    ├── events.rs
    ├── constants.rs
    └── instructions/
        ├── mod.rs
        ├── initialize.rs
        ├── claim.rs
        ├── update_config.rs
        ├── set_vault.rs
        ├── close_user_state.rs
        └── mint_to_vault.rs

sdk/src/          # TypeScript client helpers
tests/            # Anchor TypeScript tests + utils
docs/             # Optional deeper notes (architecture, security)

Phase documents (01-… through 09-…) record the locked decisions that this README reflects. Any change to account layout, authority model, or security invariants requires an explicit update to those documents first.


License & Status

Implementation status is tracked against the phase documents.
Current implementation matches the locked architecture and security model on the feature-claim branch (see 02-architecture-and-account-design.md and 03-security-model.md).

This README is the primary entry point for developers and auditors.

About

Drip token faucet program

Resources

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages