Skip to content

Commit 27b4ade

Browse files
author
tilo-14
committed
Add airdrop implementations
- compressed-claim: slot-based time-lock, distributes compressed tokens - distributor: linear vesting, clawback, admin controls, Merkle proofs - README with feature comparison and cost breakdown - CLAUDE.md docs for example programs
1 parent e96d0e2 commit 27b4ade

File tree

22 files changed

+10193
-0
lines changed

22 files changed

+10193
-0
lines changed

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[submodule "airdrop-implementations/distributor"]
2+
path = airdrop-implementations/distributor
3+
url = https://github.com/Lightprotocol/distributor.git

account-comparison/CLAUDE.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# Account Comparison
2+
3+
Side-by-side implementation of Solana accounts vs compressed accounts. Shows equivalent create and update operations for both account types using identical data structures.
4+
5+
## Summary
6+
7+
- Demonstrates equivalent PDA and compressed account patterns with identical seeds `["account", user.key()]`
8+
- Compressed accounts use `LightAccount::new_init` for creation and `LightAccount::new_mut` for updates
9+
- Updates require client to pass existing state because on-chain storage is a Poseidon hash
10+
- All fields marked `#[hash]` are included in Poseidon hash computation
11+
12+
## Source Structure
13+
14+
```
15+
programs/account-comparison/src/
16+
lib.rs # Program entrypoint, instructions, account structs
17+
programs/account-comparison/tests/
18+
test_solana_account.rs # LiteSVM tests for standard accounts
19+
test_compressed_account.rs # Light Protocol tests for compressed accounts
20+
```
21+
22+
## Accounts
23+
24+
### AccountData (Solana PDA)
25+
26+
| Field | Type | Offset |
27+
|-------|------|--------|
28+
| discriminator | `[u8; 8]` | 0..8 |
29+
| user | `Pubkey` | 8..40 |
30+
| name | `String` | 40..168 |
31+
| data | `[u8; 128]` | 168..232 |
32+
33+
- **Seeds**: `["account", user.key()]`
34+
- **Discriminator**: 8 bytes, SHA256("account:AccountData")[0..8]
35+
- **Space**: 232 bytes
36+
37+
### CompressedAccountData (LightAccount)
38+
39+
```rust
40+
#[derive(LightDiscriminator, LightHasher)]
41+
pub struct CompressedAccountData {
42+
#[hash] pub user: Pubkey,
43+
#[hash] pub name: String,
44+
#[hash] pub data: [u8; 128],
45+
}
46+
```
47+
48+
- **Address seeds**: `["account", user.key()]`
49+
- **Discriminator**: `LightDiscriminator` derive generates from struct name
50+
- **Hashing**: Poseidon hash of all `#[hash]` fields (user, name, data)
51+
52+
### CPI Signer
53+
54+
```rust
55+
const CPI_SIGNER: CpiSigner = derive_light_cpi_signer!("FYX4GmKJYzSiycc7XZKf12NGXNE9siSx1cJubYJniHcv");
56+
```
57+
58+
## Instructions
59+
60+
| # | Instruction | Accounts | Parameters | Logic |
61+
|---|-------------|----------|------------|-------|
62+
| 0 | `create_account` | `user` (signer, mut), `account` (init PDA), `system_program` | `name: String` | Initializes PDA with seeds `["account", user]`, sets `data = [1u8; 128]` |
63+
| 1 | `update_data` | `user` (signer, mut), `account` (mut, has_one = user) | `data: [u8; 128]` | Overwrites account.data |
64+
| 2 | `create_compressed_account` | `user` (signer, mut) + remaining_accounts | `name`, `proof`, `address_tree_info`, `output_tree_index` | Validates ADDRESS_TREE_V2, derives address, calls `LightAccount::new_init`, invokes light-system-program |
65+
| 3 | `update_compressed_account` | `user` (signer, mut) + remaining_accounts | `new_data`, `existing_data`, `name`, `proof`, `account_meta` | Reconstructs state via `LightAccount::new_mut`, verifies user ownership, invokes light-system-program |
66+
67+
## Security
68+
69+
| Check | Location | Description |
70+
|-------|----------|-------------|
71+
| Address tree validation | `create_compressed_account` | Verifies `address_tree_pubkey.to_bytes() == ADDRESS_TREE_V2` |
72+
| Owner verification | `update_compressed_account` | Asserts `compressed_account.user == ctx.accounts.user.key()` |
73+
| PDA constraint | `update_data` | Anchor `has_one = user` constraint |
74+
| Signer requirement | All instructions | User must sign transaction |
75+
76+
## Errors
77+
78+
| Error | Message |
79+
|-------|---------|
80+
| `CustomError::Unauthorized` | "No authority to perform this action" |
81+
| `ProgramError::InvalidAccountData` | Returned when address tree validation fails |

airdrop-implementations/README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Airdrop Implementations
2+
3+
Simple Implementation: compressed-claim - Distributes compressed tokens that get decompressed on claim
4+
5+
Advanced Implementation: distributor - Distributes SPL tokens, uses compressed PDAs to track claims
6+
7+
8+
## Quick comparison
9+
10+
| | compressed-claim | distributor |
11+
|--|------------------|-------------|
12+
| Time-lock | Slot-based (all-or-nothing) | Timestamp-based (linear vesting) |
13+
| Partial claims | No | Yes |
14+
| Clawback | No | Yes |
15+
| Admin controls | No | Yes |
16+
17+
## Cost (10,000 recipients)
18+
19+
| | compressed-claim | distributor | Regular |
20+
|--|----------------:|------------:|----------------:|
21+
| Setup | ~0.03 SOL | ~0.002 SOL | ~0.002 SOL |
22+
| Claim tracking | 0 | ~0.03 SOL | ~6 SOL |
23+
| **Total** | **~0.03 SOL** | **~0.03 SOL** | **~6 SOL** |
24+
25+
## Getting started
26+
27+
- [compressed-claim README](./compressed-claim/README.md)
28+
- [distributor README](./distributor/README.md)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
.anchor
2+
.cargo
3+
.DS_Store
4+
**/.DS_Store
5+
**/target
6+
**/*.rs.bk
7+
node_modules
8+
test-ledger
9+
dist
10+
test-keypair.json
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# Compressed Claim
2+
3+
Claims time-locked compressed tokens and decompresses them to an SPL token account.
4+
5+
## [README](README.md)
6+
7+
## Source Structure
8+
9+
```text
10+
program/src/
11+
├── lib.rs # entrypoint, declare_id!
12+
├── error.rs # ClaimError (3 variants)
13+
├── instruction.rs # ClaimIxData, ClaimAccounts, builders
14+
└── processor.rs # process_claim logic
15+
```
16+
17+
## Accounts
18+
19+
### Airdrop PDA
20+
21+
Seeds: `[claimant.to_bytes(), mint.to_bytes(), unlock_slot.to_le_bytes(), bump]`
22+
23+
Derived via `Pubkey::create_program_address`. Holds compressed tokens until claim.
24+
25+
### ClaimIxData (Instruction Data)
26+
27+
| Field | Type | Description |
28+
|-------|------|-------------|
29+
| proof | ValidityProof | ZK proof for compressed account |
30+
| packed_tree_info | PackedStateTreeInfo | Merkle tree state info |
31+
| amount | u64 | Tokens to claim |
32+
| lamports | Option<u64> | Optional lamports to transfer |
33+
| mint | Pubkey | Token mint address |
34+
| unlock_slot | u64 | Slot when tokens unlock |
35+
| bump_seed | u8 | PDA bump seed |
36+
37+
## Instructions
38+
39+
| Instruction | Path | Accounts | Logic |
40+
|-------------|------|----------|-------|
41+
| Claim | processor.rs | claimant (signer), fee_payer (signer), airdrop_pda, + 13 Light/CToken accounts | Verifies unlock_slot <= current_slot, validates PDA, invokes decompress CPI |
42+
43+
### Claim Accounts (16 total)
44+
45+
0. claimant (signer)
46+
1. fee_payer (signer)
47+
2. associated_airdrop_pda
48+
3. ctoken_cpi_authority_pda
49+
4. light_system_program
50+
5. registered_program_pda
51+
6. noop_program
52+
7. account_compression_authority
53+
8. account_compression_program
54+
9. ctoken_program
55+
10. spl_interface_pda (token_pool)
56+
11. decompress_destination (writable)
57+
12. token_program
58+
13. system_program
59+
14. state_tree (writable)
60+
15. queue (writable)
61+
62+
## Key Concepts
63+
64+
**Time-lock**: Tokens unlock at `unlock_slot`. Claim fails if `current_slot < unlock_slot`.
65+
66+
**PDA Derivation**: `[claimant, mint, unlock_slot, bump]` - ensures only rightful claimant can claim.
67+
68+
**Decompress CPI**: Invokes `light_ctoken_sdk::decompress` to convert compressed tokens to SPL.
69+
70+
## Security
71+
72+
**Signer checks**: Both claimant and fee_payer must sign.
73+
74+
**PDA validation**: Derived PDA must match provided airdrop_pda account.
75+
76+
**Program check**: ctoken_program must equal `light_ctoken_sdk::ctoken::id()`.
77+
78+
## Errors
79+
80+
| Error | Description |
81+
|-------|-------------|
82+
| MissingRequiredSignature | Claimant or fee_payer not signer |
83+
| TokensLocked | current_slot < unlock_slot |
84+
| InvalidPDA | Derived PDA doesn't match provided account |

0 commit comments

Comments
 (0)