merkle_distributor/
lib.rs

1//! A program for distributing tokens efficiently via uploading a [Merkle root](https://en.wikipedia.org/wiki/Merkle_tree).
2//!
3//! This program is largely based off of [Uniswap's Merkle Distributor](https://github.com/Uniswap/merkle-distributor).
4//!
5//! # Rationale
6//!
7//! Although Solana has low fees for executing transactions, it requires staking tokens to pay for storage costs, also known as "rent". These rent costs can add up when sending tokens to thousands or tens of thousands of wallets, making it economically unreasonable to distribute tokens to everyone.
8//!
9//! The Merkle distributor, pioneered by [Uniswap](https://github.com/Uniswap/merkle-distributor), solves this issue by deriving a 256-bit "root hash" from a tree of balances. This puts the gas cost on the claimer. Solana has the additional advantage of being able to reclaim rent from closed token accounts, so the net cost to the user should be around `0.000010 SOL` (at the time of writing).
10//!
11//! The Merkle distributor is also significantly easier to manage from an operations perspective, since one does not need to send a transaction to each individual address that may be redeeming tokens.
12//!
13//! # License
14//!
15//! The Merkle distributor program and SDK is distributed under the GPL v3.0 license.
16
17use 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/// The [merkle_distributor] program.
26#[program]
27pub mod merkle_distributor {
28    use super::*;
29
30    /// Creates a new [MerkleDistributor].
31    /// After creating this [MerkleDistributor], the account should be seeded with tokens via its ATA.
32    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    /// Claims tokens from the [MerkleDistributor].
56    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            // This check is redundant, we should not be able to initialize a claim status account at the same key.
68            !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        // Verify the merkle proof.
77        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        // Mark it claimed and send the tokens.
88        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/// Accounts for [merkle_distributor::new_distributor].
145#[derive(Accounts)]
146pub struct NewDistributor<'info> {
147    /// Base key of the distributor.
148    pub base: Signer<'info>,
149
150    /// [MerkleDistributor].
151    #[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    /// The mint to distribute.
164    pub mint: Account<'info, Mint>,
165
166    /// Payer to create the distributor.
167    #[account(mut)]
168    pub payer: Signer<'info>,
169
170    /// The [System] program.
171    pub system_program: Program<'info, System>,
172}
173
174/// [merkle_distributor::claim] accounts.
175#[derive(Accounts)]
176#[instruction(_bump: u8, index: u64)]
177pub struct Claim<'info> {
178    /// The [MerkleDistributor].
179    #[account(
180        mut,
181        address = from.owner
182    )]
183    pub distributor: Account<'info, MerkleDistributor>,
184
185    /// Status of the claim.
186    #[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    /// Distributor ATA containing the tokens to distribute.
200    #[account(mut)]
201    pub from: Account<'info, TokenAccount>,
202
203    /// Account to send the claimed tokens to.
204    #[account(mut)]
205    pub to: Account<'info, TokenAccount>,
206
207    /// Who is claiming the tokens.
208    #[account(address = to.owner @ ErrorCode::OwnerMismatch)]
209    pub claimant: Signer<'info>,
210
211    /// Payer of the claim.
212    #[account(mut)]
213    pub payer: Signer<'info>,
214
215    /// The [System] program.
216    pub system_program: Program<'info, System>,
217
218    /// SPL [Token] program.
219    pub token_program: Program<'info, Token>,
220}
221
222/// State for the account which distributes tokens.
223#[account]
224#[derive(Default)]
225pub struct MerkleDistributor {
226    /// Base key used to generate the PDA.
227    pub base: Pubkey,
228    /// Bump seed.
229    pub bump: u8,
230
231    /// The 256-bit merkle root.
232    pub root: [u8; 32],
233
234    /// [Mint] of the token to be distributed.
235    pub mint: Pubkey,
236    /// Maximum number of tokens that can ever be claimed from this [MerkleDistributor].
237    pub max_total_claim: u64,
238    /// Maximum number of nodes that can ever be claimed from this [MerkleDistributor].
239    pub max_num_nodes: u64,
240    /// Total amount of tokens that have been claimed.
241    pub total_amount_claimed: u64,
242    /// Number of nodes that have been claimed.
243    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/// Holds whether or not a claimant has claimed tokens.
251///
252/// TODO: this is probably better stored as the node that was verified.
253#[account]
254#[derive(Default)]
255pub struct ClaimStatus {
256    /// If true, the tokens have been claimed.
257    pub is_claimed: bool,
258    /// Authority that claimed the tokens.
259    pub claimant: Pubkey,
260    /// When the tokens were claimed.
261    pub claimed_at: i64,
262    /// Amount of tokens claimed.
263    pub amount: u64,
264}
265
266impl ClaimStatus {
267    pub const LEN: usize = 1 + PUBKEY_BYTES + 8 + 8;
268}
269
270/// Emitted when tokens are claimed.
271#[event]
272pub struct ClaimedEvent {
273    /// Index of the claim.
274    pub index: u64,
275    /// User that claimed.
276    pub claimant: Pubkey,
277    /// Amount of tokens to distribute.
278    pub amount: u64,
279}
280
281/// Error codes.
282#[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}