1use anchor_lang::{prelude::*, solana_program::pubkey::PUBKEY_BYTES};
18use anchor_spl::token::{self, Mint, Token, TokenAccount};
19use vipers::prelude::*;
20
21pub mod merkle_proof;
22
23declare_id!("MRKGLMizK9XSTaD1d1jbVkdHZbQVCSnPpYiTw9aKQv8");
24
25#[program]
27pub mod merkle_distributor {
28 use super::*;
29
30 pub fn new_distributor(
33 ctx: Context<NewDistributor>,
34 _bump: u8,
35 root: [u8; 32],
36 max_total_claim: u64,
37 max_num_nodes: u64,
38 ) -> Result<()> {
39 let distributor = &mut ctx.accounts.distributor;
40
41 distributor.base = ctx.accounts.base.key();
42 distributor.bump = unwrap_bump!(ctx, "distributor");
43
44 distributor.root = root;
45 distributor.mint = ctx.accounts.mint.key();
46
47 distributor.max_total_claim = max_total_claim;
48 distributor.max_num_nodes = max_num_nodes;
49 distributor.total_amount_claimed = 0;
50 distributor.num_nodes_claimed = 0;
51
52 Ok(())
53 }
54
55 pub fn claim(
57 ctx: Context<Claim>,
58 _bump: u8,
59 index: u64,
60 amount: u64,
61 proof: Vec<[u8; 32]>,
62 ) -> Result<()> {
63 assert_keys_neq!(ctx.accounts.from, ctx.accounts.to);
64
65 let claim_status = &mut ctx.accounts.claim_status;
66 invariant!(
67 !claim_status.is_claimed && claim_status.claimed_at == 0,
69 DropAlreadyClaimed
70 );
71
72 let claimant_account = &ctx.accounts.claimant;
73 let distributor = &ctx.accounts.distributor;
74 invariant!(claimant_account.is_signer, Unauthorized);
75
76 let node = anchor_lang::solana_program::keccak::hashv(&[
78 &index.to_le_bytes(),
79 &claimant_account.key().to_bytes(),
80 &amount.to_le_bytes(),
81 ]);
82 invariant!(
83 merkle_proof::verify(proof, distributor.root, node.0),
84 InvalidProof
85 );
86
87 claim_status.amount = amount;
89 claim_status.is_claimed = true;
90 let clock = Clock::get()?;
91 claim_status.claimed_at = clock.unix_timestamp;
92 claim_status.claimant = claimant_account.key();
93
94 let seeds = [
95 b"MerkleDistributor".as_ref(),
96 &distributor.base.to_bytes(),
97 &[ctx.accounts.distributor.bump],
98 ];
99
100 #[allow(deprecated)]
101 {
102 vipers::assert_ata!(
103 ctx.accounts.from,
104 ctx.accounts.distributor,
105 distributor.mint
106 );
107 }
108 assert_keys_eq!(ctx.accounts.to.owner, claimant_account.key(), OwnerMismatch);
109 token::transfer(
110 CpiContext::new(
111 ctx.accounts.token_program.to_account_info(),
112 token::Transfer {
113 from: ctx.accounts.from.to_account_info(),
114 to: ctx.accounts.to.to_account_info(),
115 authority: ctx.accounts.distributor.to_account_info(),
116 },
117 )
118 .with_signer(&[&seeds[..]]),
119 amount,
120 )?;
121
122 let distributor = &mut ctx.accounts.distributor;
123 distributor.total_amount_claimed =
124 unwrap_int!(distributor.total_amount_claimed.checked_add(amount));
125 invariant!(
126 distributor.total_amount_claimed <= distributor.max_total_claim,
127 ExceededMaxClaim
128 );
129 distributor.num_nodes_claimed = unwrap_int!(distributor.num_nodes_claimed.checked_add(1));
130 invariant!(
131 distributor.num_nodes_claimed <= distributor.max_num_nodes,
132 ExceededMaxNumNodes
133 );
134
135 emit!(ClaimedEvent {
136 index,
137 claimant: claimant_account.key(),
138 amount
139 });
140 Ok(())
141 }
142}
143
144#[derive(Accounts)]
146pub struct NewDistributor<'info> {
147 pub base: Signer<'info>,
149
150 #[account(
152 init,
153 seeds = [
154 b"MerkleDistributor".as_ref(),
155 base.key().to_bytes().as_ref()
156 ],
157 bump,
158 space = 8 + MerkleDistributor::LEN,
159 payer = payer
160 )]
161 pub distributor: Account<'info, MerkleDistributor>,
162
163 pub mint: Account<'info, Mint>,
165
166 #[account(mut)]
168 pub payer: Signer<'info>,
169
170 pub system_program: Program<'info, System>,
172}
173
174#[derive(Accounts)]
176#[instruction(_bump: u8, index: u64)]
177pub struct Claim<'info> {
178 #[account(
180 mut,
181 address = from.owner
182 )]
183 pub distributor: Account<'info, MerkleDistributor>,
184
185 #[account(
187 init,
188 seeds = [
189 b"ClaimStatus".as_ref(),
190 index.to_le_bytes().as_ref(),
191 distributor.key().to_bytes().as_ref()
192 ],
193 bump,
194 space = 8 + ClaimStatus::LEN,
195 payer = payer
196 )]
197 pub claim_status: Account<'info, ClaimStatus>,
198
199 #[account(mut)]
201 pub from: Account<'info, TokenAccount>,
202
203 #[account(mut)]
205 pub to: Account<'info, TokenAccount>,
206
207 #[account(address = to.owner @ ErrorCode::OwnerMismatch)]
209 pub claimant: Signer<'info>,
210
211 #[account(mut)]
213 pub payer: Signer<'info>,
214
215 pub system_program: Program<'info, System>,
217
218 pub token_program: Program<'info, Token>,
220}
221
222#[account]
224#[derive(Default)]
225pub struct MerkleDistributor {
226 pub base: Pubkey,
228 pub bump: u8,
230
231 pub root: [u8; 32],
233
234 pub mint: Pubkey,
236 pub max_total_claim: u64,
238 pub max_num_nodes: u64,
240 pub total_amount_claimed: u64,
242 pub num_nodes_claimed: u64,
244}
245
246impl MerkleDistributor {
247 pub const LEN: usize = PUBKEY_BYTES + 1 + 32 + PUBKEY_BYTES + 8 * 4;
248}
249
250#[account]
254#[derive(Default)]
255pub struct ClaimStatus {
256 pub is_claimed: bool,
258 pub claimant: Pubkey,
260 pub claimed_at: i64,
262 pub amount: u64,
264}
265
266impl ClaimStatus {
267 pub const LEN: usize = 1 + PUBKEY_BYTES + 8 + 8;
268}
269
270#[event]
272pub struct ClaimedEvent {
273 pub index: u64,
275 pub claimant: Pubkey,
277 pub amount: u64,
279}
280
281#[error_code]
283pub enum ErrorCode {
284 #[msg("Invalid Merkle proof.")]
285 InvalidProof,
286 #[msg("Drop already claimed.")]
287 DropAlreadyClaimed,
288 #[msg("Exceeded maximum claim amount.")]
289 ExceededMaxClaim,
290 #[msg("Exceeded maximum number of claimed nodes.")]
291 ExceededMaxNumNodes,
292 #[msg("Account is not authorized to execute this instruction")]
293 Unauthorized,
294 #[msg("Token account owner did not match intended owner")]
295 OwnerMismatch,
296}