use anchor_lang::{ context::Context, prelude::*, solana_program::hash::hashv, system_program::System, Accounts, Key, Result, }; use anchor_spl::{ token, token::{Token, TokenAccount}, }; use jito_merkle_verify::verify; use crate::{ error::ErrorCode, state::{ claim_status::ClaimStatus, claimed_event::NewClaimEvent, merkle_distributor::MerkleDistributor, }, }; // We need to discern between leaf and intermediate nodes to prevent trivial second // pre-image attacks. // https://flawed.net.nz/2018/02/21/attacking-merkle-trees-with-a-second-preimage-attack const LEAF_PREFIX: &[u8] = &[0]; /// [merkle_distributor::new_claim] accounts. #[derive(Accounts)] pub struct NewClaim<'info> { /// The [MerkleDistributor]. #[account(mut)] pub distributor: Account<'info, MerkleDistributor>, /// Claim status PDA #[account( init, seeds = [ b"ClaimStatus".as_ref(), claimant.key().to_bytes().as_ref(), distributor.key().to_bytes().as_ref() ], bump, space = ClaimStatus::LEN, payer = claimant )] pub claim_status: Account<'info, ClaimStatus>, /// Distributor ATA containing the tokens to distribute. #[account( mut, associated_token::mint = distributor.mint, associated_token::authority = distributor.key(), address = distributor.token_vault )] pub from: Account<'info, TokenAccount>, /// Account to send the claimed tokens to. #[account( mut, token::mint=distributor.mint, token::authority = claimant.key() )] pub to: Account<'info, TokenAccount>, /// Who is claiming the tokens. #[account(mut, address = to.owner @ ErrorCode::OwnerMismatch)] pub claimant: Signer<'info>, /// SPL [Token] program. pub token_program: Program<'info, Token>, /// The [System] program. pub system_program: Program<'info, System>, } /// Initializes a new claim from the [MerkleDistributor]. /// 1. Increments num_nodes_claimed by 1 /// 2. Initializes claim_status /// 3. Transfers claim_status.unlocked_amount to the claimant /// 4. Increments total_amount_claimed by claim_status.unlocked_amount /// CHECK: /// 1. The claim window has not expired and the distributor has not been clawed back /// 2. The claimant is the owner of the to account /// 3. Num nodes claimed is less than max_num_nodes /// 4. The merkle proof is valid #[allow(clippy::result_large_err)] pub fn handle_new_claim( ctx: Context, amount_unlocked: u64, amount_locked: u64, proof: Vec<[u8; 32]>, ) -> Result<()> { let distributor = &mut ctx.accounts.distributor; let curr_ts = Clock::get()?.unix_timestamp; let curr_slot = Clock::get()?.slot; require!(!distributor.clawed_back, ErrorCode::ClaimExpired); require!( distributor.enable_slot <= curr_slot, ErrorCode::ClaimingIsNotStarted ); require!( distributor.start_ts <= curr_ts, ErrorCode::ClaimingIsNotStarted ); distributor.num_nodes_claimed = distributor .num_nodes_claimed .checked_add(1) .ok_or(ErrorCode::ArithmeticError)?; require!( distributor.num_nodes_claimed <= distributor.max_num_nodes, ErrorCode::MaxNodesExceeded ); let claimant_account = &ctx.accounts.claimant; // Verify the merkle proof. let node = hashv(&[ &claimant_account.key().to_bytes(), &amount_unlocked.to_le_bytes(), &amount_locked.to_le_bytes(), ]); let distributor_key = ctx.accounts.distributor.key(); let distributor = &ctx.accounts.distributor; let node = hashv(&[LEAF_PREFIX, &node.to_bytes()]); require!( verify(proof, distributor.root, node.to_bytes()), ErrorCode::InvalidProof ); let claim_status = &mut ctx.accounts.claim_status; // Seed initial values claim_status.claimant = claimant_account.key(); claim_status.locked_amount = amount_locked; claim_status.locked_amount_withdrawn = 0; claim_status.unlocked_amount = amount_unlocked; claim_status.closable = distributor.closable; claim_status.distributor = distributor_key; claim_status.update_unlocked_amount_claimed( curr_ts, distributor.start_ts, distributor.end_ts, )?; let amount_forgone = claim_status.get_unlocked_amount_forgone()?; let seeds = [ b"MerkleDistributor".as_ref(), &distributor.mint.to_bytes(), &distributor.version.to_le_bytes(), &[ctx.accounts.distributor.bump], ]; token::transfer( CpiContext::new( ctx.accounts.token_program.to_account_info(), token::Transfer { from: ctx.accounts.from.to_account_info(), to: ctx.accounts.to.to_account_info(), authority: ctx.accounts.distributor.to_account_info(), }, ) .with_signer(&[&seeds[..]]), claim_status.unlocked_amount_claimed, )?; let distributor = &mut ctx.accounts.distributor; distributor.total_amount_claimed = distributor .total_amount_claimed .checked_add(claim_status.unlocked_amount_claimed) .ok_or(ErrorCode::ArithmeticError)?; distributor.total_amount_forgone = distributor .total_amount_forgone .checked_add(amount_forgone) .ok_or(ErrorCode::ArithmeticError)?; require!( distributor .total_amount_claimed .checked_add(distributor.total_amount_forgone) .ok_or(ErrorCode::ArithmeticError)? <= distributor.max_total_claim, ErrorCode::ExceededMaxClaim ); // Note: might get truncated, do not rely on msg!( "Created new claim with {} claimed and {} forgone", claim_status.unlocked_amount_claimed, amount_forgone, ); emit!(NewClaimEvent { claimant: claimant_account.key(), timestamp: curr_ts, amount_claimed: claim_status.unlocked_amount_claimed, amount_forgone: amount_forgone, }); Ok(()) }