Skip to content

Signature Distributor

The EDDSA Distributor verifies claims with EDDSA (Ed25519) signatures instead of Merkle proofs. An airdrop stores an authorized signer rather than a Merkle root, and each claim needs that signer's signature. There is no Merkle tree to generate in advance, so the signer can authorize claims as the distribution runs.

Differences from the Merkle Distributor

Feature Merkle Distributor EDDSA Distributor
Verification Merkle proofs Ed25519 signatures
Data Storage Merkle root Authorized signer
Claim Data Fixed per root (the owner can replace the root) Dynamic per claim
Flexibility Limited High

System overview

Account structure

Accounts of the signature distributor programs Accounts of the signature distributor programs
Accounts of the signature distributor programs

Storage pattern

The program has one config account. It stores the admin's address, the default fee collector's address, and the address of a pending new admin for two-step ownership transfers.

pub struct Config {
  pub admin: Pubkey,
  pub new_admin: Pubkey,
  pub default_fee_collector: Pubkey,
}

One deployment can manage many airdrops. Each airdrop is stored as follows:

pub struct Airdrop {
  pub owner: Pubkey,
  pub authorized_signer: Pubkey,
  pub token: Pubkey,
  pub start_time: u64,
  pub end_time: u64,
  pub fee_collector: Pubkey,
  pub fee_token: Pubkey,
  pub paused: bool,
}

Each processed claim creates a Claim account. Because the account is created with init, a second claim with the same claim ID fails:

pub struct Claim {
  pub used: bool,
}

PDA seeds

Account Seeds Count
config "config" One per program
version "version" One per program (created by version)
airdrop "airdrop", project_id One per project ID
vault "vault", airdrop address One per airdrop; token authority is the airdrop PDA
claim "claim", airdrop address, user_claim_id One per claimed claim ID

Claim mechanism

A claim is submitted with this instruction:

pub fn claim(
  ctx: Context<Claim>,
  project_id: String,
  recipient: Pubkey,
  user_claim_id: [u8; 32],
  group: [u8; 32],
  user_claim_data: EDDSADistributorData,
  signature: Vec<u8>
) -> Result<()>

The program does not verify the signature itself. The transaction must also contain an Ed25519 native program instruction that verifies signature from the airdrop's authorized_signer over the claim hash; claim() finds that instruction through the instructions sysvar. The claim hash is the Keccak-256 hash of the Borsh-serialized UserClaimData struct (group is not part of it). The encode_hash_to_be_signed instruction returns this hash.

pub struct UserClaimData {
  pub recipient: Pubkey,
  pub user_claim_id: [u8; 32],
  pub user_claim_data: EDDSADistributorData,
  pub airdrop: Pubkey,
}

Any account can submit a claim with a valid signature. Tokens are always sent to the recipient's associated token account; the submitting account pays the fee and account rent.

Instructions

Function Description
initialize() Initialize an airdrop, create required accounts
set_base_params() Sets token, time window, and authorized signer
claim() Processes one signature-authorized claim
toggle_pause() Emergency pause functionality
deposit() Deposit tokens to be airdropped
withdraw() Owner withdraws the full vault balance
encode_hash_to_be_signed() Returns the hash the authorized signer must sign

Distributor variants

eddsa-token-distributor-solana

This is the standard implementation. It supports both SPL Token and Token-2022 tokens, claims are time-locked by claimable_timestamp, and the owner withdraws unclaimed tokens with the standard withdrawal function. Its claim data is:

pub struct EDDSADistributorData {
  pub claimable_timestamp: u64,
  pub claimable_amount: u64,
}

eddsa-token-with-fees-distributor-solana

This variant adds a fee to the claim data. The fee is part of what the authorized signer signs, so each claim can carry its own fee amount (see the fee system below).

pub struct EDDSADistributorData {
  pub claimable_timestamp: u64,
  pub claimable_amount: u64,
  pub fees: u64,
}

Fee system

On every claim, the distributor makes a CPI to the fee collector. The account that signs the claim transaction pays the fee. It is not deducted from the claimed tokens.

The fee collector is the external FeeCollector program, which calculates fees and manages the vault accounts. Each airdrop stores its own fee_collector, copied from default_fee_collector at initialization. How the fee is calculated depends on the variant:

  • The base program calls collect_fee with the claimed amount. The fee collector reads the project's Fee account (seeds "fee", distributor program ID, project_id). If that account is set to default, the fee is claimable_amount * default_fees_bips / 10000. Otherwise the fee collector uses a fixed fee if one is set, and custom bips if not.
  • The with-fees program calls collect_fee_amount with user_claim_data.fees. The fee collector charges that amount as given and does not use the project's Fee account.

Fees are paid in native SOL when fee_token is the default public key, and in an SPL token otherwise. The program admin sets the fee token per airdrop with set_fee_token.

Fee tokens are held in Program Derived Accounts (PDAs). SOL fees go to the fee collector's storage PDA, and SPL fees go to the fee collector's vault PDA (seeds "vault", fee token mint).

Security

Access control

Role Permissions
Program Admin Set default fee collector, set an airdrop's fee collector and fee token, transfer program admin
Airdrop Owner Set parameters (token, time window, authorized signer), toggle pause, withdraw tokens, transfer airdrop ownership
Authorized Signer Sign claim authorizations
Users Submit claims with valid signatures

deposit has no owner check: any account can deposit the airdrop's token into the vault.

Claim safeguards

The airdrop owner can pause claims in an emergency. Each claim needs a matching Ed25519 signature, verified through the Ed25519 native program, and must fall inside the distribution window. The per-claim-ID account blocks a second claim with the same ID.

Events and errors

Events

Event Description
Deployed Program config created by deploy
Initialized Successful airdrop initialization (project_id)
ClaimDelegateSet Defined, but never emitted: these programs have no claim delegate
Claimed Successful token claim. The recipient field holds the signer of the claim

Errors

Error Condition
UnsupportedOperation start_time is not before end_time in set_base_params
TimeInactive Outside distribution window
InvalidSignature No matching Ed25519 instruction from the authorized signer over the claim hash
OutsideClaimableTimeRange claimable_timestamp is not after start_time and before now
NotOwner Unpermissioned account trying to call an owner-permissioned function
NotProgramAdmin Unpermissioned account trying to call a program admin-permissioned function
NotPermissioned Unpermissioned account trying to receive program admin status
InvalidFeeCollector Provided fee collector account is incorrect
Paused Airdrop has been paused

A repeated claim ID fails because the claim account already exists; there is no dedicated error code for it.