diff --git a/programs/merkle-distributor/Cargo.toml b/programs/merkle-distributor/Cargo.toml index 17e5cb3..28942fc 100644 --- a/programs/merkle-distributor/Cargo.toml +++ b/programs/merkle-distributor/Cargo.toml @@ -18,7 +18,7 @@ default = [] localnet = [] [dependencies] -anchor-lang = "0.28.0" +anchor-lang = { version = "0.28.0", features = ["init-if-needed"] } anchor-spl = "0.28.0" bytemuck = "1.14.0" jito-merkle-verify = { path = "../../verify" } diff --git a/programs/merkle-distributor/src/error.rs b/programs/merkle-distributor/src/error.rs index 3cc64e7..088bc6c 100644 --- a/programs/merkle-distributor/src/error.rs +++ b/programs/merkle-distributor/src/error.rs @@ -59,4 +59,10 @@ pub enum ErrorCode { InvalidLocker, #[msg("Escrow is not max lock")] EscrowIsNotMaxLock, + #[msg("Invalid remaining accounts")] + InvalidRemainingAccounts, + #[msg("Invalid account")] + InvalidAccount, + #[msg("Canopy root miss match with real root")] + CanopyRootMissMatch, } diff --git a/programs/merkle-distributor/src/instructions/admin/create_canopy_tree.rs b/programs/merkle-distributor/src/instructions/admin/create_canopy_tree.rs new file mode 100644 index 0000000..c0eba71 --- /dev/null +++ b/programs/merkle-distributor/src/instructions/admin/create_canopy_tree.rs @@ -0,0 +1,51 @@ +use crate::{ + error::ErrorCode, + state::{canopy_tree::CanopyTree, merkle_distributor::MerkleDistributor}, +}; +use anchor_lang::prelude::*; + +#[derive(Accounts)] +#[instruction(depth: u8)] +pub struct CreateCanopyTree<'info> { + /// [CanopyTree] + #[account( + init, + seeds = [ + b"CanopyTree".as_ref(), + distributor.key().to_bytes().as_ref(), + ], + bump, + space = CanopyTree::space(depth as usize), + payer = payer + )] + pub canopy_tree: Account<'info, CanopyTree>, + + /// The [MerkleDistributor]. + pub distributor: AccountLoader<'info, MerkleDistributor>, + + /// Payer wallet, responsible for creating the distributor and paying for the transaction. + #[account(mut)] + pub payer: Signer<'info>, + + /// The [System] program. + pub system_program: Program<'info, System>, +} + +pub fn handle_create_canopy_tree( + ctx: Context, + depth: u8, + root: [u8; 32], + canopy_nodes: Vec<[u8; 32]>, +) -> Result<()> { + let canopy_tree = &mut ctx.accounts.canopy_tree; + + let verify_canopy_root = canopy_tree.verify_canopy_root(root, canopy_nodes.clone()); + require!(verify_canopy_root, ErrorCode::CanopyRootMissMatch); + + canopy_tree.root = root; + canopy_tree.depth = depth; + canopy_tree.nodes = canopy_nodes; + canopy_tree.distributor = ctx.accounts.distributor.key(); + + Ok(()) +} diff --git a/programs/merkle-distributor/src/instructions/admin/fund_distributor_root.rs b/programs/merkle-distributor/src/instructions/admin/fund_distributor_root.rs new file mode 100644 index 0000000..9e2794e --- /dev/null +++ b/programs/merkle-distributor/src/instructions/admin/fund_distributor_root.rs @@ -0,0 +1,73 @@ +use anchor_lang::{ + accounts::{account::Account, program::Program}, + context::Context, + prelude::*, + Accounts, Result, ToAccountInfo, +}; +use anchor_spl::{ + associated_token::AssociatedToken, + token::{self, Mint, Token, TokenAccount}, +}; + +use crate::state::distributor_root::DistributorRoot; + +/// Accounts required for distributing tokens from the parent vault to distributor vaults. +#[derive(Accounts)] +pub struct FundDistributorRoot<'info> { + /// The [DistributorRoot] + #[account(mut, has_one = mint)] + pub distributor_root: AccountLoader<'info, DistributorRoot>, + + /// Distributor root vault + #[account( + init_if_needed, + associated_token::mint = mint, + associated_token::authority = distributor_root, + payer = payer + )] + pub distributor_root_vault: Account<'info, TokenAccount>, + + /// The mint to distribute. + pub mint: Account<'info, Mint>, + + /// Payer. + #[account(mut)] + pub payer: Signer<'info>, + + /// Payer Token Account. + #[account(mut)] + pub payer_token: Account<'info, TokenAccount>, + + /// The [System] program. + pub system_program: Program<'info, System>, + + /// The [Token] program. + pub token_program: Program<'info, Token>, + + // Associated token program. + pub associated_token_program: Program<'info, AssociatedToken>, +} + +pub fn handle_fund_distributor_root( + ctx: Context, + max_amount: u64, +) -> Result<()> { + let fund_amount = { + let mut distributor_root = ctx.accounts.distributor_root.load_mut()?; + distributor_root.get_and_set_fund_amount(max_amount)? + }; + + token::transfer( + CpiContext::new( + ctx.accounts.token_program.to_account_info(), + token::Transfer { + from: ctx.accounts.payer_token.to_account_info(), + to: ctx.accounts.distributor_root_vault.to_account_info(), + authority: ctx.accounts.payer.to_account_info(), + }, + ), + fund_amount, + )?; + + Ok(()) +} diff --git a/programs/merkle-distributor/src/instructions/admin/fund_merkle_distributor_from_root.rs b/programs/merkle-distributor/src/instructions/admin/fund_merkle_distributor_from_root.rs new file mode 100644 index 0000000..fb65cb4 --- /dev/null +++ b/programs/merkle-distributor/src/instructions/admin/fund_merkle_distributor_from_root.rs @@ -0,0 +1,78 @@ +use anchor_lang::{ + accounts::{account::Account, program::Program}, + context::{Context, CpiContext}, + prelude::*, + Accounts, Result, ToAccountInfo, +}; +use anchor_spl::token::{self, Token, TokenAccount}; + +use crate::state::{distributor_root::DistributorRoot, merkle_distributor::MerkleDistributor}; + +/// Accounts required for distributing tokens from the parent vault to distributor vaults. +#[derive(Accounts)] +pub struct FundMerkleDisitributorFromRoot<'info> { + /// The [DistributorRoot]. + pub distributor_root: AccountLoader<'info, DistributorRoot>, + + /// Distributor root vault containing the tokens to distribute to distributor vault. + #[account( + mut, + associated_token::mint = distributor_root.load()?.mint, + associated_token::authority = distributor_root.key(), + address = distributor_root.load()?.distributor_root_vault, + )] + pub distributor_root_vault: Account<'info, TokenAccount>, + + /// The [MerkleDistributor]. + #[account(mut, constraint = distributor.load()?.distributor_root == distributor_root.key())] + pub distributor: AccountLoader<'info, MerkleDistributor>, + + /// Distributor vault + #[account( + mut, + associated_token::mint = distributor.load()?.mint, + associated_token::authority = distributor.key(), + )] + pub distributor_vault: Account<'info, TokenAccount>, + + /// SPL [Token] program. + pub token_program: Program<'info, Token>, +} + +/// Handles the distribution of tokens from the parent vault to multiple distributor vaults. +pub fn handle_fund_merkle_distributor_from_root<'info>( + ctx: Context<'_, '_, '_, 'info, FundMerkleDisitributorFromRoot<'info>>, +) -> Result<()> { + let distributor_root = ctx.accounts.distributor_root.load()?; + let signer = distributor_root.signer(); + let seeds = signer.seeds(); + + let mut distributor_state = ctx.accounts.distributor.load_mut()?; + + // Check distributor has been funded token + if distributor_state.funded_amount == 0 { + let fund_amount = distributor_state.max_total_claim; + token::transfer( + CpiContext::new( + ctx.accounts.token_program.to_account_info(), + token::Transfer { + from: ctx.accounts.distributor_root_vault.to_account_info(), + to: ctx.accounts.distributor_vault.to_account_info(), + authority: ctx.accounts.distributor_root.to_account_info(), + }, + ) + .with_signer(&[&seeds[..]]), + fund_amount, + )?; + + distributor_state.accumulate_funded_amount(fund_amount)?; + + msg!( + "Funded {} tokens to distributor version {}.", + fund_amount, + distributor_state.version + ); + } + + Ok(()) +} diff --git a/programs/merkle-distributor/src/instructions/admin/mod.rs b/programs/merkle-distributor/src/instructions/admin/mod.rs index 14f9a9a..f7a8c36 100644 --- a/programs/merkle-distributor/src/instructions/admin/mod.rs +++ b/programs/merkle-distributor/src/instructions/admin/mod.rs @@ -5,6 +5,10 @@ pub mod new_distributor; pub mod set_activation_point; pub mod set_admin; pub mod set_clawback_receiver; +pub mod new_distributor_root; +pub mod fund_merkle_distributor_from_root; +pub mod fund_distributor_root; +pub mod create_canopy_tree; pub use clawback::*; pub use close_claim_status::*; @@ -15,3 +19,7 @@ pub use set_admin::*; pub use set_clawback_receiver::*; pub mod set_operator; pub use set_operator::*; +pub use new_distributor_root::*; +pub use fund_merkle_distributor_from_root::*; +pub use fund_distributor_root::*; +pub use create_canopy_tree::*; diff --git a/programs/merkle-distributor/src/instructions/admin/new_distributor.rs b/programs/merkle-distributor/src/instructions/admin/new_distributor.rs index d5930a6..1fa043e 100644 --- a/programs/merkle-distributor/src/instructions/admin/new_distributor.rs +++ b/programs/merkle-distributor/src/instructions/admin/new_distributor.rs @@ -1,10 +1,12 @@ use crate::error::ErrorCode::ArithmeticError; +use crate::state::distributor_root::DistributorRoot; use crate::state::merkle_distributor::{ActivationType, ClaimType}; use crate::{ error::ErrorCode, state::merkle_distributor::{AirdropBonus, MerkleDistributor}, }; use anchor_lang::{account, context::Context, prelude::*, Accounts, Key, ToAccountInfo}; +use anchor_spl::associated_token::AssociatedToken; use anchor_spl::token::{Mint, Token, TokenAccount}; #[cfg(feature = "localnet")] @@ -13,10 +15,9 @@ const SECONDS_PER_DAY: i64 = 0; #[cfg(not(feature = "localnet"))] const SECONDS_PER_DAY: i64 = 24 * 3600; // 24 hours * 3600 seconds -#[derive(AnchorSerialize, AnchorDeserialize, InitSpace)] +#[derive(AnchorSerialize, AnchorDeserialize, Debug)] pub struct NewDistributorParams { pub version: u64, - pub root: [u8; 32], pub total_claim: u64, pub max_num_nodes: u64, pub start_vesting_ts: i64, @@ -29,7 +30,7 @@ pub struct NewDistributorParams { pub bonus_vesting_duration: u64, pub claim_type: u8, pub operator: Pubkey, - pub locker: Pubkey, + pub locker: Pubkey } impl NewDistributorParams { @@ -123,10 +124,14 @@ pub struct NewDistributor<'info> { ], bump, space = 8 + MerkleDistributor::INIT_SPACE, - payer = admin + payer = payer )] pub distributor: AccountLoader<'info, MerkleDistributor>, + /// The [DistributorRoot]. + #[account(mut)] + pub distributor_root: AccountLoader<'info, DistributorRoot>, + /// Base key of the distributor. pub base: Signer<'info>, @@ -140,21 +145,28 @@ pub struct NewDistributor<'info> { /// Token vault /// Should create previously #[account( + init_if_needed, associated_token::mint = mint, associated_token::authority=distributor, + payer = payer )] pub token_vault: Account<'info, TokenAccount>, - /// Admin wallet, responsible for creating the distributor and paying for the transaction. - /// Also has the authority to set the clawback receiver and change itself. + /// CHECK: This account is not use to read or write + pub admin: UncheckedAccount<'info>, + + /// Payer wallet, responsible for creating the distributor and paying for the transaction. #[account(mut)] - pub admin: Signer<'info>, + pub payer: Signer<'info>, /// The [System] program. pub system_program: Program<'info, System>, /// The [Token] program. pub token_program: Program<'info, Token>, + + // Associated token program. + pub associated_token_program: Program<'info, AssociatedToken>, } /// Creates a new [MerkleDistributor]. @@ -172,12 +184,9 @@ pub fn handle_new_distributor( params: &NewDistributorParams, ) -> Result<()> { params.validate()?; - let mut distributor = ctx.accounts.distributor.load_init()?; - distributor.bump = *ctx.bumps.get("distributor").unwrap(); distributor.version = params.version; - distributor.root = params.root; distributor.mint = ctx.accounts.mint.key(); distributor.token_vault = ctx.accounts.token_vault.key(); distributor.max_total_claim = params.get_max_total_claim()?; @@ -200,6 +209,7 @@ pub fn handle_new_distributor( distributor.activation_type = params.activation_type; distributor.operator = params.operator; distributor.locker = params.locker; + distributor.distributor_root = ctx.accounts.distributor_root.key(); // Note: might get truncated, do not rely on msg! { @@ -220,5 +230,11 @@ pub fn handle_new_distributor( distributor.claim_type, }; + drop(distributor); + + // increase total distributor created + let mut distributor_root = ctx.accounts.distributor_root.load_mut()?; + distributor_root.update_new_distributor()?; + Ok(()) } diff --git a/programs/merkle-distributor/src/instructions/admin/new_distributor_root.rs b/programs/merkle-distributor/src/instructions/admin/new_distributor_root.rs new file mode 100644 index 0000000..7014625 --- /dev/null +++ b/programs/merkle-distributor/src/instructions/admin/new_distributor_root.rs @@ -0,0 +1,72 @@ +use crate::state::distributor_root::DistributorRoot; +use anchor_lang::prelude::*; +use anchor_spl::{ + associated_token::AssociatedToken, + token::{Mint, Token, TokenAccount}, +}; + +#[derive(Accounts)] +pub struct NewDistributorRoot<'info> { + /// [DistributorRoot] + #[account( + init, + seeds = [ + b"DistributorRoot".as_ref(), + base.key().to_bytes().as_ref(), + mint.key().to_bytes().as_ref(), + ], + bump, + space = 8 + DistributorRoot::INIT_SPACE, + payer = payer + )] + pub distributor_root: AccountLoader<'info, DistributorRoot>, + + /// Distributor root vault + #[account( + init_if_needed, + associated_token::mint = mint, + associated_token::authority = distributor_root, + payer = payer + )] + pub distributor_root_vault: Account<'info, TokenAccount>, + + /// The mint to distribute. + pub mint: Account<'info, Mint>, + + /// Base key of the distributor. + pub base: Signer<'info>, + + /// CHECK: This account is not use to read or write + pub admin: UncheckedAccount<'info>, + + /// Payer wallet, responsible for creating the distributor and paying for the transaction. + #[account(mut)] + pub payer: Signer<'info>, + + /// The [System] program. + pub system_program: Program<'info, System>, + + /// The [Token] program. + pub token_program: Program<'info, Token>, + + // Associated token program. + pub associated_token_program: Program<'info, AssociatedToken>, +} + +pub fn handle_new_distributor_root( + ctx: Context, + max_claim_amount: u64, + max_distributor: u64, +) -> Result<()> { + let mut distributor_root = ctx.accounts.distributor_root.load_init()?; + + distributor_root.bump = *ctx.bumps.get("distributor_root").unwrap(); + distributor_root.mint = ctx.accounts.mint.key(); + distributor_root.base = ctx.accounts.base.key(); + distributor_root.admin = ctx.accounts.admin.key(); + distributor_root.max_claim_amount = max_claim_amount; + distributor_root.max_distributor = max_distributor; + distributor_root.distributor_root_vault = ctx.accounts.distributor_root_vault.key(); + + Ok(()) +} diff --git a/programs/merkle-distributor/src/instructions/claim/new_claim.rs b/programs/merkle-distributor/src/instructions/claim/new_claim.rs index 691fb89..c1908bc 100644 --- a/programs/merkle-distributor/src/instructions/claim/new_claim.rs +++ b/programs/merkle-distributor/src/instructions/claim/new_claim.rs @@ -6,12 +6,11 @@ use anchor_spl::{ token, token::{Token, TokenAccount}, }; -use jito_merkle_verify::verify; use crate::{ error::ErrorCode, state::{ - claim_status::ClaimStatus, claimed_event::NewClaimEvent, + canopy_tree::CanopyTree, claim_status::ClaimStatus, claimed_event::NewClaimEvent, merkle_distributor::MerkleDistributor, }, }; @@ -28,6 +27,10 @@ pub struct NewClaim<'info> { #[account(mut)] pub distributor: AccountLoader<'info, MerkleDistributor>, + /// The [CanopyTree]. + #[account(has_one = distributor)] + pub canopy_tree: Account<'info, CanopyTree>, + /// Claim status PDA #[account( init, @@ -84,10 +87,11 @@ pub fn handle_new_claim( ctx: Context, amount_unlocked: u64, amount_locked: u64, + leaf_index: u32, proof: Vec<[u8; 32]>, ) -> Result<()> { let mut distributor = ctx.accounts.distributor.load_mut()?; - + let canopy_tree = &ctx.accounts.canopy_tree; require!(!distributor.clawed_back(), ErrorCode::ClaimExpired); // check operator @@ -117,10 +121,8 @@ pub fn handle_new_claim( let node = hashv(&[LEAF_PREFIX, &node.to_bytes()]); - require!( - verify(proof, distributor.root, node.to_bytes()), - ErrorCode::InvalidProof - ); + let verify_leaf = canopy_tree.verify_leaf(proof, node.to_bytes(), leaf_index); + require!(verify_leaf, ErrorCode::InvalidProof); let mut claim_status = ctx.accounts.claim_status.load_init()?; diff --git a/programs/merkle-distributor/src/instructions/claim_and_stake/new_claim_and_stake.rs b/programs/merkle-distributor/src/instructions/claim_and_stake/new_claim_and_stake.rs index f99d37d..c1d027d 100644 --- a/programs/merkle-distributor/src/instructions/claim_and_stake/new_claim_and_stake.rs +++ b/programs/merkle-distributor/src/instructions/claim_and_stake/new_claim_and_stake.rs @@ -1,3 +1,4 @@ +use crate::state::canopy_tree::CanopyTree; use crate::LEAF_PREFIX; use crate::{ error::ErrorCode, @@ -11,7 +12,6 @@ use anchor_lang::{ Key, Result, }; use anchor_spl::token::{Token, TokenAccount}; -use jito_merkle_verify::verify; use locked_voter::program::LockedVoter as Voter; use locked_voter::{self as voter, Escrow, Locker}; @@ -23,6 +23,10 @@ pub struct NewClaimAndStake<'info> { #[account(mut, has_one = locker)] pub distributor: AccountLoader<'info, MerkleDistributor>, + /// The [CanopyTree]. + #[account(has_one = distributor)] + pub canopy_tree: Account<'info, CanopyTree>, + /// Claim status PDA #[account( init, @@ -90,10 +94,11 @@ pub fn handle_new_claim_and_stake( ctx: Context, amount_unlocked: u64, amount_locked: u64, + leaf_index: u32, proof: Vec<[u8; 32]>, ) -> Result<()> { let mut distributor = ctx.accounts.distributor.load_mut()?; - + let canopy_tree = &ctx.accounts.canopy_tree; require!(!distributor.clawed_back(), ErrorCode::ClaimExpired); // check operator @@ -126,10 +131,8 @@ pub fn handle_new_claim_and_stake( let node = hashv(&[LEAF_PREFIX, &node.to_bytes()]); - require!( - verify(proof, distributor.root, node.to_bytes()), - ErrorCode::InvalidProof - ); + let verify_leaf = canopy_tree.verify_leaf(proof, node.to_bytes(), leaf_index); + require!(verify_leaf, ErrorCode::InvalidProof); let mut claim_status = ctx.accounts.claim_status.load_init()?; diff --git a/programs/merkle-distributor/src/lib.rs b/programs/merkle-distributor/src/lib.rs index 3cc1f9a..c7eb08a 100644 --- a/programs/merkle-distributor/src/lib.rs +++ b/programs/merkle-distributor/src/lib.rs @@ -34,9 +34,24 @@ security_txt! { #[program] pub mod merkle_distributor { + use super::*; /// ADMIN FUNCTIONS //// + #[allow(clippy::result_large_err)] + pub fn new_distributor_root( + ctx: Context, + max_claim_amount: u64, + max_distributor: u64, + ) -> Result<()> { + handle_new_distributor_root(ctx, max_claim_amount, max_distributor) + } + + #[allow(clippy::result_large_err)] + pub fn fund_distributor_root(ctx: Context, max_amount: u64) -> Result<()> { + handle_fund_distributor_root(ctx, max_amount) + } + #[allow(clippy::result_large_err)] pub fn new_distributor( ctx: Context, @@ -45,6 +60,22 @@ pub mod merkle_distributor { handle_new_distributor(ctx, ¶ms) } + pub fn create_canopy_tree( + ctx: Context, + depth: u8, + root: [u8; 32], + canopy_nodes: Vec<[u8; 32]>, + ) -> Result<()> { + handle_create_canopy_tree(ctx, depth, root, canopy_nodes) + } + + #[allow(clippy::result_large_err)] + pub fn fund_merkle_distributor_from_root<'info>( + ctx: Context<'_, '_, '_, 'info, FundMerkleDisitributorFromRoot<'info>>, + ) -> Result<()> { + handle_fund_merkle_distributor_from_root(ctx) + } + /// only available in test phase #[allow(clippy::result_large_err)] pub fn close_distributor(ctx: Context) -> Result<()> { @@ -91,9 +122,10 @@ pub mod merkle_distributor { ctx: Context, amount_unlocked: u64, amount_locked: u64, + leaf_index: u32, proof: Vec<[u8; 32]>, ) -> Result<()> { - handle_new_claim(ctx, amount_unlocked, amount_locked, proof) + handle_new_claim(ctx, amount_unlocked, amount_locked, leaf_index, proof) } #[allow(clippy::result_large_err)] @@ -106,9 +138,10 @@ pub mod merkle_distributor { ctx: Context, amount_unlocked: u64, amount_locked: u64, + leaf_index: u32, proof: Vec<[u8; 32]>, ) -> Result<()> { - handle_new_claim_and_stake(ctx, amount_unlocked, amount_locked, proof) + handle_new_claim_and_stake(ctx, amount_unlocked, amount_locked, leaf_index, proof) } #[allow(clippy::result_large_err)] diff --git a/programs/merkle-distributor/src/state/canopy_tree.rs b/programs/merkle-distributor/src/state/canopy_tree.rs new file mode 100644 index 0000000..a326f9b --- /dev/null +++ b/programs/merkle-distributor/src/state/canopy_tree.rs @@ -0,0 +1,177 @@ +use anchor_lang::prelude::*; +use solana_program::hash::hashv; + +#[account] +#[derive(Default)] +pub struct CanopyTree { + /// The 256-bit merkle root. + pub root: [u8; 32], + /// The depth of merkle will store onchain + /// With `depth``: total levels from the root to leaves: depth + 1 + pub depth: u8, + /// A vector of node hashes representing canopy leaves node + pub nodes: Vec<[u8; 32]>, + /// The distributor associated with this Merkle tree + pub distributor: Pubkey, + /// Buffer + pub buffer: [u64; 5], +} + +impl CanopyTree { + /// Maximum nodes at level i: 2^i + /// Example: depth = 2 => maximum canopy nodes = 2^2. + pub fn space(depth: usize) -> usize { + let max_nodes = 1 << depth; + let size = (8 + 33 + 4 + 32 * max_nodes + 32 + 8 * 5) as usize; + size + } + + pub fn verify_canopy_root(&self, root: [u8; 32], canopy_nodes: Vec<[u8; 32]>) -> bool { + let mut current_nodes = canopy_nodes; + + while current_nodes.len() > 1 { + let mut next_level: Vec<[u8; 32]> = vec![]; + let mut i = 0; + while i < current_nodes.len() { + if i + 1 < current_nodes.len() { + let lsib = current_nodes[i]; + let rsib = current_nodes[i + 1]; + let hashed_node; + if lsib <= rsib { + hashed_node = hashv(&[&[1u8], &lsib, &rsib]).to_bytes(); + } else { + hashed_node = hashv(&[&[1u8], &rsib, &lsib]).to_bytes(); + }; + next_level.push(hashed_node); + i += 2; + } else { + next_level.push(current_nodes[i]); + i += 1; + } + } + current_nodes = next_level; + } + + current_nodes[0] == root + } + + pub fn verify_leaf(&self, proof: Vec<[u8; 32]>, leaf: [u8; 32], leaf_index: u32) -> bool { + let mut current_hash = leaf; + let mut current_index = leaf_index; + for proof_element in proof.into_iter() { + if current_hash <= proof_element { + current_hash = hashv(&[&[1u8], ¤t_hash, &proof_element]).to_bytes(); + } else { + current_hash = hashv(&[&[1u8], &proof_element, ¤t_hash]).to_bytes(); + } + + current_index = current_index.checked_div(2).unwrap(); + } + + // Compare current_hash vs canopy nodes + let expected_hash = self.nodes[current_index as usize]; + + current_hash == expected_hash + } +} + +#[cfg(test)] +mod canopy_test { + use super::*; + const ROOT: [u8; 32] = [ + 149, 44, 101, 93, 7, 225, 23, 191, 86, 53, 145, 35, 119, 19, 172, 27, 31, 77, 106, 39, 46, + 55, 2, 145, 190, 58, 158, 147, 10, 28, 75, 191, + ]; + + #[test] + fn test_verify_canopy_nodes_case_layer1() { + let nodes: Vec<[u8; 32]> = Vec::from([ + [ + 98, 122, 112, 160, 154, 57, 123, 38, 138, 237, 90, 29, 104, 179, 248, 208, 177, + 206, 20, 24, 18, 197, 73, 220, 182, 102, 240, 180, 223, 71, 3, 113, + ], + [ + 170, 65, 27, 221, 57, 223, 88, 94, 203, 241, 104, 246, 54, 119, 188, 225, 16, 65, + 109, 98, 73, 121, 49, 218, 50, 213, 136, 253, 205, 65, 213, 225, + ], + ]); + + let canopy_nodes = CanopyTree::default(); + let result = canopy_nodes.verify_canopy_root(ROOT, nodes); + assert_eq!(result, true); + } + + #[test] + fn test_verify_canopy_nodes_case_layer2() { + let nodes: Vec<[u8; 32]> = Vec::from([ + [ + 144, 158, 139, 112, 26, 179, 41, 202, 82, 243, 65, 200, 218, 132, 105, 194, 85, 30, + 7, 183, 154, 178, 33, 139, 166, 206, 248, 31, 11, 210, 164, 215, + ], + [ + 230, 163, 113, 64, 107, 48, 40, 9, 189, 18, 77, 16, 53, 248, 247, 199, 178, 127, + 181, 108, 223, 253, 137, 55, 234, 87, 205, 210, 112, 189, 107, 151, + ], + [ + 170, 65, 27, 221, 57, 223, 88, 94, 203, 241, 104, 246, 54, 119, 188, 225, 16, 65, + 109, 98, 73, 121, 49, 218, 50, 213, 136, 253, 205, 65, 213, 225, + ], + ]); + + let canopy_nodes = CanopyTree::default(); + let result = canopy_nodes.verify_canopy_root(ROOT, nodes); + assert_eq!(result, true); + } + + #[test] + fn test_verify_canopy_nodes_case_layer3() { + let nodes: Vec<[u8; 32]> = Vec::from([ + [ + 144, 158, 139, 112, 26, 179, 41, 202, 82, 243, 65, 200, 218, 132, 105, 194, 85, 30, + 7, 183, 154, 178, 33, 139, 166, 206, 248, 31, 11, 210, 164, 215, + ], + [ + 230, 163, 113, 64, 107, 48, 40, 9, 189, 18, 77, 16, 53, 248, 247, 199, 178, 127, + 181, 108, 223, 253, 137, 55, 234, 87, 205, 210, 112, 189, 107, 151, + ], + [ + 170, 65, 27, 221, 57, 223, 88, 94, 203, 241, 104, 246, 54, 119, 188, 225, 16, 65, + 109, 98, 73, 121, 49, 218, 50, 213, 136, 253, 205, 65, 213, 225, + ], + ]); + + let canopy_nodes = CanopyTree::default(); + let result = canopy_nodes.verify_canopy_root(ROOT, nodes); + assert_eq!(result, true); + } + + #[test] + fn test_verify_canopy_nodes_case_layer4() { + let nodes: Vec<[u8; 32]> = Vec::from([ + [ + 33, 157, 45, 11, 26, 60, 103, 187, 252, 243, 171, 223, 86, 120, 181, 97, 192, 132, + 46, 183, 5, 136, 21, 227, 189, 227, 15, 100, 87, 60, 126, 125, + ], + [ + 41, 94, 138, 1, 107, 84, 242, 150, 6, 169, 100, 252, 138, 82, 36, 24, 252, 84, 228, + 124, 237, 219, 68, 212, 215, 111, 253, 141, 173, 45, 35, 229, + ], + [ + 51, 147, 193, 24, 169, 158, 140, 118, 17, 108, 141, 221, 212, 82, 234, 88, 4, 64, + 109, 209, 148, 214, 84, 78, 184, 8, 130, 210, 172, 169, 198, 151, + ], + [ + 91, 133, 169, 74, 229, 46, 159, 235, 129, 154, 97, 186, 225, 169, 62, 105, 125, 67, + 158, 69, 168, 115, 109, 254, 205, 237, 71, 42, 15, 240, 64, 192, + ], + [ + 170, 65, 27, 221, 57, 223, 88, 94, 203, 241, 104, 246, 54, 119, 188, 225, 16, 65, + 109, 98, 73, 121, 49, 218, 50, 213, 136, 253, 205, 65, 213, 225, + ], + ]); + + let canopy_nodes = CanopyTree::default(); + let result = canopy_nodes.verify_canopy_root(ROOT, nodes); + assert_eq!(result, true); + } +} diff --git a/programs/merkle-distributor/src/state/claim_status.rs b/programs/merkle-distributor/src/state/claim_status.rs index 62ad7c5..6e8a25e 100644 --- a/programs/merkle-distributor/src/state/claim_status.rs +++ b/programs/merkle-distributor/src/state/claim_status.rs @@ -26,10 +26,12 @@ pub struct ClaimStatus { /// padding 0 pub padding_0: [u8; 7], /// padding 1 - pub padding_1: u128, + pub padding_1: u64, + /// buffer + pub buffer: u128, } -const_assert!(ClaimStatus::INIT_SPACE == 152); +const_assert!(ClaimStatus::INIT_SPACE == 160); impl ClaimStatus { /// Returns amount withdrawable, factoring in unlocked tokens and previous withdraws. diff --git a/programs/merkle-distributor/src/state/distributor_root.rs b/programs/merkle-distributor/src/state/distributor_root.rs new file mode 100644 index 0000000..fe29800 --- /dev/null +++ b/programs/merkle-distributor/src/state/distributor_root.rs @@ -0,0 +1,77 @@ +use anchor_lang::prelude::*; +use crate::math::safe_math::SafeMath; +use static_assertions::const_assert; + +/// Parent Account: Authority of parent vault use to distribute fund to all distributors +#[account(zero_copy)] +#[derive(Default, InitSpace)] +pub struct DistributorRoot { + /// Bump seed. + pub bump: u8, + /// padding 0 + pub padding_0: [u8; 7], + /// Mint of the token to be distributed. + pub mint: Pubkey, + /// Base key of distributor root + pub base: Pubkey, + /// Admin + pub admin: Pubkey, + /// Token Address of distributor root vault + pub distributor_root_vault: Pubkey, + /// Max claim amount + pub max_claim_amount: u64, + /// Max distributor + pub max_distributor: u64, + /// total funded amount + pub total_funded_amount: u64, + /// total escrow created + pub total_distributor_created: u64, + /// Buffer for future use or alignment. + pub buffer: [u128; 5], +} + +const_assert!(DistributorRoot::INIT_SPACE == 248); + +impl DistributorRoot { + /// Returns the DistributorRootVaultSigner for PDA signing. + pub fn signer(&self) -> DistributorRootVaultSigner { + DistributorRootVaultSigner { + base: self.base.to_bytes(), + mint: self.mint.to_bytes(), + bump: [self.bump], + } + } + + pub fn get_and_set_fund_amount(&mut self, max_amount: u64) -> Result { + let max_amount_should_be_funded = + self.max_claim_amount.safe_sub(self.total_funded_amount)?; + let funded_amount = max_amount_should_be_funded.min(max_amount); + + self.total_funded_amount = self.total_funded_amount.safe_add(funded_amount)?; + Ok(funded_amount) + } + + pub fn update_new_distributor(&mut self) -> Result<()> { + self.total_distributor_created = self.total_distributor_created.safe_add(1)?; + Ok(()) + } +} + +/// Representing the signer seeds for the DistributorRootVault. +pub struct DistributorRootVaultSigner { + base: [u8; 32], + mint: [u8; 32], + bump: [u8; 1], +} + +impl DistributorRootVaultSigner { + /// Returns the seeds required for PDA signing. + pub fn seeds(&self) -> [&[u8]; 4] { + [ + b"DistributorRoot".as_ref(), + &self.base, + &self.mint, + &self.bump, + ] + } +} diff --git a/programs/merkle-distributor/src/state/merkle_distributor.rs b/programs/merkle-distributor/src/state/merkle_distributor.rs index 092643b..683a74e 100644 --- a/programs/merkle-distributor/src/state/merkle_distributor.rs +++ b/programs/merkle-distributor/src/state/merkle_distributor.rs @@ -29,8 +29,6 @@ pub enum ClaimType { #[account(zero_copy)] #[derive(Default, Debug, InitSpace)] pub struct MerkleDistributor { - /// The 256-bit merkle root. - pub root: [u8; 32], /// [Mint] of the token to be distributed. pub mint: Pubkey, /// base key of distributor. @@ -45,6 +43,8 @@ pub struct MerkleDistributor { pub locker: Pubkey, /// operator for signing in permissioned merkle tree pub operator: Pubkey, + /// Distributor root use to distribute fund to all distributor + pub distributor_root: Pubkey, /// Version of the airdrop pub version: u64, /// Maximum number of tokens that can ever be claimed from this [MerkleDistributor]. @@ -63,6 +63,8 @@ pub struct MerkleDistributor { pub clawback_start_ts: i64, /// this merkle tree is activated from this slot or timestamp pub activation_point: u64, + /// The total amount has been funded + pub funded_amount: u64, /// activation type, 0 means slot, 1 means timestamp pub activation_type: u8, /// claim type @@ -78,7 +80,10 @@ pub struct MerkleDistributor { // bonus multiplier pub airdrop_bonus: AirdropBonus, // padding 2 - pub padding_2: [u128; 5], + pub padding_2: [u8; 8], + // buffer + pub buffer: [u128; 5], + } #[zero_copy] @@ -223,9 +228,14 @@ impl MerkleDistributor { bump: [self.bump], } } + + pub fn accumulate_funded_amount(&mut self, amount: u64) -> Result<()> { + self.funded_amount = self.funded_amount.safe_add(amount)?; + Ok(()) + } } -const_assert!(MerkleDistributor::INIT_SPACE == 440); +const_assert!(MerkleDistributor::INIT_SPACE == 456); pub struct MerkleDistributorSigner { base: [u8; 32], diff --git a/programs/merkle-distributor/src/state/mod.rs b/programs/merkle-distributor/src/state/mod.rs index d347205..0695981 100644 --- a/programs/merkle-distributor/src/state/mod.rs +++ b/programs/merkle-distributor/src/state/mod.rs @@ -1,3 +1,5 @@ pub mod claim_status; pub mod claimed_event; pub mod merkle_distributor; +pub mod distributor_root; +pub mod canopy_tree; \ No newline at end of file diff --git a/target/types/merkle_distributor.ts b/target/types/merkle_distributor.ts index 58cb561..536f7cd 100644 --- a/target/types/merkle_distributor.ts +++ b/target/types/merkle_distributor.ts @@ -3,24 +3,24 @@ export type MerkleDistributor = { "name": "merkle_distributor", "instructions": [ { - "name": "newDistributor", + "name": "newDistributorRoot", "docs": [ "ADMIN FUNCTIONS ////" ], "accounts": [ { - "name": "distributor", + "name": "distributorRoot", "isMut": true, "isSigner": false, "docs": [ - "[MerkleDistributor]." + "[DistributorRoot]" ], "pda": { "seeds": [ { "kind": "const", "type": "string", - "value": "MerkleDistributor" + "value": "DistributorRoot" }, { "kind": "account", @@ -32,29 +32,16 @@ export type MerkleDistributor = { "type": "publicKey", "account": "Mint", "path": "mint" - }, - { - "kind": "arg", - "type": "u64", - "path": "version" } ] } }, { - "name": "base", - "isMut": false, - "isSigner": true, - "docs": [ - "Base key of the distributor." - ] - }, - { - "name": "clawbackReceiver", + "name": "distributorRootVault", "isMut": true, "isSigner": false, "docs": [ - "Clawback receiver token account" + "Distributor root vault" ] }, { @@ -66,21 +53,24 @@ export type MerkleDistributor = { ] }, { - "name": "tokenVault", + "name": "base", "isMut": false, - "isSigner": false, + "isSigner": true, "docs": [ - "Token vault", - "Should create previously" + "Base key of the distributor." ] }, { "name": "admin", + "isMut": false, + "isSigner": false + }, + { + "name": "payer", "isMut": true, "isSigner": true, "docs": [ - "Admin wallet, responsible for creating the distributor and paying for the transaction.", - "Also has the authority to set the clawback receiver and change itself." + "Payer wallet, responsible for creating the distributor and paying for the transaction." ] }, { @@ -98,298 +88,236 @@ export type MerkleDistributor = { "docs": [ "The [Token] program." ] + }, + { + "name": "associatedTokenProgram", + "isMut": false, + "isSigner": false } ], "args": [ { - "name": "params", - "type": { - "defined": "NewDistributorParams" - } + "name": "maxClaimAmount", + "type": "u64" + }, + { + "name": "maxDistributor", + "type": "u64" } ] }, { - "name": "closeDistributor", - "docs": [ - "only available in test phase" - ], + "name": "fundDistributorRoot", "accounts": [ { - "name": "distributor", + "name": "distributorRoot", "isMut": true, "isSigner": false, "docs": [ - "[MerkleDistributor]." + "The [DistributorRoot]" ], "relations": [ - "admin", - "token_vault" + "mint" ] }, { - "name": "tokenVault", + "name": "distributorRootVault", "isMut": true, "isSigner": false, "docs": [ - "Clawback receiver token account" + "Distributor root vault" ] }, { - "name": "admin", + "name": "mint", + "isMut": false, + "isSigner": false, + "docs": [ + "The mint to distribute." + ] + }, + { + "name": "payer", "isMut": true, "isSigner": true, "docs": [ - "Admin wallet, responsible for creating the distributor and paying for the transaction.", - "Also has the authority to set the clawback receiver and change itself." + "Payer." ] }, { - "name": "destinationTokenAccount", + "name": "payerToken", "isMut": true, "isSigner": false, "docs": [ - "account receive token back" + "Payer Token Account." ] }, { - "name": "tokenProgram", + "name": "systemProgram", "isMut": false, "isSigner": false, "docs": [ - "The [Token] program." - ] - } - ], - "args": [] - }, - { - "name": "closeClaimStatus", - "docs": [ - "only available in test phase" - ], - "accounts": [ - { - "name": "claimStatus", - "isMut": true, - "isSigner": false, - "relations": [ - "claimant", - "admin" + "The [System] program." ] }, { - "name": "claimant", - "isMut": true, - "isSigner": false - }, - { - "name": "admin", + "name": "tokenProgram", "isMut": false, - "isSigner": true - } - ], - "args": [] - }, - { - "name": "setActivationPoint", - "accounts": [ - { - "name": "distributor", - "isMut": true, "isSigner": false, "docs": [ - "[MerkleDistributor]." - ], - "relations": [ - "admin" + "The [Token] program." ] }, { - "name": "admin", - "isMut": true, - "isSigner": true, - "docs": [ - "Payer to create the distributor." - ] + "name": "associatedTokenProgram", + "isMut": false, + "isSigner": false } ], "args": [ { - "name": "activationPoint", + "name": "maxAmount", "type": "u64" } ] }, { - "name": "clawback", + "name": "newDistributor", "accounts": [ { "name": "distributor", "isMut": true, "isSigner": false, "docs": [ - "The [MerkleDistributor]." + "[MerkleDistributor]." ], - "relations": [ - "clawback_receiver" - ] - }, - { - "name": "from", - "isMut": true, - "isSigner": false, - "docs": [ - "Distributor ATA containing the tokens to distribute." - ] + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "MerkleDistributor" + }, + { + "kind": "account", + "type": "publicKey", + "path": "base" + }, + { + "kind": "account", + "type": "publicKey", + "account": "Mint", + "path": "mint" + }, + { + "kind": "arg", + "type": "u64", + "path": "version" + } + ] + } }, { - "name": "clawbackReceiver", + "name": "distributorRoot", "isMut": true, "isSigner": false, "docs": [ - "The Clawback token account." + "The [DistributorRoot]." ] }, { - "name": "tokenProgram", + "name": "base", "isMut": false, - "isSigner": false, + "isSigner": true, "docs": [ - "SPL [Token] program." + "Base key of the distributor." ] - } - ], - "args": [] - }, - { - "name": "setClawbackReceiver", - "accounts": [ + }, { - "name": "distributor", + "name": "clawbackReceiver", "isMut": true, "isSigner": false, "docs": [ - "The [MerkleDistributor]." - ], - "relations": [ - "admin" + "Clawback receiver token account" ] }, { - "name": "newClawbackAccount", + "name": "mint", "isMut": false, "isSigner": false, "docs": [ - "New clawback account" + "The mint to distribute." ] }, { - "name": "admin", - "isMut": false, - "isSigner": true, - "docs": [ - "Admin signer" - ] - } - ], - "args": [] - }, - { - "name": "setAdmin", - "accounts": [ - { - "name": "distributor", + "name": "tokenVault", "isMut": true, "isSigner": false, "docs": [ - "The [MerkleDistributor]." - ], - "relations": [ - "admin" + "Token vault", + "Should create previously" ] }, { "name": "admin", "isMut": false, + "isSigner": false + }, + { + "name": "payer", + "isMut": true, "isSigner": true, "docs": [ - "Admin signer" + "Payer wallet, responsible for creating the distributor and paying for the transaction." ] }, { - "name": "newAdmin", + "name": "systemProgram", "isMut": false, "isSigner": false, "docs": [ - "New admin account" + "The [System] program." ] - } - ], - "args": [] - }, - { - "name": "setOperator", - "accounts": [ + }, { - "name": "distributor", - "isMut": true, + "name": "tokenProgram", + "isMut": false, "isSigner": false, "docs": [ - "The [MerkleDistributor]." - ], - "relations": [ - "admin" + "The [Token] program." ] }, { - "name": "admin", + "name": "associatedTokenProgram", "isMut": false, - "isSigner": true, - "docs": [ - "Admin signer" - ] + "isSigner": false } ], "args": [ { - "name": "newOperator", - "type": "publicKey" + "name": "params", + "type": { + "defined": "NewDistributorParams" + } } ] }, { - "name": "newClaim", - "docs": [ - "USER FUNCTIONS /////" - ], + "name": "createCanopyTree", "accounts": [ { - "name": "distributor", + "name": "canopyTree", "isMut": true, "isSigner": false, "docs": [ - "The [MerkleDistributor]." - ] - }, - { - "name": "claimStatus", - "isMut": true, - "isSigner": false, - "docs": [ - "Claim status PDA" + "[CanopyTree]" ], "pda": { "seeds": [ { "kind": "const", "type": "string", - "value": "ClaimStatus" - }, - { - "kind": "account", - "type": "publicKey", - "path": "claimant" + "value": "CanopyTree" }, { "kind": "account", @@ -400,44 +328,19 @@ export type MerkleDistributor = { } }, { - "name": "from", - "isMut": true, - "isSigner": false, - "docs": [ - "Distributor ATA containing the tokens to distribute." - ] - }, - { - "name": "to", - "isMut": true, + "name": "distributor", + "isMut": false, "isSigner": false, "docs": [ - "Account to send the claimed tokens to." + "The [MerkleDistributor]." ] }, { - "name": "claimant", + "name": "payer", "isMut": true, "isSigner": true, "docs": [ - "Who is claiming the tokens." - ] - }, - { - "name": "operator", - "isMut": false, - "isSigner": true, - "isOptional": true, - "docs": [ - "operator" - ] - }, - { - "name": "tokenProgram", - "isMut": false, - "isSigner": false, - "docs": [ - "SPL [Token] program." + "Payer wallet, responsible for creating the distributor and paying for the transaction." ] }, { @@ -451,15 +354,20 @@ export type MerkleDistributor = { ], "args": [ { - "name": "amountUnlocked", - "type": "u64" + "name": "depth", + "type": "u8" }, { - "name": "amountLocked", - "type": "u64" + "name": "root", + "type": { + "array": [ + "u8", + 32 + ] + } }, { - "name": "proof", + "name": "canopyNodes", "type": { "vec": { "array": [ @@ -472,59 +380,38 @@ export type MerkleDistributor = { ] }, { - "name": "claimLocked", + "name": "fundMerkleDistributorFromRoot", "accounts": [ { - "name": "distributor", - "isMut": true, + "name": "distributorRoot", + "isMut": false, "isSigner": false, "docs": [ - "The [MerkleDistributor]." + "The [DistributorRoot]." ] }, { - "name": "claimStatus", + "name": "distributorRootVault", "isMut": true, "isSigner": false, "docs": [ - "Claim Status PDA" - ], - "relations": [ - "distributor", - "claimant" + "Distributor root vault containing the tokens to distribute to distributor vault." ] }, { - "name": "from", + "name": "distributor", "isMut": true, "isSigner": false, "docs": [ - "Distributor ATA containing the tokens to distribute." + "The [MerkleDistributor]." ] }, { - "name": "to", + "name": "distributorVault", "isMut": true, "isSigner": false, "docs": [ - "Account to send the claimed tokens to." - ] - }, - { - "name": "claimant", - "isMut": false, - "isSigner": true, - "docs": [ - "Who is claiming the tokens." - ] - }, - { - "name": "operator", - "isMut": false, - "isSigner": true, - "isOptional": true, - "docs": [ - "operator" + "Distributor vault" ] }, { @@ -539,135 +426,119 @@ export type MerkleDistributor = { "args": [] }, { - "name": "newClaimAndStake", + "name": "closeDistributor", + "docs": [ + "only available in test phase" + ], "accounts": [ { "name": "distributor", "isMut": true, "isSigner": false, "docs": [ - "The [MerkleDistributor]." + "[MerkleDistributor]." ], "relations": [ - "locker" + "admin", + "token_vault" ] }, { - "name": "claimStatus", - "isMut": true, - "isSigner": false, - "docs": [ - "Claim status PDA" - ], - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "ClaimStatus" - }, - { - "kind": "account", - "type": "publicKey", - "path": "claimant" - }, - { - "kind": "account", - "type": "publicKey", - "path": "distributor" - } - ] - } - }, - { - "name": "from", + "name": "tokenVault", "isMut": true, "isSigner": false, "docs": [ - "Distributor ATA containing the tokens to distribute." + "Clawback receiver token account" ] }, { - "name": "claimant", + "name": "admin", "isMut": true, "isSigner": true, "docs": [ - "Who is claiming the tokens." - ] - }, - { - "name": "operator", - "isMut": false, - "isSigner": true, - "isOptional": true, - "docs": [ - "operator" + "Admin wallet, responsible for creating the distributor and paying for the transaction.", + "Also has the authority to set the clawback receiver and change itself." ] }, { - "name": "tokenProgram", - "isMut": false, + "name": "destinationTokenAccount", + "isMut": true, "isSigner": false, "docs": [ - "SPL [Token] program." + "account receive token back" ] }, { - "name": "systemProgram", + "name": "tokenProgram", "isMut": false, "isSigner": false, "docs": [ - "The [System] program." + "The [Token] program." ] - }, + } + ], + "args": [] + }, + { + "name": "closeClaimStatus", + "docs": [ + "only available in test phase" + ], + "accounts": [ { - "name": "voterProgram", - "isMut": false, + "name": "claimStatus", + "isMut": true, "isSigner": false, - "docs": [ - "Voter program" + "relations": [ + "claimant", + "admin" ] }, { - "name": "locker", + "name": "claimant", "isMut": true, "isSigner": false }, { - "name": "escrow", + "name": "admin", + "isMut": false, + "isSigner": true + } + ], + "args": [] + }, + { + "name": "setActivationPoint", + "accounts": [ + { + "name": "distributor", "isMut": true, - "isSigner": false + "isSigner": false, + "docs": [ + "[MerkleDistributor]." + ], + "relations": [ + "admin" + ] }, { - "name": "escrowTokens", + "name": "admin", "isMut": true, - "isSigner": false + "isSigner": true, + "docs": [ + "Payer to create the distributor." + ] } ], "args": [ { - "name": "amountUnlocked", - "type": "u64" - }, - { - "name": "amountLocked", + "name": "activationPoint", "type": "u64" - }, - { - "name": "proof", - "type": { - "vec": { - "array": [ - "u8", - 32 - ] - } - } } ] }, { - "name": "claimLockedAndStake", + "name": "clawback", "accounts": [ { "name": "distributor", @@ -677,19 +548,7 @@ export type MerkleDistributor = { "The [MerkleDistributor]." ], "relations": [ - "locker" - ] - }, - { - "name": "claimStatus", - "isMut": true, - "isSigner": false, - "docs": [ - "Claim Status PDA" - ], - "relations": [ - "distributor", - "claimant" + "clawback_receiver" ] }, { @@ -701,20 +560,11 @@ export type MerkleDistributor = { ] }, { - "name": "claimant", - "isMut": false, - "isSigner": true, - "docs": [ - "Who is claiming the tokens." - ] - }, - { - "name": "operator", - "isMut": false, - "isSigner": true, - "isOptional": true, + "name": "clawbackReceiver", + "isMut": true, + "isSigner": false, "docs": [ - "operator" + "The Clawback token account." ] }, { @@ -724,10 +574,506 @@ export type MerkleDistributor = { "docs": [ "SPL [Token] program." ] - }, - { - "name": "voterProgram", - "isMut": false, + } + ], + "args": [] + }, + { + "name": "setClawbackReceiver", + "accounts": [ + { + "name": "distributor", + "isMut": true, + "isSigner": false, + "docs": [ + "The [MerkleDistributor]." + ], + "relations": [ + "admin" + ] + }, + { + "name": "newClawbackAccount", + "isMut": false, + "isSigner": false, + "docs": [ + "New clawback account" + ] + }, + { + "name": "admin", + "isMut": false, + "isSigner": true, + "docs": [ + "Admin signer" + ] + } + ], + "args": [] + }, + { + "name": "setAdmin", + "accounts": [ + { + "name": "distributor", + "isMut": true, + "isSigner": false, + "docs": [ + "The [MerkleDistributor]." + ], + "relations": [ + "admin" + ] + }, + { + "name": "admin", + "isMut": false, + "isSigner": true, + "docs": [ + "Admin signer" + ] + }, + { + "name": "newAdmin", + "isMut": false, + "isSigner": false, + "docs": [ + "New admin account" + ] + } + ], + "args": [] + }, + { + "name": "setOperator", + "accounts": [ + { + "name": "distributor", + "isMut": true, + "isSigner": false, + "docs": [ + "The [MerkleDistributor]." + ], + "relations": [ + "admin" + ] + }, + { + "name": "admin", + "isMut": false, + "isSigner": true, + "docs": [ + "Admin signer" + ] + } + ], + "args": [ + { + "name": "newOperator", + "type": "publicKey" + } + ] + }, + { + "name": "newClaim", + "docs": [ + "USER FUNCTIONS /////" + ], + "accounts": [ + { + "name": "distributor", + "isMut": true, + "isSigner": false, + "docs": [ + "The [MerkleDistributor]." + ] + }, + { + "name": "canopyTree", + "isMut": false, + "isSigner": false, + "docs": [ + "The [CanopyTree]." + ], + "relations": [ + "distributor" + ] + }, + { + "name": "claimStatus", + "isMut": true, + "isSigner": false, + "docs": [ + "Claim status PDA" + ], + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "ClaimStatus" + }, + { + "kind": "account", + "type": "publicKey", + "path": "claimant" + }, + { + "kind": "account", + "type": "publicKey", + "path": "distributor" + } + ] + } + }, + { + "name": "from", + "isMut": true, + "isSigner": false, + "docs": [ + "Distributor ATA containing the tokens to distribute." + ] + }, + { + "name": "to", + "isMut": true, + "isSigner": false, + "docs": [ + "Account to send the claimed tokens to." + ] + }, + { + "name": "claimant", + "isMut": true, + "isSigner": true, + "docs": [ + "Who is claiming the tokens." + ] + }, + { + "name": "operator", + "isMut": false, + "isSigner": true, + "isOptional": true, + "docs": [ + "operator" + ] + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false, + "docs": [ + "SPL [Token] program." + ] + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false, + "docs": [ + "The [System] program." + ] + } + ], + "args": [ + { + "name": "amountUnlocked", + "type": "u64" + }, + { + "name": "amountLocked", + "type": "u64" + }, + { + "name": "leafIndex", + "type": "u32" + }, + { + "name": "proof", + "type": { + "vec": { + "array": [ + "u8", + 32 + ] + } + } + } + ] + }, + { + "name": "claimLocked", + "accounts": [ + { + "name": "distributor", + "isMut": true, + "isSigner": false, + "docs": [ + "The [MerkleDistributor]." + ] + }, + { + "name": "claimStatus", + "isMut": true, + "isSigner": false, + "docs": [ + "Claim Status PDA" + ], + "relations": [ + "distributor", + "claimant" + ] + }, + { + "name": "from", + "isMut": true, + "isSigner": false, + "docs": [ + "Distributor ATA containing the tokens to distribute." + ] + }, + { + "name": "to", + "isMut": true, + "isSigner": false, + "docs": [ + "Account to send the claimed tokens to." + ] + }, + { + "name": "claimant", + "isMut": false, + "isSigner": true, + "docs": [ + "Who is claiming the tokens." + ] + }, + { + "name": "operator", + "isMut": false, + "isSigner": true, + "isOptional": true, + "docs": [ + "operator" + ] + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false, + "docs": [ + "SPL [Token] program." + ] + } + ], + "args": [] + }, + { + "name": "newClaimAndStake", + "accounts": [ + { + "name": "distributor", + "isMut": true, + "isSigner": false, + "docs": [ + "The [MerkleDistributor]." + ], + "relations": [ + "locker" + ] + }, + { + "name": "canopyTree", + "isMut": false, + "isSigner": false, + "docs": [ + "The [CanopyTree]." + ], + "relations": [ + "distributor" + ] + }, + { + "name": "claimStatus", + "isMut": true, + "isSigner": false, + "docs": [ + "Claim status PDA" + ], + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "ClaimStatus" + }, + { + "kind": "account", + "type": "publicKey", + "path": "claimant" + }, + { + "kind": "account", + "type": "publicKey", + "path": "distributor" + } + ] + } + }, + { + "name": "from", + "isMut": true, + "isSigner": false, + "docs": [ + "Distributor ATA containing the tokens to distribute." + ] + }, + { + "name": "claimant", + "isMut": true, + "isSigner": true, + "docs": [ + "Who is claiming the tokens." + ] + }, + { + "name": "operator", + "isMut": false, + "isSigner": true, + "isOptional": true, + "docs": [ + "operator" + ] + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false, + "docs": [ + "SPL [Token] program." + ] + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false, + "docs": [ + "The [System] program." + ] + }, + { + "name": "voterProgram", + "isMut": false, + "isSigner": false, + "docs": [ + "Voter program" + ] + }, + { + "name": "locker", + "isMut": true, + "isSigner": false + }, + { + "name": "escrow", + "isMut": true, + "isSigner": false + }, + { + "name": "escrowTokens", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "amountUnlocked", + "type": "u64" + }, + { + "name": "amountLocked", + "type": "u64" + }, + { + "name": "leafIndex", + "type": "u32" + }, + { + "name": "proof", + "type": { + "vec": { + "array": [ + "u8", + 32 + ] + } + } + } + ] + }, + { + "name": "claimLockedAndStake", + "accounts": [ + { + "name": "distributor", + "isMut": true, + "isSigner": false, + "docs": [ + "The [MerkleDistributor]." + ], + "relations": [ + "locker" + ] + }, + { + "name": "claimStatus", + "isMut": true, + "isSigner": false, + "docs": [ + "Claim Status PDA" + ], + "relations": [ + "distributor", + "claimant" + ] + }, + { + "name": "from", + "isMut": true, + "isSigner": false, + "docs": [ + "Distributor ATA containing the tokens to distribute." + ] + }, + { + "name": "claimant", + "isMut": false, + "isSigner": true, + "docs": [ + "Who is claiming the tokens." + ] + }, + { + "name": "operator", + "isMut": false, + "isSigner": true, + "isOptional": true, + "docs": [ + "operator" + ] + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false, + "docs": [ + "SPL [Token] program." + ] + }, + { + "name": "voterProgram", + "isMut": false, "isSigner": false, "docs": [ "Voter program" @@ -753,6 +1099,67 @@ export type MerkleDistributor = { } ], "accounts": [ + { + "name": "canopyTree", + "type": { + "kind": "struct", + "fields": [ + { + "name": "root", + "docs": [ + "The 256-bit merkle root." + ], + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "depth", + "docs": [ + "The depth of merkle will store onchain", + "With `depth``: total levels from the root to leaves: depth + 1" + ], + "type": "u8" + }, + { + "name": "nodes", + "docs": [ + "A vector of node hashes representing canopy leaves node" + ], + "type": { + "vec": { + "array": [ + "u8", + 32 + ] + } + } + }, + { + "name": "distributor", + "docs": [ + "The distributor associated with this Merkle tree" + ], + "type": "publicKey" + }, + { + "name": "buffer", + "docs": [ + "Buffer" + ], + "type": { + "array": [ + "u64", + 5 + ] + } + } + ] + } + }, { "name": "claimStatus", "docs": [ @@ -761,6 +1168,13 @@ export type MerkleDistributor = { "type": { "kind": "struct", "fields": [ + { + "name": "admin", + "docs": [ + "admin of merkle tree, store for for testing purpose" + ], + "type": "publicKey" + }, { "name": "distributor", "docs": [ @@ -796,43 +1210,155 @@ export type MerkleDistributor = { ], "type": "u64" }, + { + "name": "bonusAmount", + "docs": [ + "Bonus amount" + ], + "type": "u64" + }, { "name": "closable", "docs": [ "indicate that whether admin can close this account, for testing purpose" ], - "type": "bool" + "type": "u8" }, { - "name": "admin", + "name": "padding0", "docs": [ - "admin of merkle tree, store for for testing purpose" + "padding 0" ], - "type": "publicKey" + "type": { + "array": [ + "u8", + 7 + ] + } + }, + { + "name": "padding1", + "docs": [ + "padding 1" + ], + "type": "u64" + }, + { + "name": "buffer", + "docs": [ + "buffer" + ], + "type": "u128" } ] } }, { - "name": "merkleDistributor", + "name": "distributorRoot", "docs": [ - "State for the account which distributes tokens." + "Parent Account: Authority of parent vault use to distribute fund to all distributors" ], "type": { "kind": "struct", "fields": [ { - "name": "root", + "name": "bump", "docs": [ - "The 256-bit merkle root." + "Bump seed." + ], + "type": "u8" + }, + { + "name": "padding0", + "docs": [ + "padding 0" ], "type": { "array": [ "u8", - 32 + 7 ] } }, + { + "name": "mint", + "docs": [ + "Mint of the token to be distributed." + ], + "type": "publicKey" + }, + { + "name": "base", + "docs": [ + "Base key of distributor root" + ], + "type": "publicKey" + }, + { + "name": "admin", + "docs": [ + "Admin" + ], + "type": "publicKey" + }, + { + "name": "distributorRootVault", + "docs": [ + "Token Address of distributor root vault" + ], + "type": "publicKey" + }, + { + "name": "maxClaimAmount", + "docs": [ + "Max claim amount" + ], + "type": "u64" + }, + { + "name": "maxDistributor", + "docs": [ + "Max distributor" + ], + "type": "u64" + }, + { + "name": "totalFundedAmount", + "docs": [ + "total funded amount" + ], + "type": "u64" + }, + { + "name": "totalDistributorCreated", + "docs": [ + "total escrow created" + ], + "type": "u64" + }, + { + "name": "buffer", + "docs": [ + "Buffer for future use or alignment." + ], + "type": { + "array": [ + "u128", + 5 + ] + } + } + ] + } + }, + { + "name": "merkleDistributor", + "docs": [ + "State for the account which distributes tokens." + ], + "type": { + "kind": "struct", + "fields": [ { "name": "mint", "docs": [ @@ -882,6 +1408,13 @@ export type MerkleDistributor = { ], "type": "publicKey" }, + { + "name": "distributorRoot", + "docs": [ + "Distributor root use to distribute fund to all distributor" + ], + "type": "publicKey" + }, { "name": "version", "docs": [ @@ -945,6 +1478,13 @@ export type MerkleDistributor = { ], "type": "u64" }, + { + "name": "fundedAmount", + "docs": [ + "The total amount has been funded" + ], + "type": "u64" + }, { "name": "activationType", "docs": [ @@ -997,6 +1537,24 @@ export type MerkleDistributor = { "type": { "defined": "AirdropBonus" } + }, + { + "name": "padding2", + "type": { + "array": [ + "u8", + 8 + ] + } + }, + { + "name": "buffer", + "type": { + "array": [ + "u128", + 5 + ] + } } ] } @@ -1012,15 +1570,6 @@ export type MerkleDistributor = { "name": "version", "type": "u64" }, - { - "name": "root", - "type": { - "array": [ - "u8", - 32 - ] - } - }, { "name": "totalClaim", "type": "u64" @@ -1304,30 +1853,221 @@ export type MerkleDistributor = { { "code": 6025, "name": "SameOperator", - "msg": "same operator" + "msg": "Same operator" + }, + { + "code": 6026, + "name": "InvalidLocker", + "msg": "Invalid locker" + }, + { + "code": 6027, + "name": "EscrowIsNotMaxLock", + "msg": "Escrow is not max lock" + }, + { + "code": 6028, + "name": "InvalidRemainingAccounts", + "msg": "Invalid remaining accounts" + }, + { + "code": 6029, + "name": "InvalidAccount", + "msg": "Invalid account" + }, + { + "code": 6030, + "name": "CanopyRootMissMatch", + "msg": "Canopy root miss match with real root" + } + ] +}; + +export const IDL: MerkleDistributor = { + "version": "0.1.0", + "name": "merkle_distributor", + "instructions": [ + { + "name": "newDistributorRoot", + "docs": [ + "ADMIN FUNCTIONS ////" + ], + "accounts": [ + { + "name": "distributorRoot", + "isMut": true, + "isSigner": false, + "docs": [ + "[DistributorRoot]" + ], + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "DistributorRoot" + }, + { + "kind": "account", + "type": "publicKey", + "path": "base" + }, + { + "kind": "account", + "type": "publicKey", + "account": "Mint", + "path": "mint" + } + ] + } + }, + { + "name": "distributorRootVault", + "isMut": true, + "isSigner": false, + "docs": [ + "Distributor root vault" + ] + }, + { + "name": "mint", + "isMut": false, + "isSigner": false, + "docs": [ + "The mint to distribute." + ] + }, + { + "name": "base", + "isMut": false, + "isSigner": true, + "docs": [ + "Base key of the distributor." + ] + }, + { + "name": "admin", + "isMut": false, + "isSigner": false + }, + { + "name": "payer", + "isMut": true, + "isSigner": true, + "docs": [ + "Payer wallet, responsible for creating the distributor and paying for the transaction." + ] + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false, + "docs": [ + "The [System] program." + ] + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false, + "docs": [ + "The [Token] program." + ] + }, + { + "name": "associatedTokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "maxClaimAmount", + "type": "u64" + }, + { + "name": "maxDistributor", + "type": "u64" + } + ] }, { - "code": 6026, - "name": "InvalidLocker", - "msg": "Invalid locker" + "name": "fundDistributorRoot", + "accounts": [ + { + "name": "distributorRoot", + "isMut": true, + "isSigner": false, + "docs": [ + "The [DistributorRoot]" + ], + "relations": [ + "mint" + ] + }, + { + "name": "distributorRootVault", + "isMut": true, + "isSigner": false, + "docs": [ + "Distributor root vault" + ] + }, + { + "name": "mint", + "isMut": false, + "isSigner": false, + "docs": [ + "The mint to distribute." + ] + }, + { + "name": "payer", + "isMut": true, + "isSigner": true, + "docs": [ + "Payer." + ] + }, + { + "name": "payerToken", + "isMut": true, + "isSigner": false, + "docs": [ + "Payer Token Account." + ] + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false, + "docs": [ + "The [System] program." + ] + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false, + "docs": [ + "The [Token] program." + ] + }, + { + "name": "associatedTokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "maxAmount", + "type": "u64" + } + ] }, - { - "code": 6027, - "name": "EscrowIsNotMaxLock", - "msg": "Escrow is not max lock" - } - ] -}; - -export const IDL: MerkleDistributor = { - "version": "0.1.0", - "name": "merkle_distributor", - "instructions": [ { "name": "newDistributor", - "docs": [ - "ADMIN FUNCTIONS ////" - ], "accounts": [ { "name": "distributor", @@ -1362,6 +2102,14 @@ export const IDL: MerkleDistributor = { ] } }, + { + "name": "distributorRoot", + "isMut": true, + "isSigner": false, + "docs": [ + "The [DistributorRoot]." + ] + }, { "name": "base", "isMut": false, @@ -1379,37 +2127,176 @@ export const IDL: MerkleDistributor = { ] }, { - "name": "mint", + "name": "mint", + "isMut": false, + "isSigner": false, + "docs": [ + "The mint to distribute." + ] + }, + { + "name": "tokenVault", + "isMut": true, + "isSigner": false, + "docs": [ + "Token vault", + "Should create previously" + ] + }, + { + "name": "admin", + "isMut": false, + "isSigner": false + }, + { + "name": "payer", + "isMut": true, + "isSigner": true, + "docs": [ + "Payer wallet, responsible for creating the distributor and paying for the transaction." + ] + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false, + "docs": [ + "The [System] program." + ] + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false, + "docs": [ + "The [Token] program." + ] + }, + { + "name": "associatedTokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": "NewDistributorParams" + } + } + ] + }, + { + "name": "createCanopyTree", + "accounts": [ + { + "name": "canopyTree", + "isMut": true, + "isSigner": false, + "docs": [ + "[CanopyTree]" + ], + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "CanopyTree" + }, + { + "kind": "account", + "type": "publicKey", + "path": "distributor" + } + ] + } + }, + { + "name": "distributor", + "isMut": false, + "isSigner": false, + "docs": [ + "The [MerkleDistributor]." + ] + }, + { + "name": "payer", + "isMut": true, + "isSigner": true, + "docs": [ + "Payer wallet, responsible for creating the distributor and paying for the transaction." + ] + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false, + "docs": [ + "The [System] program." + ] + } + ], + "args": [ + { + "name": "depth", + "type": "u8" + }, + { + "name": "root", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "canopyNodes", + "type": { + "vec": { + "array": [ + "u8", + 32 + ] + } + } + } + ] + }, + { + "name": "fundMerkleDistributorFromRoot", + "accounts": [ + { + "name": "distributorRoot", "isMut": false, "isSigner": false, "docs": [ - "The mint to distribute." + "The [DistributorRoot]." ] }, { - "name": "tokenVault", - "isMut": false, + "name": "distributorRootVault", + "isMut": true, "isSigner": false, "docs": [ - "Token vault", - "Should create previously" + "Distributor root vault containing the tokens to distribute to distributor vault." ] }, { - "name": "admin", + "name": "distributor", "isMut": true, - "isSigner": true, + "isSigner": false, "docs": [ - "Admin wallet, responsible for creating the distributor and paying for the transaction.", - "Also has the authority to set the clawback receiver and change itself." + "The [MerkleDistributor]." ] }, { - "name": "systemProgram", - "isMut": false, + "name": "distributorVault", + "isMut": true, "isSigner": false, "docs": [ - "The [System] program." + "Distributor vault" ] }, { @@ -1417,18 +2304,11 @@ export const IDL: MerkleDistributor = { "isMut": false, "isSigner": false, "docs": [ - "The [Token] program." + "SPL [Token] program." ] } ], - "args": [ - { - "name": "params", - "type": { - "defined": "NewDistributorParams" - } - } - ] + "args": [] }, { "name": "closeDistributor", @@ -1693,6 +2573,17 @@ export const IDL: MerkleDistributor = { "The [MerkleDistributor]." ] }, + { + "name": "canopyTree", + "isMut": false, + "isSigner": false, + "docs": [ + "The [CanopyTree]." + ], + "relations": [ + "distributor" + ] + }, { "name": "claimStatus", "isMut": true, @@ -1779,6 +2670,10 @@ export const IDL: MerkleDistributor = { "name": "amountLocked", "type": "u64" }, + { + "name": "leafIndex", + "type": "u32" + }, { "name": "proof", "type": { @@ -1873,6 +2768,17 @@ export const IDL: MerkleDistributor = { "locker" ] }, + { + "name": "canopyTree", + "isMut": false, + "isSigner": false, + "docs": [ + "The [CanopyTree]." + ], + "relations": [ + "distributor" + ] + }, { "name": "claimStatus", "isMut": true, @@ -1974,6 +2880,10 @@ export const IDL: MerkleDistributor = { "name": "amountLocked", "type": "u64" }, + { + "name": "leafIndex", + "type": "u32" + }, { "name": "proof", "type": { @@ -2074,6 +2984,67 @@ export const IDL: MerkleDistributor = { } ], "accounts": [ + { + "name": "canopyTree", + "type": { + "kind": "struct", + "fields": [ + { + "name": "root", + "docs": [ + "The 256-bit merkle root." + ], + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "depth", + "docs": [ + "The depth of merkle will store onchain", + "With `depth``: total levels from the root to leaves: depth + 1" + ], + "type": "u8" + }, + { + "name": "nodes", + "docs": [ + "A vector of node hashes representing canopy leaves node" + ], + "type": { + "vec": { + "array": [ + "u8", + 32 + ] + } + } + }, + { + "name": "distributor", + "docs": [ + "The distributor associated with this Merkle tree" + ], + "type": "publicKey" + }, + { + "name": "buffer", + "docs": [ + "Buffer" + ], + "type": { + "array": [ + "u64", + 5 + ] + } + } + ] + } + }, { "name": "claimStatus", "docs": [ @@ -2082,6 +3053,13 @@ export const IDL: MerkleDistributor = { "type": { "kind": "struct", "fields": [ + { + "name": "admin", + "docs": [ + "admin of merkle tree, store for for testing purpose" + ], + "type": "publicKey" + }, { "name": "distributor", "docs": [ @@ -2117,43 +3095,155 @@ export const IDL: MerkleDistributor = { ], "type": "u64" }, + { + "name": "bonusAmount", + "docs": [ + "Bonus amount" + ], + "type": "u64" + }, { "name": "closable", "docs": [ "indicate that whether admin can close this account, for testing purpose" ], - "type": "bool" + "type": "u8" }, { - "name": "admin", + "name": "padding0", "docs": [ - "admin of merkle tree, store for for testing purpose" + "padding 0" ], - "type": "publicKey" + "type": { + "array": [ + "u8", + 7 + ] + } + }, + { + "name": "padding1", + "docs": [ + "padding 1" + ], + "type": "u64" + }, + { + "name": "buffer", + "docs": [ + "buffer" + ], + "type": "u128" } ] } }, { - "name": "merkleDistributor", + "name": "distributorRoot", "docs": [ - "State for the account which distributes tokens." + "Parent Account: Authority of parent vault use to distribute fund to all distributors" ], "type": { "kind": "struct", "fields": [ { - "name": "root", + "name": "bump", "docs": [ - "The 256-bit merkle root." + "Bump seed." + ], + "type": "u8" + }, + { + "name": "padding0", + "docs": [ + "padding 0" ], "type": { "array": [ "u8", - 32 + 7 ] } }, + { + "name": "mint", + "docs": [ + "Mint of the token to be distributed." + ], + "type": "publicKey" + }, + { + "name": "base", + "docs": [ + "Base key of distributor root" + ], + "type": "publicKey" + }, + { + "name": "admin", + "docs": [ + "Admin" + ], + "type": "publicKey" + }, + { + "name": "distributorRootVault", + "docs": [ + "Token Address of distributor root vault" + ], + "type": "publicKey" + }, + { + "name": "maxClaimAmount", + "docs": [ + "Max claim amount" + ], + "type": "u64" + }, + { + "name": "maxDistributor", + "docs": [ + "Max distributor" + ], + "type": "u64" + }, + { + "name": "totalFundedAmount", + "docs": [ + "total funded amount" + ], + "type": "u64" + }, + { + "name": "totalDistributorCreated", + "docs": [ + "total escrow created" + ], + "type": "u64" + }, + { + "name": "buffer", + "docs": [ + "Buffer for future use or alignment." + ], + "type": { + "array": [ + "u128", + 5 + ] + } + } + ] + } + }, + { + "name": "merkleDistributor", + "docs": [ + "State for the account which distributes tokens." + ], + "type": { + "kind": "struct", + "fields": [ { "name": "mint", "docs": [ @@ -2203,6 +3293,13 @@ export const IDL: MerkleDistributor = { ], "type": "publicKey" }, + { + "name": "distributorRoot", + "docs": [ + "Distributor root use to distribute fund to all distributor" + ], + "type": "publicKey" + }, { "name": "version", "docs": [ @@ -2266,6 +3363,13 @@ export const IDL: MerkleDistributor = { ], "type": "u64" }, + { + "name": "fundedAmount", + "docs": [ + "The total amount has been funded" + ], + "type": "u64" + }, { "name": "activationType", "docs": [ @@ -2318,6 +3422,24 @@ export const IDL: MerkleDistributor = { "type": { "defined": "AirdropBonus" } + }, + { + "name": "padding2", + "type": { + "array": [ + "u8", + 8 + ] + } + }, + { + "name": "buffer", + "type": { + "array": [ + "u128", + 5 + ] + } } ] } @@ -2333,15 +3455,6 @@ export const IDL: MerkleDistributor = { "name": "version", "type": "u64" }, - { - "name": "root", - "type": { - "array": [ - "u8", - 32 - ] - } - }, { "name": "totalClaim", "type": "u64" @@ -2625,7 +3738,7 @@ export const IDL: MerkleDistributor = { { "code": 6025, "name": "SameOperator", - "msg": "same operator" + "msg": "Same operator" }, { "code": 6026, @@ -2636,6 +3749,21 @@ export const IDL: MerkleDistributor = { "code": 6027, "name": "EscrowIsNotMaxLock", "msg": "Escrow is not max lock" + }, + { + "code": 6028, + "name": "InvalidRemainingAccounts", + "msg": "Invalid remaining accounts" + }, + { + "code": 6029, + "name": "InvalidAccount", + "msg": "Invalid account" + }, + { + "code": 6030, + "name": "CanopyRootMissMatch", + "msg": "Canopy root miss match with real root" } ] }; diff --git a/tests/merkle_distributor/index.ts b/tests/merkle_distributor/index.ts index 0488b8f..0cabff0 100644 --- a/tests/merkle_distributor/index.ts +++ b/tests/merkle_distributor/index.ts @@ -1,362 +1,682 @@ import fs from "fs"; -import { Keypair, PublicKey, Transaction } from "@solana/web3.js"; +import { + ComputeBudgetProgram, + Keypair, + PublicKey, + AccountMeta, +} from "@solana/web3.js"; import BN from "bn.js"; import { AnchorProvider, Program, Wallet, web3 } from "@coral-xyz/anchor"; import { - MerkleDistributor, - IDL as MerkleDistributorIDL, + MerkleDistributor, + IDL as MerkleDistributorIDL, } from "../../target/types/merkle_distributor"; import { encodeU64, getOrCreateAssociatedTokenAccountWrap } from "../common"; -import { getAssociatedTokenAddressSync, TOKEN_PROGRAM_ID } from "@solana/spl-token"; +import { + ASSOCIATED_TOKEN_PROGRAM_ID, + getAssociatedTokenAddressSync, + TOKEN_PROGRAM_ID, +} from "@solana/spl-token"; import { LOCKED_VOTER_PROGRAM_ID } from "../locked_voter/setup"; const MERKLE_DISTRIBUTOR_PROGRAM_ID = new web3.PublicKey( - "DiS3nNjFVMieMgmiQFm6wgJL7nevk4NrhXKLbtEH1Z2R" + "DiS3nNjFVMieMgmiQFm6wgJL7nevk4NrhXKLbtEH1Z2R" ); const res = fs.readFileSync( - process.cwd() + + process.cwd() + "/keys/localnet/admin-bossj3JvwiNK7pvjr149DqdtJxf2gdygbcmEPTkb2F1.json", - "utf8" + "utf8" ); export function deriveDistributor( - base: web3.PublicKey, - mint: web3.PublicKey, - version: number, + base: web3.PublicKey, + mint: web3.PublicKey, + version: number ) { - let [pk, _] = web3.PublicKey.findProgramAddressSync( - [Buffer.from("MerkleDistributor"), base.toBuffer(), mint.toBuffer(), encodeU64(version)], - MERKLE_DISTRIBUTOR_PROGRAM_ID - ); - return pk + let [pk, _] = web3.PublicKey.findProgramAddressSync( + [ + Buffer.from("MerkleDistributor"), + base.toBuffer(), + mint.toBuffer(), + encodeU64(version), + ], + MERKLE_DISTRIBUTOR_PROGRAM_ID + ); + return pk; } +export function deriveDistributorRootAccount( + mint: web3.PublicKey, + base: web3.PublicKey +) { + let [pk, _] = web3.PublicKey.findProgramAddressSync( + [Buffer.from("DistributorRoot"), base.toBuffer(), mint.toBuffer()], + MERKLE_DISTRIBUTOR_PROGRAM_ID + ); + return pk; +} +export function deriveCanopyTreeAccount(distributor: web3.PublicKey) { + let [pk, _] = web3.PublicKey.findProgramAddressSync( + [Buffer.from("CanopyTree"), distributor.toBuffer()], + MERKLE_DISTRIBUTOR_PROGRAM_ID + ); + return pk; +} export function deriveClaimStatus( - distributor: web3.PublicKey, - claimant: web3.PublicKey, + distributor: web3.PublicKey, + claimant: web3.PublicKey ) { - let [pk, _] = web3.PublicKey.findProgramAddressSync( - [Buffer.from("ClaimStatus"), claimant.toBuffer(), distributor.toBuffer()], - MERKLE_DISTRIBUTOR_PROGRAM_ID - ); - return pk + let [pk, _] = web3.PublicKey.findProgramAddressSync( + [Buffer.from("ClaimStatus"), claimant.toBuffer(), distributor.toBuffer()], + MERKLE_DISTRIBUTOR_PROGRAM_ID + ); + return pk; } - - export const ADMIN = Keypair.fromSecretKey(new Uint8Array(JSON.parse(res))); export const ADMIN_PUBKEY = ADMIN.publicKey; -export function createDistributorProgram(wallet: Wallet): Program { - const provider = new AnchorProvider(AnchorProvider.env().connection, wallet, { - maxRetries: 3, - }); - const program = new Program( - MerkleDistributorIDL, - MERKLE_DISTRIBUTOR_PROGRAM_ID, - provider - ); - return program; +export function createDistributorProgram( + wallet: Wallet +): Program { + const provider = new AnchorProvider(AnchorProvider.env().connection, wallet, { + maxRetries: 3, + }); + const program = new Program( + MerkleDistributorIDL, + MERKLE_DISTRIBUTOR_PROGRAM_ID, + provider + ); + return program; } - -export interface CreateNewDisitrbutorParams { - admin: Keypair; - version: number; - root: Buffer; - totalClaim: BN; - maxNumNodes: BN; - startVestingTs: BN; - endVestingTs: BN; - clawbackStartTs: BN; - activationPoint: BN; - activationType: number; - closable: boolean; - totalBonus: BN; - bonusVestingDuration: BN; - claimType: number; - operator: PublicKey; - locker: PublicKey; - mint: PublicKey; - clawbackReceiver: PublicKey; +export interface CreateNewDistributorRootParams { + admin: Keypair; + mint: PublicKey; + maxClaimAmount: BN; + maxDistributor: BN; } -export async function createNewDistributor( - params: CreateNewDisitrbutorParams +export async function createNewDistributorRoot( + params: CreateNewDistributorRootParams ) { - let { - admin, - version, - root, - totalClaim, - maxNumNodes, - startVestingTs, - endVestingTs, - clawbackStartTs, - activationPoint, - activationType, - closable, totalBonus, bonusVestingDuration, - claimType, operator, locker, mint, clawbackReceiver - } = params; - const program = createDistributorProgram(new Wallet(admin)); - - let base = Keypair.generate(); - - let distributor = deriveDistributor(base.publicKey, mint, version); - let tokenVault = await getOrCreateAssociatedTokenAccountWrap(program.provider.connection, admin, mint, distributor); - await program.methods - .newDistributor({ - version: new BN(version), - root: Array.from(new Uint8Array(root)), - totalClaim, - maxNumNodes, - startVestingTs, - endVestingTs, - clawbackStartTs, - activationPoint, - activationType, - closable, - totalBonus, - bonusVestingDuration, - claimType, - operator, - locker, - }) - .accounts({ - distributor, - mint, - clawbackReceiver, - tokenVault, - admin: admin.publicKey, - base: base.publicKey, - systemProgram: web3.SystemProgram.programId, - tokenProgram: TOKEN_PROGRAM_ID, - }).signers([base]) - .rpc().catch(console.log).then(console.log); - - return { distributor, tokenVault }; + console.log("Create distributor root") + let { admin, mint, maxClaimAmount, maxDistributor } = params; + const program = createDistributorProgram(new Wallet(admin)); + + const base = Keypair.generate(); + let distributorRoot = deriveDistributorRootAccount(mint, base.publicKey); + let distributorRootVault = await getOrCreateAssociatedTokenAccountWrap( + program.provider.connection, + admin, + mint, + distributorRoot + ); + await program.methods + .newDistributorRoot(maxClaimAmount, maxDistributor) + .accounts({ + distributorRoot, + distributorRootVault, + mint, + base: base.publicKey, + admin: admin.publicKey, + payer: admin.publicKey, + systemProgram: web3.SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + }) + .signers([base]) + .rpc() + .catch(console.log) + .then(console.log); + + return { distributorRoot, distributorRootVault }; } -export interface ClaimParams { - claimant: Keypair; - operator?: Keypair; - distributor: PublicKey, - amountUnlocked: BN; - amountLocked: BN; - proof: Array[]; +export interface CreateNewDisitrbutorParams { + admin: Keypair; + version: number; + totalClaim: BN; + maxNumNodes: BN; + startVestingTs: BN; + endVestingTs: BN; + clawbackStartTs: BN; + activationPoint: BN; + activationType: number; + closable: boolean; + totalBonus: BN; + bonusVestingDuration: BN; + claimType: number; + operator: PublicKey; + locker: PublicKey; + mint: PublicKey; + clawbackReceiver: PublicKey; + distributorRoot: PublicKey; } - -export async function claim( - params: ClaimParams -) { - let { claimant, amountUnlocked, amountLocked, proof, distributor, operator } = params; - const program = createDistributorProgram(new Wallet(claimant)); - - let distributorState = await program.account.merkleDistributor.fetch(distributor); - let claimStatus = deriveClaimStatus(distributor, claimant.publicKey); - let to = await getOrCreateAssociatedTokenAccountWrap(program.provider.connection, claimant, distributorState.mint, claimant.publicKey); - - if (operator == null) { - await program.methods.newClaim(amountUnlocked, amountLocked, proof).accounts({ - distributor, - claimant: claimant.publicKey, - claimStatus, - from: distributorState.tokenVault, - to, - systemProgram: web3.SystemProgram.programId, - tokenProgram: TOKEN_PROGRAM_ID, - operator: null, - }).rpc().catch(console.log).then(console.log); - } else { - // user sign tx firstly (need to verify signature to avoid spaming) - let tx = await program.methods.newClaim(amountUnlocked, amountLocked, proof).accounts({ - distributor, - claimant: claimant.publicKey, - claimStatus, - from: distributorState.tokenVault, - to, - systemProgram: web3.SystemProgram.programId, - tokenProgram: TOKEN_PROGRAM_ID, - operator: operator.publicKey, - }).transaction(); - - // pass tx to operator to sign - const { blockhash, lastValidBlockHeight } = - await program.provider.connection.getLatestBlockhash(); - tx.feePayer = claimant.publicKey; - tx.recentBlockhash = blockhash; - tx.lastValidBlockHeight = lastValidBlockHeight; - tx.partialSign(operator); - - // pass back user to sign - const signedTx = await new Wallet(claimant).signTransaction(tx); - const txHash = await program.provider.connection.sendRawTransaction(signedTx.serialize()); - console.log(txHash); - } +export async function createNewDistributor(params: CreateNewDisitrbutorParams) { + let { + admin, + version, + totalClaim, + maxNumNodes, + startVestingTs, + endVestingTs, + clawbackStartTs, + activationPoint, + activationType, + closable, + totalBonus, + bonusVestingDuration, + claimType, + operator, + locker, + mint, + clawbackReceiver, + distributorRoot, + } = params; + const program = createDistributorProgram(new Wallet(admin)); + + let base = Keypair.generate(); + + let distributor = deriveDistributor(base.publicKey, mint, version); + let tokenVault = await getOrCreateAssociatedTokenAccountWrap( + program.provider.connection, + admin, + mint, + distributor + ); + await program.methods + .newDistributor({ + version: new BN(version), + totalClaim, + maxNumNodes, + startVestingTs, + endVestingTs, + clawbackStartTs, + activationPoint, + activationType, + closable, + totalBonus, + bonusVestingDuration, + claimType, + operator, + locker, + }) + .accounts({ + distributor, + distributorRoot, + mint, + clawbackReceiver, + tokenVault, + admin: admin.publicKey, + base: base.publicKey, + systemProgram: web3.SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + }) + .preInstructions([ + ComputeBudgetProgram.setComputeUnitLimit({ + units: 400_000, + }), + ]) + .signers([base]) + .rpc() + .catch(console.log) + .then(console.log); + + return { distributor, tokenVault }; } +export interface CreateCanopyTreeParams { + admin: Keypair; + distributor: PublicKey; + depth: number; + root: number[]; + canopyNodes: Array[]; +} -export interface ClaimAndStakeParams { - claimant: Keypair; - escrow: PublicKey, - operator?: Keypair; - distributor: PublicKey, - amountUnlocked: BN; - amountLocked: BN; - proof: Array[]; +export async function createCanopyTree(params: CreateCanopyTreeParams) { + console.log("create canopy tree") + + let { admin, distributor, depth, root, canopyNodes } = params; + const program = createDistributorProgram(new Wallet(admin)); + const canopyTree = deriveCanopyTreeAccount(distributor); + await program.methods + .createCanopyTree(depth, root, canopyNodes) + .accounts({ + canopyTree, + distributor, + payer: admin.publicKey, + systemProgram: web3.SystemProgram.programId, + }) + .preInstructions([ + ComputeBudgetProgram.setComputeUnitLimit({ + units: 400_000, + }), + ]) + .rpc() + .catch(console.log) + .then(console.log); + + return canopyTree; } -export async function claimAndStake( - params: ClaimAndStakeParams -) { - let { claimant, amountUnlocked, amountLocked, proof, distributor, operator, escrow } = params; - const program = createDistributorProgram(new Wallet(claimant)); - - let distributorState = await program.account.merkleDistributor.fetch(distributor); - let claimStatus = deriveClaimStatus(distributor, claimant.publicKey); - - if (operator == null) { - await program.methods.newClaimAndStake(amountUnlocked, amountLocked, proof).accounts({ - distributor, - claimant: claimant.publicKey, - claimStatus, - from: distributorState.tokenVault, - systemProgram: web3.SystemProgram.programId, - tokenProgram: TOKEN_PROGRAM_ID, - operator: null, - voterProgram: LOCKED_VOTER_PROGRAM_ID, - locker: distributorState.locker, - escrow, - escrowTokens: getAssociatedTokenAddressSync(distributorState.mint, escrow, true), - - }).rpc().catch(console.log).then(console.log); - } else { - await program.methods.newClaimAndStake(amountUnlocked, amountLocked, proof).accounts({ - distributor, - claimant: claimant.publicKey, - claimStatus, - from: distributorState.tokenVault, - systemProgram: web3.SystemProgram.programId, - tokenProgram: TOKEN_PROGRAM_ID, - operator: operator.publicKey, - voterProgram: LOCKED_VOTER_PROGRAM_ID, - locker: distributorState.locker, - escrow, - escrowTokens: getAssociatedTokenAddressSync(distributorState.mint, escrow, true), - }).signers([operator]).rpc().catch(console.log).then(console.log); - } +export interface FundDistributorRootParams { + admin: Keypair; + payer: Keypair; + distributorRoot: PublicKey; + mint: PublicKey; + maxAmount: BN; } +export async function fundDistributorRoot(params: FundDistributorRootParams) { + console.log("fund to distributor root") + + let { admin, payer, distributorRoot, mint, maxAmount } = params; + const program = createDistributorProgram(new Wallet(admin)); + const distributorRootVault = getAssociatedTokenAddressSync( + mint, + distributorRoot, + true + ); + + let payerToken = getAssociatedTokenAddressSync(mint, payer.publicKey); + await program.methods + .fundDistributorRoot(maxAmount) + .accounts({ + distributorRoot, + distributorRootVault, + mint, + payer: payer.publicKey, + payerToken, + systemProgram: web3.SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + }) + .signers([payer]) + .rpc() + .catch(console.log) + .then(console.log); + return { distributorRoot, distributorRootVault }; +} -export interface ClaimLockedParams { - claimant: Keypair; - operator?: Keypair; - distributor: PublicKey, +export interface FundMerkleDistributorFromRootParams { + admin: Keypair; + distributorRoot: PublicKey; + distributorRootVault: PublicKey; + distributor: PublicKey; + distributorVault: PublicKey; } -export async function claimLocked( - params: ClaimLockedParams +export async function fundMerkleDistributorFromRoot( + params: FundMerkleDistributorFromRootParams ) { - let { claimant, distributor, operator } = params; - const program = createDistributorProgram(new Wallet(claimant)); - - let distributorState = await program.account.merkleDistributor.fetch(distributor); - let claimStatus = deriveClaimStatus(distributor, claimant.publicKey); - let to = await getOrCreateAssociatedTokenAccountWrap(program.provider.connection, claimant, distributorState.mint, claimant.publicKey); - - if (operator == null) { - await program.methods.claimLocked().accounts({ - distributor, - claimant: claimant.publicKey, - claimStatus, - from: distributorState.tokenVault, - to, - tokenProgram: TOKEN_PROGRAM_ID, - operator: null, - }).rpc().catch(console.log).then(console.log); - } else { - await program.methods.claimLocked().accounts({ - distributor, - claimant: claimant.publicKey, - claimStatus, - from: distributorState.tokenVault, - to, - tokenProgram: TOKEN_PROGRAM_ID, - operator: operator.publicKey, - }).signers([operator]).rpc().catch(console.log).then(console.log); - } + let { + admin, + distributorRoot, + distributorRootVault, + distributorVault, + distributor, + } = params; + const program = createDistributorProgram(new Wallet(admin)); + await program.methods + .fundMerkleDistributorFromRoot() + .accounts({ + distributorRoot, + distributorRootVault, + distributor, + distributorVault, + tokenProgram: TOKEN_PROGRAM_ID, + }) + .rpc() + .catch(console.log) + .then(console.log); + + return { distributorRoot, distributorRootVault }; } +export interface ClaimParams { + claimant: Keypair; + operator?: Keypair; + distributor: PublicKey; + amountUnlocked: BN; + amountLocked: BN; + proof: Array[]; + leafIndex: number; +} +export async function claim(params: ClaimParams) { + let { + claimant, + amountUnlocked, + amountLocked, + proof, + distributor, + operator, + leafIndex, + } = params; + const program = createDistributorProgram(new Wallet(claimant)); + + let distributorState = await program.account.merkleDistributor.fetch( + distributor + ); + let canopyTree = deriveCanopyTreeAccount(distributor); + let claimStatus = deriveClaimStatus(distributor, claimant.publicKey); + let to = await getOrCreateAssociatedTokenAccountWrap( + program.provider.connection, + claimant, + distributorState.mint, + claimant.publicKey + ); + + if (operator == null) { + await program.methods + .newClaim(amountUnlocked, amountLocked, leafIndex, proof) + .accounts({ + distributor, + canopyTree, + claimant: claimant.publicKey, + claimStatus, + from: distributorState.tokenVault, + to, + systemProgram: web3.SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + operator: null, + }) + .preInstructions([ + ComputeBudgetProgram.setComputeUnitLimit({ + units: 400_000, + }), + ]) + .rpc() + .catch(console.log) + .then(console.log); + } else { + // user sign tx firstly (need to verify signature to avoid spaming) + let tx = await program.methods + .newClaim(amountUnlocked, amountLocked, leafIndex, proof) + .accounts({ + distributor, + canopyTree, + claimant: claimant.publicKey, + claimStatus, + from: distributorState.tokenVault, + to, + systemProgram: web3.SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + operator: operator.publicKey, + }) + .transaction(); + + // pass tx to operator to sign + const { blockhash, lastValidBlockHeight } = + await program.provider.connection.getLatestBlockhash(); + tx.feePayer = claimant.publicKey; + tx.recentBlockhash = blockhash; + tx.lastValidBlockHeight = lastValidBlockHeight; + tx.partialSign(operator); + + // pass back user to sign + const signedTx = await new Wallet(claimant).signTransaction(tx); + const txHash = await program.provider.connection.sendRawTransaction( + signedTx.serialize() + ); + console.log(txHash); + } +} -export interface ClaimLockedAndStakeParams { - claimant: Keypair; - operator?: Keypair; - distributor: PublicKey, - escrow: PublicKey, +export interface ClaimAndStakeParams { + claimant: Keypair; + escrow: PublicKey; + operator?: Keypair; + distributor: PublicKey; + amountUnlocked: BN; + amountLocked: BN; + proof: Array[]; + leafIndex: number; } -export async function claimLockedAndStake( - params: ClaimLockedAndStakeParams -) { - let { claimant, distributor, operator, escrow } = params; - const program = createDistributorProgram(new Wallet(claimant)); - - let distributorState = await program.account.merkleDistributor.fetch(distributor); - let claimStatus = deriveClaimStatus(distributor, claimant.publicKey); - - if (operator == null) { - await program.methods.claimLockedAndStake().accounts({ - distributor, - claimant: claimant.publicKey, - claimStatus, - from: distributorState.tokenVault, - tokenProgram: TOKEN_PROGRAM_ID, - operator: null, - voterProgram: LOCKED_VOTER_PROGRAM_ID, - locker: distributorState.locker, - escrow, - escrowTokens: getAssociatedTokenAddressSync(distributorState.mint, escrow, true), - }).rpc().catch(console.log).then(console.log); - } else { - await program.methods.claimLockedAndStake().accounts({ - distributor, - claimant: claimant.publicKey, - claimStatus, - from: distributorState.tokenVault, - tokenProgram: TOKEN_PROGRAM_ID, - operator: operator.publicKey, - voterProgram: LOCKED_VOTER_PROGRAM_ID, - locker: distributorState.locker, - escrow, - escrowTokens: getAssociatedTokenAddressSync(distributorState.mint, escrow, true), - }).signers([operator]).rpc().catch(console.log).then(console.log); - } +export async function claimAndStake(params: ClaimAndStakeParams) { + let { + claimant, + amountUnlocked, + amountLocked, + proof, + distributor, + operator, + escrow, + leafIndex, + } = params; + const program = createDistributorProgram(new Wallet(claimant)); + + let distributorState = await program.account.merkleDistributor.fetch( + distributor + ); + let canopyTree = deriveCanopyTreeAccount(distributor); + let claimStatus = deriveClaimStatus(distributor, claimant.publicKey); + + if (operator == null) { + await program.methods + .newClaimAndStake(amountUnlocked, amountLocked, leafIndex, proof) + .accounts({ + distributor, + canopyTree, + claimant: claimant.publicKey, + claimStatus, + from: distributorState.tokenVault, + systemProgram: web3.SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + operator: null, + voterProgram: LOCKED_VOTER_PROGRAM_ID, + locker: distributorState.locker, + escrow, + escrowTokens: getAssociatedTokenAddressSync( + distributorState.mint, + escrow, + true + ), + }) + .preInstructions([ + ComputeBudgetProgram.setComputeUnitLimit({ + units: 400_000, + }), + ]) + .rpc() + .catch(console.log) + .then(console.log); + } else { + await program.methods + .newClaimAndStake(amountUnlocked, amountLocked, leafIndex, proof) + .accounts({ + distributor, + canopyTree, + claimant: claimant.publicKey, + claimStatus, + from: distributorState.tokenVault, + systemProgram: web3.SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + operator: operator.publicKey, + voterProgram: LOCKED_VOTER_PROGRAM_ID, + locker: distributorState.locker, + escrow, + escrowTokens: getAssociatedTokenAddressSync( + distributorState.mint, + escrow, + true + ), + }) + .preInstructions([ + ComputeBudgetProgram.setComputeUnitLimit({ + units: 400_000, + }), + ]) + .signers([operator]) + .rpc() + .catch(console.log) + .then(console.log); + } } +export interface ClaimLockedParams { + claimant: Keypair; + operator?: Keypair; + distributor: PublicKey; +} -export interface ClawbackParams { - payer: Keypair; - distributor: PublicKey, +export async function claimLocked(params: ClaimLockedParams) { + let { claimant, distributor, operator } = params; + const program = createDistributorProgram(new Wallet(claimant)); + + let distributorState = await program.account.merkleDistributor.fetch( + distributor + ); + let claimStatus = deriveClaimStatus(distributor, claimant.publicKey); + let to = await getOrCreateAssociatedTokenAccountWrap( + program.provider.connection, + claimant, + distributorState.mint, + claimant.publicKey + ); + + if (operator == null) { + await program.methods + .claimLocked() + .accounts({ + distributor, + claimant: claimant.publicKey, + claimStatus, + from: distributorState.tokenVault, + to, + tokenProgram: TOKEN_PROGRAM_ID, + operator: null, + }) + .preInstructions([ + ComputeBudgetProgram.setComputeUnitLimit({ + units: 400_000, + }), + ]) + .rpc() + .catch(console.log) + .then(console.log); + } else { + await program.methods + .claimLocked() + .accounts({ + distributor, + claimant: claimant.publicKey, + claimStatus, + from: distributorState.tokenVault, + to, + tokenProgram: TOKEN_PROGRAM_ID, + operator: operator.publicKey, + }) + .preInstructions([ + ComputeBudgetProgram.setComputeUnitLimit({ + units: 400_000, + }), + ]) + .signers([operator]) + .rpc() + .catch(console.log) + .then(console.log); + } } -export async function clawBack( - params: ClawbackParams -) { - let { payer, distributor } = params; - const program = createDistributorProgram(new Wallet(payer)); +export interface ClaimLockedAndStakeParams { + claimant: Keypair; + operator?: Keypair; + distributor: PublicKey; + escrow: PublicKey; +} - let distributorState = await program.account.merkleDistributor.fetch(distributor); +export async function claimLockedAndStake(params: ClaimLockedAndStakeParams) { + let { claimant, distributor, operator, escrow } = params; + const program = createDistributorProgram(new Wallet(claimant)); - await program.methods.clawback().accounts({ + let distributorState = await program.account.merkleDistributor.fetch( + distributor + ); + let claimStatus = deriveClaimStatus(distributor, claimant.publicKey); + + if (operator == null) { + await program.methods + .claimLockedAndStake() + .accounts({ distributor, + claimant: claimant.publicKey, + claimStatus, from: distributorState.tokenVault, - clawbackReceiver: distributorState.clawbackReceiver, tokenProgram: TOKEN_PROGRAM_ID, - }).rpc().catch(console.log).then(console.log); -} \ No newline at end of file + operator: null, + voterProgram: LOCKED_VOTER_PROGRAM_ID, + locker: distributorState.locker, + escrow, + escrowTokens: getAssociatedTokenAddressSync( + distributorState.mint, + escrow, + true + ), + }) + .preInstructions([ + ComputeBudgetProgram.setComputeUnitLimit({ + units: 400_000, + }), + ]) + .rpc() + .catch(console.log) + .then(console.log); + } else { + await program.methods + .claimLockedAndStake() + .accounts({ + distributor, + claimant: claimant.publicKey, + claimStatus, + from: distributorState.tokenVault, + tokenProgram: TOKEN_PROGRAM_ID, + operator: operator.publicKey, + voterProgram: LOCKED_VOTER_PROGRAM_ID, + locker: distributorState.locker, + escrow, + escrowTokens: getAssociatedTokenAddressSync( + distributorState.mint, + escrow, + true + ), + }) + .signers([operator]) + .rpc() + .catch(console.log) + .then(console.log); + } +} + +export interface ClawbackParams { + payer: Keypair; + distributor: PublicKey; +} + +export async function clawBack(params: ClawbackParams) { + let { payer, distributor } = params; + const program = createDistributorProgram(new Wallet(payer)); + + let distributorState = await program.account.merkleDistributor.fetch( + distributor + ); + + await program.methods + .clawback() + .accounts({ + distributor, + from: distributorState.tokenVault, + clawbackReceiver: distributorState.clawbackReceiver, + tokenProgram: TOKEN_PROGRAM_ID, + }) + .rpc() + .catch(console.log) + .then(console.log); +} diff --git a/tests/merkle_tree/balance-tree.ts b/tests/merkle_tree/balance-tree.ts index d76c5d3..6bb9904 100644 --- a/tests/merkle_tree/balance-tree.ts +++ b/tests/merkle_tree/balance-tree.ts @@ -3,10 +3,15 @@ import { sha256 } from "js-sha256"; import { MerkleTree } from "./merkle-tree"; - export class BalanceTree { private readonly _tree: MerkleTree; - constructor(balances: { account: web3.PublicKey; amountUnlocked: BN, amountLocked: BN }[]) { + constructor( + balances: { + account: web3.PublicKey; + amountUnlocked: BN; + amountLocked: BN; + }[] + ) { this._tree = new MerkleTree( balances.map(({ account, amountUnlocked, amountLocked }, index) => { return BalanceTree.toNode(account, amountUnlocked, amountLocked); @@ -30,7 +35,11 @@ export class BalanceTree { } // keccak256(abi.encode(index, account, amount)) - static toNode(account: web3.PublicKey, amountUnlocked: BN, amountLocked: BN): Buffer { + static toNode( + account: web3.PublicKey, + amountUnlocked: BN, + amountLocked: BN + ): Buffer { const buf = Buffer.concat([ account.toBuffer(), new BN(amountUnlocked).toArrayLike(Buffer, "le", 8), @@ -38,10 +47,7 @@ export class BalanceTree { ]); const hashedBuff = Buffer.from(sha256(buf), "hex"); - const bufWithPrefix = Buffer.concat([ - Buffer.from([0]), - hashedBuff - ]); + const bufWithPrefix = Buffer.concat([Buffer.from([0]), hashedBuff]); return Buffer.from(sha256(bufWithPrefix), "hex"); } @@ -51,15 +57,43 @@ export class BalanceTree { } // returns the hex bytes32 values of the proof - getHexProof(account: web3.PublicKey, amountUnlocked: BN, amountLocked: BN): string[] { - return this._tree.getHexProof(BalanceTree.toNode(account, amountUnlocked, amountLocked)); + getHexProof( + account: web3.PublicKey, + amountUnlocked: BN, + amountLocked: BN + ): string[] { + return this._tree.getHexProof( + BalanceTree.toNode(account, amountUnlocked, amountLocked) + ); } getRoot(): Buffer { return this._tree.getRoot(); } - getProof(account: web3.PublicKey, amountUnlocked: BN, amountLocked: BN): Buffer[] { - return this._tree.getProof(BalanceTree.toNode(account, amountUnlocked, amountLocked)); + getProof( + account: web3.PublicKey, + amountUnlocked: BN, + amountLocked: BN + ): Buffer[] { + return this._tree.getProof( + BalanceTree.toNode(account, amountUnlocked, amountLocked) + ); + } + + getCanopyNodes(depth: number): Buffer[] { + return this._tree.getCanopyNodes(depth); + } + + getPartialProof( + account: web3.PublicKey, + amountUnlocked: BN, + amountLocked: BN, + depth: number + ): { proof: Buffer[]; index: number } { + return this._tree.getPartialProof( + BalanceTree.toNode(account, amountUnlocked, amountLocked), + depth + ); } } diff --git a/tests/merkle_tree/merkle-tree.ts b/tests/merkle_tree/merkle-tree.ts index 363e252..6151f4d 100644 --- a/tests/merkle_tree/merkle-tree.ts +++ b/tests/merkle_tree/merkle-tree.ts @@ -28,7 +28,10 @@ function bufArrToHexArr(arr: Buffer[]): string[] { } function sortAndConcat(...args: Buffer[]): Buffer { - return Buffer.concat([Buffer.from([1]), Buffer.concat([...args].sort(Buffer.compare.bind(null)))]); + return Buffer.concat([ + Buffer.from([1]), + Buffer.concat([...args].sort(Buffer.compare.bind(null))), + ]); } export class MerkleTree { @@ -99,6 +102,11 @@ export class MerkleTree { return Buffer.from(sha256(sortAndConcat(first, second)), "hex"); } + getCanopyNodes(depth: number): Buffer[] { + // if set depth = 2, it's mean we will store all nodes at layer 2 of merkle tree onchain. + return this._layers[this._layers.length - depth - 1]; + } + getRoot(): Buffer { const root = this._layers[this._layers.length - 1]?.[0]; invariant(root, "root"); @@ -111,7 +119,6 @@ export class MerkleTree { getProof(el: Buffer): Buffer[] { const initialIdx = this._bufferElementPositionIndex[el.toString("hex")]; - if (typeof initialIdx !== "number") { throw new Error("Element does not exist in Merkle tree"); } @@ -119,17 +126,52 @@ export class MerkleTree { let idx = initialIdx; return this._layers.reduce((proof, layer) => { const pairElement = getPairElement(idx, layer); - if (pairElement) { proof.push(pairElement); } idx = Math.floor(idx / 2); - return proof; }, []); } + getPartialProof( + el: Buffer, + depth: number + ): { proof: Buffer[]; index: number } { + const initialIdx = this._bufferElementPositionIndex[el.toString("hex")]; + + if (typeof initialIdx !== "number") { + throw new Error("Element does not exist in Merkle tree"); + } + + const partialLayers = this._layers.slice(0, this._layers.length - depth - 1); + + const { proof, idx, leafIndex } = partialLayers.reduce( + (acc, layer) => { + const pairElement = getPairElement(acc.idx, layer); + if (pairElement) { + acc.proof.push(pairElement); + if (acc.leafIndex === null) { + acc.leafIndex = acc.idx; + } + } + acc.idx = Math.floor(acc.idx / 2); + return acc; + }, + { + proof: [] as Buffer[], + idx: initialIdx, + leafIndex: null as number | null, + } + ); + + // If no proof elements were found, set leafIndex to the final idx + const finalLeafIndex = proof.length ? leafIndex! : idx; + + return { proof, index: finalLeafIndex }; + } + getHexProof(el: Buffer): string[] { const proof = this.getProof(el); diff --git a/tests/test_canopy_is_root.ts b/tests/test_canopy_is_root.ts new file mode 100644 index 0000000..f18003e --- /dev/null +++ b/tests/test_canopy_is_root.ts @@ -0,0 +1,251 @@ +import * as anchor from "@coral-xyz/anchor"; +import { BalanceTree } from "./merkle_tree"; +import { Wallet, web3 } from "@coral-xyz/anchor"; +import { + ADMIN, + claim, + claimLocked, + clawBack, + createCanopyTree, + createNewDistributor, + createNewDistributorRoot, + fundDistributorRoot, + fundMerkleDistributorFromRoot, +} from "./merkle_distributor"; +import { + createAndFundWallet, + getBlockTime, + getOrCreateAssociatedTokenAccountWrap, + getRandomInt, + sleep, +} from "./common"; +import { BN } from "bn.js"; +import { Keypair, PublicKey } from "@solana/web3.js"; +import { createMint, mintTo, TOKEN_PROGRAM_ID } from "@solana/spl-token"; +const provider = anchor.AnchorProvider.env(); + +describe("Canopy nodes is root", () => { + let admin = Keypair.generate(); + let tree: BalanceTree; + let maxNumNodes = 5; + let whitelistedKPs: web3.Keypair[] = []; + let amountUnlockedArr: anchor.BN[] = []; + let amountLockedArr: anchor.BN[] = []; + let totalClaim = new BN(0); + let mint: PublicKey; + let depth = 0; + let maxClaimAmount: anchor.BN; + let maxDistributor = new BN(1) + + before(async () => { + await createAndFundWallet(provider.connection, ADMIN); + await createAndFundWallet(provider.connection, admin); + + for (let i = 0; i < maxNumNodes; i++) { + const result = await createAndFundWallet(provider.connection); + whitelistedKPs.push(result.keypair); + let amountUnlocked = new BN(getRandomInt(1000, 20000)); + let amountLocked = new BN(getRandomInt(1000, 20000)); + + amountUnlockedArr.push(amountUnlocked); + amountLockedArr.push(amountLocked); + totalClaim = totalClaim.add(amountUnlocked).add(amountLocked); + } + + maxClaimAmount = totalClaim; + + tree = new BalanceTree( + whitelistedKPs.map((kp, index) => { + return { + account: kp.publicKey, + amountUnlocked: amountUnlockedArr[index], + amountLocked: amountLockedArr[index], + }; + }) + ); + + mint = await createMint( + provider.connection, + ADMIN, + ADMIN.publicKey, + null, + 6, + web3.Keypair.generate(), + { + commitment: "confirmed", + }, + TOKEN_PROGRAM_ID + ); + + // mint to admin + console.log("Mint to admin"); + const adminTokenAccount = await getOrCreateAssociatedTokenAccountWrap( + provider.connection, + admin, + mint, + admin.publicKey + ); + + await mintTo( + provider.connection, + ADMIN, + mint, + adminTokenAccount, + ADMIN, + totalClaim.toNumber() + ); + }); + it("Full flow", async () => { + console.log("create distributor"); + let currentTime = await getBlockTime(provider.connection); + let startVestingTs = new BN(currentTime + 6); + let endVestingTs = new BN(currentTime + 9); + let clawbackStartTs = new BN(currentTime + 10); + let activationType = 1; // timestamp + let activationPoint = new BN(currentTime + 5); + let closable = false; + let totalBonus = new BN(0); + let bonusVestingDuration = new BN(0); + let claimType = 0; + let operator = web3.SystemProgram.programId; + let locker = web3.SystemProgram.programId; + let canopyBufNodes = tree.getCanopyNodes(depth); + //// + let canopyNodes = []; + canopyBufNodes.forEach(function (value) { + canopyNodes.push(Array.from(new Uint8Array(value))); + }); + + + let clawbackReceiver = await getOrCreateAssociatedTokenAccountWrap( + provider.connection, + ADMIN, + mint, + ADMIN.publicKey + ); + + // create distributor root + let { distributorRoot, distributorRootVault } = + await createNewDistributorRoot({ + admin, + mint, + maxClaimAmount, + maxDistributor, + }); + + + let { distributor, tokenVault } = await createNewDistributor({ + admin, + version: 0, + totalClaim, + maxNumNodes: new BN(maxNumNodes), + startVestingTs, + endVestingTs, + clawbackStartTs, + activationPoint, + activationType, + closable, + totalBonus, + bonusVestingDuration, + claimType, + operator, + locker, + mint, + clawbackReceiver, + distributorRoot + }); + + + // create canopy tree correspond with distributor + await createCanopyTree({ + admin, + distributor, + depth, + root: Array.from(new Uint8Array(tree.getRoot())), + canopyNodes, + }); + + // fund to distributor root + await fundDistributorRoot({ + admin, + payer: admin, + distributorRoot, + mint, + maxAmount: maxClaimAmount, + }); + // fund to distributor from root + await fundMerkleDistributorFromRoot({ + admin, + distributorRoot, + distributorRootVault, + distributor, + distributorVault: tokenVault, + }); + + while (true) { + const currentTime = await getBlockTime(provider.connection); + if (currentTime > activationPoint.toNumber()) { + break; + } else { + await sleep(1000); + console.log("Wait until activationPoint"); + } + } + + for (let i = 0; i < maxNumNodes - 1; i++) { + console.log("claim index: ", i); + var proofBuffers = tree.getPartialProof( + whitelistedKPs[i].publicKey, + amountUnlockedArr[i], + amountLockedArr[i], + depth + ); + let proof = []; + proofBuffers.proof.forEach(function (value) { + proof.push(Array.from(new Uint8Array(value))); + }); + + await claim({ + distributor, + claimant: whitelistedKPs[i], + amountUnlocked: amountUnlockedArr[i], + amountLocked: amountLockedArr[i], + proof, + leafIndex: proofBuffers.index, + }); + } + + while (true) { + const currentTime = await getBlockTime(provider.connection); + if (currentTime > startVestingTs.toNumber()) { + break; + } else { + await sleep(1000); + console.log("Wait until startVestingTs"); + } + } + console.log("claim locked"); + for (let i = 0; i < maxNumNodes - 1; i++) { + console.log("claim locked index: ", i); + await claimLocked({ + distributor, + claimant: whitelistedKPs[i], + }); + } + + while (true) { + const currentTime = await getBlockTime(provider.connection); + if (currentTime > clawbackStartTs.toNumber()) { + break; + } else { + await sleep(1000); + console.log("Wait until clawbackStartTs"); + } + } + console.log("clawback"); + await clawBack({ + distributor, + payer: ADMIN, + }); + }); +}); diff --git a/tests/test_claim_and_stake_permissioned.ts b/tests/test_claim_and_stake_permissioned.ts index 00e3d9a..df8c1ff 100644 --- a/tests/test_claim_and_stake_permissioned.ts +++ b/tests/test_claim_and_stake_permissioned.ts @@ -1,183 +1,277 @@ import * as anchor from "@coral-xyz/anchor"; import { BalanceTree } from "./merkle_tree"; import { web3 } from "@coral-xyz/anchor"; -import { ADMIN, claimAndStake, claimLockedAndStake, clawBack, createNewDistributor } from "./merkle_distributor"; -import { createAndFundWallet, getBlockTime, getOrCreateAssociatedTokenAccountWrap, getRandomInt, sleep } from "./common"; +import { + ADMIN, + claimAndStake, + claimLockedAndStake, + clawBack, + createCanopyTree, + createNewDistributor, + createNewDistributorRoot, + fundDistributorRoot, + fundMerkleDistributorFromRoot, +} from "./merkle_distributor"; +import { + createAndFundWallet, + getBlockTime, + getOrCreateAssociatedTokenAccountWrap, + getRandomInt, + sleep, +} from "./common"; import { BN } from "bn.js"; import { Keypair, PublicKey } from "@solana/web3.js"; -import { createMint, mintTo, TOKEN_PROGRAM_ID } from "@solana/spl-token"; +import { + createMint, + getAssociatedTokenAddressSync, + mintTo, + TOKEN_PROGRAM_ID, +} from "@solana/spl-token"; import { createNewEscrowWithMaxLock, setupLocker } from "./locked_voter/setup"; const provider = anchor.AnchorProvider.env(); describe("Claim and stake permissioned", () => { - let admin = Keypair.generate(); - let operator = Keypair.generate(); - let tree: BalanceTree; - let maxNumNodes = 5; - let whitelistedKPs: web3.Keypair[] = []; - let amountUnlockedArr: anchor.BN[] = []; - let amountLockedArr: anchor.BN[] = []; - let totalClaim = new BN(0); - let mint: PublicKey; - let locker: PublicKey; - let escrow: PublicKey; - before(async () => { - let escrowOwner = Keypair.generate(); - await createAndFundWallet(provider.connection, ADMIN); - await createAndFundWallet(provider.connection, admin); - await createAndFundWallet(provider.connection, escrowOwner); - - for (let i = 0; i < maxNumNodes; i++) { - const result = await createAndFundWallet(provider.connection); - whitelistedKPs.push(result.keypair); - let amountUnlocked = new BN(getRandomInt(1000, 20000)); - let amountLocked = new BN(getRandomInt(1000, 20000)); - - amountUnlockedArr.push(amountUnlocked); - amountLockedArr.push(amountLocked); - totalClaim = totalClaim.add(amountUnlocked).add(amountLocked); - } - - tree = new BalanceTree( - whitelistedKPs.map((kp, index) => { - return { account: kp.publicKey, amountUnlocked: amountUnlockedArr[index], amountLocked: amountLockedArr[index] }; - }) - ); - - - mint = await createMint( - provider.connection, - ADMIN, - ADMIN.publicKey, - null, - 6, - web3.Keypair.generate(), - { - commitment: "confirmed", - }, - TOKEN_PROGRAM_ID - ); - - console.log("create locker") - locker = await setupLocker({ - payer: ADMIN, - tokenMint: mint, - maxStakeVoteMultiplier: 1, - minStakeDuration: new BN(10), - maxStakeDuration: new BN(10000), - proposalActivationMinVotes: new BN(100) - }); - - console.log("create escrow") - escrow = await createNewEscrowWithMaxLock({ - locker, - escrowOwner, - }); - - }) - it("Full flow", async () => { - console.log("create distributor"); - let currentTime = await getBlockTime(provider.connection); - let startVestingTs = new BN(currentTime + 3); - let endVestingTs = new BN(currentTime + 6); - let clawbackStartTs = new BN(currentTime + 7); - let activationType = 1; // timestamp - let activationPoint = new BN(currentTime + 2); - let closable = false; - let totalBonus = new BN(0); - let bonusVestingDuration = new BN(0); - let claimType = 3; - - let clawbackReceiver = await getOrCreateAssociatedTokenAccountWrap(provider.connection, ADMIN, mint, ADMIN.publicKey); - let { distributor, tokenVault } = await createNewDistributor({ - admin, - version: 0, - root: tree.getRoot(), - totalClaim, - maxNumNodes: new BN(maxNumNodes), - startVestingTs, - endVestingTs, - clawbackStartTs, - activationPoint, - activationType, - closable, - totalBonus, - bonusVestingDuration, - claimType, - operator: operator.publicKey, - locker, - mint, - clawbackReceiver, - }); - // mint - await mintTo(provider.connection, ADMIN, mint, tokenVault, ADMIN, totalClaim.toNumber()); - - while (true) { - const currentTime = await getBlockTime(provider.connection); - if (currentTime > activationPoint.toNumber()) { - break; - } else { - await sleep(1000); - console.log("Wait until activationPoint"); - } - } - - console.log("claim and stake") - for (let i = 0; i < maxNumNodes - 1; i++) { - var proofBuffers = tree.getProof( - whitelistedKPs[i].publicKey, - amountUnlockedArr[i], - amountLockedArr[i] - ); - let proof = []; - proofBuffers.forEach(function (value) { - proof.push(Array.from(new Uint8Array(value))); - }); - console.log("claim index: ", i); - await claimAndStake({ - distributor, - claimant: whitelistedKPs[i], - amountUnlocked: amountUnlockedArr[i], - amountLocked: amountLockedArr[i], - proof, - escrow, - operator, - }) - } - - while (true) { - const currentTime = await getBlockTime(provider.connection); - if (currentTime > startVestingTs.toNumber()) { - break; - } else { - await sleep(1000); - console.log("Wait until startVestingTs"); - } - } - console.log("claim locked") - for (let i = 0; i < maxNumNodes - 1; i++) { - console.log("claim locked index: ", i); - await claimLockedAndStake({ - distributor, - claimant: whitelistedKPs[i], - escrow, - operator, - }) - } - - while (true) { - const currentTime = await getBlockTime(provider.connection); - if (currentTime > clawbackStartTs.toNumber()) { - break; - } else { - await sleep(1000); - console.log("Wait until clawbackStartTs"); - } - } - console.log("clawback") - await clawBack({ - distributor, - payer: ADMIN, - }) - }) -}) \ No newline at end of file + let admin = Keypair.generate(); + let operator = Keypair.generate(); + let tree: BalanceTree; + let maxNumNodes = 5; + let whitelistedKPs: web3.Keypair[] = []; + let amountUnlockedArr: anchor.BN[] = []; + let amountLockedArr: anchor.BN[] = []; + let totalClaim = new BN(0); + let mint: PublicKey; + let locker: PublicKey; + let escrow: PublicKey; + let depth = 2; + let maxClaimAmount: anchor.BN; + let maxDistributor: anchor.BN; + before(async () => { + let escrowOwner = Keypair.generate(); + await createAndFundWallet(provider.connection, ADMIN); + await createAndFundWallet(provider.connection, admin); + await createAndFundWallet(provider.connection, escrowOwner); + + for (let i = 0; i < maxNumNodes; i++) { + const result = await createAndFundWallet(provider.connection); + whitelistedKPs.push(result.keypair); + let amountUnlocked = new BN(getRandomInt(1000, 20000)); + let amountLocked = new BN(getRandomInt(1000, 20000)); + + amountUnlockedArr.push(amountUnlocked); + amountLockedArr.push(amountLocked); + totalClaim = totalClaim.add(amountUnlocked).add(amountLocked); + } + maxClaimAmount = totalClaim; + maxDistributor = new anchor.BN(maxNumNodes); + + tree = new BalanceTree( + whitelistedKPs.map((kp, index) => { + return { + account: kp.publicKey, + amountUnlocked: amountUnlockedArr[index], + amountLocked: amountLockedArr[index], + }; + }) + ); + + mint = await createMint( + provider.connection, + ADMIN, + ADMIN.publicKey, + null, + 6, + web3.Keypair.generate(), + { + commitment: "confirmed", + }, + TOKEN_PROGRAM_ID + ); + + // mint + console.log("Mint to admin"); + const adminTokenAccount = await getOrCreateAssociatedTokenAccountWrap( + provider.connection, + admin, + mint, + admin.publicKey + ); + + await mintTo( + provider.connection, + ADMIN, + mint, + adminTokenAccount, + ADMIN, + totalClaim.toNumber() + ); + + console.log("create locker"); + locker = await setupLocker({ + payer: ADMIN, + tokenMint: mint, + maxStakeVoteMultiplier: 1, + minStakeDuration: new BN(10), + maxStakeDuration: new BN(10000), + proposalActivationMinVotes: new BN(100), + }); + + console.log("create escrow"); + escrow = await createNewEscrowWithMaxLock({ + locker, + escrowOwner, + }); + }); + it("Full flow", async () => { + console.log("create distributor"); + let currentTime = await getBlockTime(provider.connection); + let startVestingTs = new BN(currentTime + 6); + let endVestingTs = new BN(currentTime + 9); + let clawbackStartTs = new BN(currentTime + 10); + let activationType = 1; // timestamp + let activationPoint = new BN(currentTime + 5); + let closable = false; + let totalBonus = new BN(0); + let bonusVestingDuration = new BN(0); + let claimType = 3; + let canopyBufNodes = tree.getCanopyNodes(depth); + //// + let canopyNodes = []; + canopyBufNodes.forEach(function (value) { + canopyNodes.push(Array.from(new Uint8Array(value))); + }); + + let clawbackReceiver = await getOrCreateAssociatedTokenAccountWrap( + provider.connection, + ADMIN, + mint, + ADMIN.publicKey + ); + + // create distributor root + let { distributorRoot, distributorRootVault } = + await createNewDistributorRoot({ + admin, + mint, + maxClaimAmount, + maxDistributor, + }); + + // create distributor + let { distributor, tokenVault } = await createNewDistributor({ + admin, + version: 0, + totalClaim, + maxNumNodes: new BN(maxNumNodes), + startVestingTs, + endVestingTs, + clawbackStartTs, + activationPoint, + activationType, + closable, + totalBonus, + bonusVestingDuration, + claimType, + operator: operator.publicKey, + locker, + mint, + clawbackReceiver, + distributorRoot, + }); + + // create canopy tree correspond with distributor + await createCanopyTree({ + admin, + distributor, + depth, + root: Array.from(new Uint8Array(tree.getRoot())), + canopyNodes, + }); + + // fund to distributor root + await fundDistributorRoot({ + admin, + payer: admin, + distributorRoot, + mint, + maxAmount: maxClaimAmount, + }); + // fund to distributor from root + await fundMerkleDistributorFromRoot({ + admin, + distributorRoot, + distributorRootVault, + distributor, + distributorVault: tokenVault, + }); + + while (true) { + const currentTime = await getBlockTime(provider.connection); + if (currentTime > activationPoint.toNumber()) { + break; + } else { + await sleep(1000); + console.log("Wait until activationPoint"); + } + } + + console.log("claim and stake"); + for (let i = 0; i < maxNumNodes - 1; i++) { + var proofBuffers = tree.getPartialProof( + whitelistedKPs[i].publicKey, + amountUnlockedArr[i], + amountLockedArr[i], + depth + ); + let proof = []; + proofBuffers.proof.forEach(function (value) { + proof.push(Array.from(new Uint8Array(value))); + }); + console.log("claim index: ", i); + await claimAndStake({ + distributor, + claimant: whitelistedKPs[i], + amountUnlocked: amountUnlockedArr[i], + amountLocked: amountLockedArr[i], + proof, + escrow, + operator, + leafIndex: proofBuffers.index, + }); + } + + while (true) { + const currentTime = await getBlockTime(provider.connection); + if (currentTime > startVestingTs.toNumber()) { + break; + } else { + await sleep(1000); + console.log("Wait until startVestingTs"); + } + } + console.log("claim locked"); + for (let i = 0; i < maxNumNodes - 1; i++) { + console.log("claim locked index: ", i); + await claimLockedAndStake({ + distributor, + claimant: whitelistedKPs[i], + escrow, + operator, + }); + } + + while (true) { + const currentTime = await getBlockTime(provider.connection); + if (currentTime > clawbackStartTs.toNumber()) { + break; + } else { + await sleep(1000); + console.log("Wait until clawbackStartTs"); + } + } + console.log("clawback"); + await clawBack({ + distributor, + payer: ADMIN, + }); + }); +}); diff --git a/tests/test_claim_and_stake_permissionless.ts b/tests/test_claim_and_stake_permissionless.ts index ed0ecd8..8997bf8 100644 --- a/tests/test_claim_and_stake_permissionless.ts +++ b/tests/test_claim_and_stake_permissionless.ts @@ -1,8 +1,24 @@ import * as anchor from "@coral-xyz/anchor"; import { BalanceTree } from "./merkle_tree"; import { web3 } from "@coral-xyz/anchor"; -import { ADMIN, claimAndStake, claimLockedAndStake, clawBack, createNewDistributor } from "./merkle_distributor"; -import { createAndFundWallet, getBlockTime, getOrCreateAssociatedTokenAccountWrap, getRandomInt, sleep } from "./common"; +import { + ADMIN, + claimAndStake, + claimLockedAndStake, + clawBack, + createCanopyTree, + createNewDistributor, + createNewDistributorRoot, + fundDistributorRoot, + fundMerkleDistributorFromRoot, +} from "./merkle_distributor"; +import { + createAndFundWallet, + getBlockTime, + getOrCreateAssociatedTokenAccountWrap, + getRandomInt, + sleep, +} from "./common"; import { BN } from "bn.js"; import { Keypair, PublicKey } from "@solana/web3.js"; import { createMint, mintTo, TOKEN_PROGRAM_ID } from "@solana/spl-token"; @@ -10,172 +26,245 @@ import { createNewEscrowWithMaxLock, setupLocker } from "./locked_voter/setup"; const provider = anchor.AnchorProvider.env(); describe("Claim and stake permissionless", () => { - let admin = Keypair.generate(); - let tree: BalanceTree; - let maxNumNodes = 5; - let whitelistedKPs: web3.Keypair[] = []; - let amountUnlockedArr: anchor.BN[] = []; - let amountLockedArr: anchor.BN[] = []; - let totalClaim = new BN(0); - let mint: PublicKey; - let locker: PublicKey; - let escrow: PublicKey; - before(async () => { - let escrowOwner = Keypair.generate(); - await createAndFundWallet(provider.connection, ADMIN); - await createAndFundWallet(provider.connection, admin); - await createAndFundWallet(provider.connection, escrowOwner); - - for (let i = 0; i < maxNumNodes; i++) { - const result = await createAndFundWallet(provider.connection); - whitelistedKPs.push(result.keypair); - let amountUnlocked = new BN(getRandomInt(1000, 20000)); - let amountLocked = new BN(getRandomInt(1000, 20000)); - - amountUnlockedArr.push(amountUnlocked); - amountLockedArr.push(amountLocked); - totalClaim = totalClaim.add(amountUnlocked).add(amountLocked); - } - - tree = new BalanceTree( - whitelistedKPs.map((kp, index) => { - return { account: kp.publicKey, amountUnlocked: amountUnlockedArr[index], amountLocked: amountLockedArr[index] }; - }) - ); - - - mint = await createMint( - provider.connection, - ADMIN, - ADMIN.publicKey, - null, - 6, - web3.Keypair.generate(), - { - commitment: "confirmed", - }, - TOKEN_PROGRAM_ID - ); - - console.log("create locker") - locker = await setupLocker({ - payer: ADMIN, - tokenMint: mint, - maxStakeVoteMultiplier: 1, - minStakeDuration: new BN(10), - maxStakeDuration: new BN(10000), - proposalActivationMinVotes: new BN(100) - }); - - console.log("create escrow") - escrow = await createNewEscrowWithMaxLock({ - locker, - escrowOwner, - }); - - }) - it("Full flow", async () => { - console.log("create distributor"); - let currentTime = await getBlockTime(provider.connection); - let startVestingTs = new BN(currentTime + 3); - let endVestingTs = new BN(currentTime + 6); - let clawbackStartTs = new BN(currentTime + 7); - let activationType = 1; // timestamp - let activationPoint = new BN(currentTime + 2); - let closable = false; - let totalBonus = new BN(0); - let bonusVestingDuration = new BN(0); - let claimType = 2; - let operator = web3.SystemProgram.programId; - - let clawbackReceiver = await getOrCreateAssociatedTokenAccountWrap(provider.connection, ADMIN, mint, ADMIN.publicKey); - let { distributor, tokenVault } = await createNewDistributor({ - admin, - version: 0, - root: tree.getRoot(), - totalClaim, - maxNumNodes: new BN(maxNumNodes), - startVestingTs, - endVestingTs, - clawbackStartTs, - activationPoint, - activationType, - closable, - totalBonus, - bonusVestingDuration, - claimType, - operator, - locker, - mint, - clawbackReceiver, - }); - // mint - await mintTo(provider.connection, ADMIN, mint, tokenVault, ADMIN, totalClaim.toNumber()); - - while (true) { - const currentTime = await getBlockTime(provider.connection); - if (currentTime > activationPoint.toNumber()) { - break; - } else { - await sleep(1000); - console.log("Wait until activationPoint"); - } - } - - console.log("claim and stake") - for (let i = 0; i < maxNumNodes - 1; i++) { - var proofBuffers = tree.getProof( - whitelistedKPs[i].publicKey, - amountUnlockedArr[i], - amountLockedArr[i] - ); - let proof = []; - proofBuffers.forEach(function (value) { - proof.push(Array.from(new Uint8Array(value))); - }); - console.log("claim index: ", i); - await claimAndStake({ - distributor, - claimant: whitelistedKPs[i], - amountUnlocked: amountUnlockedArr[i], - amountLocked: amountLockedArr[i], - proof, - escrow, - }) - } - - while (true) { - const currentTime = await getBlockTime(provider.connection); - if (currentTime > startVestingTs.toNumber()) { - break; - } else { - await sleep(1000); - console.log("Wait until startVestingTs"); - } - } - console.log("claim locked") - for (let i = 0; i < maxNumNodes - 1; i++) { - console.log("claim locked index: ", i); - await claimLockedAndStake({ - distributor, - claimant: whitelistedKPs[i], - escrow, - }) - } - - while (true) { - const currentTime = await getBlockTime(provider.connection); - if (currentTime > clawbackStartTs.toNumber()) { - break; - } else { - await sleep(1000); - console.log("Wait until clawbackStartTs"); - } - } - console.log("clawback") - await clawBack({ - distributor, - payer: ADMIN, - }) - }) -}) \ No newline at end of file + let admin = Keypair.generate(); + let tree: BalanceTree; + let maxNumNodes = 5; + let whitelistedKPs: web3.Keypair[] = []; + let amountUnlockedArr: anchor.BN[] = []; + let amountLockedArr: anchor.BN[] = []; + let totalClaim = new BN(0); + let mint: PublicKey; + let locker: PublicKey; + let escrow: PublicKey; + let depth = 2; + let maxClaimAmount: anchor.BN; + let maxDistributor: anchor.BN; + before(async () => { + let escrowOwner = Keypair.generate(); + await createAndFundWallet(provider.connection, ADMIN); + await createAndFundWallet(provider.connection, admin); + await createAndFundWallet(provider.connection, escrowOwner); + + for (let i = 0; i < maxNumNodes; i++) { + const result = await createAndFundWallet(provider.connection); + whitelistedKPs.push(result.keypair); + let amountUnlocked = new BN(getRandomInt(1000, 20000)); + let amountLocked = new BN(getRandomInt(1000, 20000)); + + amountUnlockedArr.push(amountUnlocked); + amountLockedArr.push(amountLocked); + totalClaim = totalClaim.add(amountUnlocked).add(amountLocked); + } + + maxClaimAmount = totalClaim; + maxDistributor = new anchor.BN(maxNumNodes); + + tree = new BalanceTree( + whitelistedKPs.map((kp, index) => { + return { + account: kp.publicKey, + amountUnlocked: amountUnlockedArr[index], + amountLocked: amountLockedArr[index], + }; + }) + ); + + mint = await createMint( + provider.connection, + ADMIN, + ADMIN.publicKey, + null, + 6, + web3.Keypair.generate(), + { + commitment: "confirmed", + }, + TOKEN_PROGRAM_ID + ); + + // mint + console.log("Mint to admin"); + const adminTokenAccount = await getOrCreateAssociatedTokenAccountWrap( + provider.connection, + admin, + mint, + admin.publicKey + ); + + await mintTo( + provider.connection, + ADMIN, + mint, + adminTokenAccount, + ADMIN, + totalClaim.toNumber() + ); + + console.log("create locker"); + locker = await setupLocker({ + payer: ADMIN, + tokenMint: mint, + maxStakeVoteMultiplier: 1, + minStakeDuration: new BN(10), + maxStakeDuration: new BN(10000), + proposalActivationMinVotes: new BN(100), + }); + + console.log("create escrow"); + escrow = await createNewEscrowWithMaxLock({ + locker, + escrowOwner, + }); + }); + it("Full flow", async () => { + console.log("create distributor"); + let currentTime = await getBlockTime(provider.connection); + let startVestingTs = new BN(currentTime + 6); + let endVestingTs = new BN(currentTime + 9); + let clawbackStartTs = new BN(currentTime + 10); + let activationType = 1; // timestamp + let activationPoint = new BN(currentTime + 5); + let closable = false; + let totalBonus = new BN(0); + let bonusVestingDuration = new BN(0); + let claimType = 2; + let operator = web3.SystemProgram.programId; + let canopyBufNodes = tree.getCanopyNodes(depth); + //// + let canopyNodes = []; + canopyBufNodes.forEach(function (value) { + canopyNodes.push(Array.from(new Uint8Array(value))); + }); + + let clawbackReceiver = await getOrCreateAssociatedTokenAccountWrap( + provider.connection, + ADMIN, + mint, + ADMIN.publicKey + ); + + // create distributor root + let { distributorRoot, distributorRootVault } = + await createNewDistributorRoot({ + admin, + mint, + maxClaimAmount, + maxDistributor, + }); + + let { distributor, tokenVault } = await createNewDistributor({ + admin, + version: 0, + totalClaim, + maxNumNodes: new BN(maxNumNodes), + startVestingTs, + endVestingTs, + clawbackStartTs, + activationPoint, + activationType, + closable, + totalBonus, + bonusVestingDuration, + claimType, + operator, + locker, + mint, + clawbackReceiver, + distributorRoot, + }); + + // create canopy tree correspond with distributor + await createCanopyTree({ + admin, + distributor, + depth, + root: Array.from(new Uint8Array(tree.getRoot())), + canopyNodes, + }); + + // fund to distributor root + await fundDistributorRoot({ + admin, + payer: admin, + distributorRoot, + mint, + maxAmount: maxClaimAmount, + }); + // fund to distributor from root + await fundMerkleDistributorFromRoot({ + admin, + distributorRoot, + distributorRootVault, + distributor, + distributorVault: tokenVault, + }); + + while (true) { + const currentTime = await getBlockTime(provider.connection); + if (currentTime > activationPoint.toNumber()) { + break; + } else { + await sleep(1000); + console.log("Wait until activationPoint"); + } + } + + console.log("claim and stake"); + for (let i = 0; i < maxNumNodes - 1; i++) { + var proofBuffers = tree.getPartialProof( + whitelistedKPs[i].publicKey, + amountUnlockedArr[i], + amountLockedArr[i], + depth + ); + let proof = []; + proofBuffers.proof.forEach(function (value) { + proof.push(Array.from(new Uint8Array(value))); + }); + console.log("claim index: ", i); + await claimAndStake({ + distributor, + claimant: whitelistedKPs[i], + amountUnlocked: amountUnlockedArr[i], + amountLocked: amountLockedArr[i], + proof, + escrow, + leafIndex: proofBuffers.index, + }); + } + + while (true) { + const currentTime = await getBlockTime(provider.connection); + if (currentTime > startVestingTs.toNumber()) { + break; + } else { + await sleep(1000); + console.log("Wait until startVestingTs"); + } + } + console.log("claim locked"); + for (let i = 0; i < maxNumNodes - 1; i++) { + console.log("claim locked index: ", i); + await claimLockedAndStake({ + distributor, + claimant: whitelistedKPs[i], + escrow, + }); + } + + while (true) { + const currentTime = await getBlockTime(provider.connection); + if (currentTime > clawbackStartTs.toNumber()) { + break; + } else { + await sleep(1000); + console.log("Wait until clawbackStartTs"); + } + } + console.log("clawback"); + await clawBack({ + distributor, + payer: ADMIN, + }); + }); +}); diff --git a/tests/test_claim_permissioned.ts b/tests/test_claim_permissioned.ts index f55b19d..5a826fe 100644 --- a/tests/test_claim_permissioned.ts +++ b/tests/test_claim_permissioned.ts @@ -1,163 +1,254 @@ import * as anchor from "@coral-xyz/anchor"; import { BalanceTree } from "./merkle_tree"; import { Wallet, web3 } from "@coral-xyz/anchor"; -import { ADMIN, claim, claimLocked, clawBack, createNewDistributor } from "./merkle_distributor"; -import { createAndFundWallet, getBlockTime, getOrCreateAssociatedTokenAccountWrap, getRandomInt, sleep } from "./common"; +import { + ADMIN, + claim, + claimLocked, + clawBack, + createCanopyTree, + createNewDistributor, + createNewDistributorRoot, + fundDistributorRoot, + fundMerkleDistributorFromRoot, +} from "./merkle_distributor"; +import { + createAndFundWallet, + getBlockTime, + getOrCreateAssociatedTokenAccountWrap, + getRandomInt, + sleep, +} from "./common"; import { BN } from "bn.js"; import { Keypair, PublicKey } from "@solana/web3.js"; import { createMint, mintTo, TOKEN_PROGRAM_ID } from "@solana/spl-token"; const provider = anchor.AnchorProvider.env(); describe("Claim permissioned", () => { - let admin = Keypair.generate(); - let operator = Keypair.generate(); - let tree: BalanceTree; - let maxNumNodes = 5; - let whitelistedKPs: web3.Keypair[] = []; - let amountUnlockedArr: anchor.BN[] = []; - let amountLockedArr: anchor.BN[] = []; - let totalClaim = new BN(0); - let mint: PublicKey; - - before(async () => { - await createAndFundWallet(provider.connection, ADMIN); - await createAndFundWallet(provider.connection, admin); - - for (let i = 0; i < maxNumNodes; i++) { - const result = await createAndFundWallet(provider.connection); - whitelistedKPs.push(result.keypair); - let amountUnlocked = new BN(getRandomInt(1000, 20000)); - let amountLocked = new BN(getRandomInt(1000, 20000)); - - amountUnlockedArr.push(amountUnlocked); - amountLockedArr.push(amountLocked); - totalClaim = totalClaim.add(amountUnlocked).add(amountLocked); - } - - tree = new BalanceTree( - whitelistedKPs.map((kp, index) => { - return { account: kp.publicKey, amountUnlocked: amountUnlockedArr[index], amountLocked: amountLockedArr[index] }; - }) - ); - - - mint = await createMint( - provider.connection, - ADMIN, - ADMIN.publicKey, - null, - 6, - web3.Keypair.generate(), - { - commitment: "confirmed", - }, - TOKEN_PROGRAM_ID - ); - - - }) - it("Full flow", async () => { - console.log("create distributor"); - let currentTime = await getBlockTime(provider.connection); - let startVestingTs = new BN(currentTime + 3); - let endVestingTs = new BN(currentTime + 6); - let clawbackStartTs = new BN(currentTime + 7); - let activationType = 1; // timestamp - let activationPoint = new BN(currentTime + 2); - let closable = false; - let totalBonus = new BN(0); - let bonusVestingDuration = new BN(0); - let claimType = 1; - let locker = web3.SystemProgram.programId; - - let clawbackReceiver = await getOrCreateAssociatedTokenAccountWrap(provider.connection, ADMIN, mint, ADMIN.publicKey); - let { distributor, tokenVault } = await createNewDistributor({ - admin, - version: 0, - root: tree.getRoot(), - totalClaim, - maxNumNodes: new BN(maxNumNodes), - startVestingTs, - endVestingTs, - clawbackStartTs, - activationPoint, - activationType, - closable, - totalBonus, - bonusVestingDuration, - claimType, - operator: operator.publicKey, - locker, - mint, - clawbackReceiver, - }); - // mint - await mintTo(provider.connection, ADMIN, mint, tokenVault, ADMIN, totalClaim.toNumber()); - - while (true) { - const currentTime = await getBlockTime(provider.connection); - if (currentTime > activationPoint.toNumber()) { - break; - } else { - await sleep(1000); - console.log("Wait until activationPoint"); - } - } - - console.log("claim") - for (let i = 0; i < maxNumNodes - 1; i++) { - var proofBuffers = tree.getProof( - whitelistedKPs[i].publicKey, - amountUnlockedArr[i], - amountLockedArr[i] - ); - let proof = []; - proofBuffers.forEach(function (value) { - proof.push(Array.from(new Uint8Array(value))); - }); - console.log("claim index: ", i); - await claim({ - distributor, - claimant: whitelistedKPs[i], - amountUnlocked: amountUnlockedArr[i], - amountLocked: amountLockedArr[i], - proof, - operator, - }) - } - - while (true) { - const currentTime = await getBlockTime(provider.connection); - if (currentTime > startVestingTs.toNumber()) { - break; - } else { - await sleep(1000); - console.log("Wait until startVestingTs"); - } - } - console.log("claim locked") - for (let i = 0; i < maxNumNodes - 1; i++) { - console.log("claim locked index: ", i); - await claimLocked({ - distributor, - operator, - claimant: whitelistedKPs[i], - }) - } - - while (true) { - const currentTime = await getBlockTime(provider.connection); - if (currentTime > clawbackStartTs.toNumber()) { - break; - } else { - await sleep(1000); - console.log("Wait until clawbackStartTs"); - } - } - console.log("clawback") - await clawBack({ - distributor, - payer: ADMIN, - }) - }) -}) \ No newline at end of file + let admin = Keypair.generate(); + let operator = Keypair.generate(); + let tree: BalanceTree; + let maxNumNodes = 5; + let whitelistedKPs: web3.Keypair[] = []; + let amountUnlockedArr: anchor.BN[] = []; + let amountLockedArr: anchor.BN[] = []; + let totalClaim = new BN(0); + let mint: PublicKey; + let depth = 2; + let maxClaimAmount: anchor.BN; + let maxDistributor: anchor.BN; + + before(async () => { + await createAndFundWallet(provider.connection, ADMIN); + await createAndFundWallet(provider.connection, admin); + + for (let i = 0; i < maxNumNodes; i++) { + const result = await createAndFundWallet(provider.connection); + whitelistedKPs.push(result.keypair); + let amountUnlocked = new BN(getRandomInt(1000, 20000)); + let amountLocked = new BN(getRandomInt(1000, 20000)); + + amountUnlockedArr.push(amountUnlocked); + amountLockedArr.push(amountLocked); + totalClaim = totalClaim.add(amountUnlocked).add(amountLocked); + } + + maxClaimAmount = totalClaim; + maxDistributor = new anchor.BN(maxNumNodes); + + tree = new BalanceTree( + whitelistedKPs.map((kp, index) => { + return { + account: kp.publicKey, + amountUnlocked: amountUnlockedArr[index], + amountLocked: amountLockedArr[index], + }; + }) + ); + + mint = await createMint( + provider.connection, + ADMIN, + ADMIN.publicKey, + null, + 6, + web3.Keypair.generate(), + { + commitment: "confirmed", + }, + TOKEN_PROGRAM_ID + ); + + // mint to admin + console.log("Mint to admin"); + const adminTokenAccount = await getOrCreateAssociatedTokenAccountWrap( + provider.connection, + admin, + mint, + admin.publicKey + ); + + await mintTo( + provider.connection, + ADMIN, + mint, + adminTokenAccount, + ADMIN, + totalClaim.toNumber() + ); + }); + it("Full flow", async () => { + console.log("create distributor"); + let currentTime = await getBlockTime(provider.connection); + let startVestingTs = new BN(currentTime + 7); + let endVestingTs = new BN(currentTime + 10); + let clawbackStartTs = new BN(currentTime + 11); + let activationType = 1; // timestamp + let activationPoint = new BN(currentTime + 6); + let closable = false; + let totalBonus = new BN(0); + let bonusVestingDuration = new BN(0); + let claimType = 1; + let locker = web3.SystemProgram.programId; + let canopyBufNodes = tree.getCanopyNodes(depth); + //// + let canopyNodes = []; + canopyBufNodes.forEach(function (value) { + canopyNodes.push(Array.from(new Uint8Array(value))); + }); + + let clawbackReceiver = await getOrCreateAssociatedTokenAccountWrap( + provider.connection, + ADMIN, + mint, + ADMIN.publicKey + ); + + // create distributor root + let { distributorRoot, distributorRootVault } = + await createNewDistributorRoot({ + admin, + mint, + maxClaimAmount, + maxDistributor, + }); + + + let { distributor, tokenVault } = await createNewDistributor({ + admin, + version: 0, + totalClaim, + maxNumNodes: new BN(maxNumNodes), + startVestingTs, + endVestingTs, + clawbackStartTs, + activationPoint, + activationType, + closable, + totalBonus, + bonusVestingDuration, + claimType, + operator: operator.publicKey, + locker, + mint, + clawbackReceiver, + distributorRoot + }); + + + // create canopy tree correspond with distributor + await createCanopyTree({ + admin, + distributor, + depth, + root: Array.from(new Uint8Array(tree.getRoot())), + canopyNodes, + }); + + // fund to distributor root + await fundDistributorRoot({ + admin, + payer: admin, + distributorRoot, + mint, + maxAmount: maxClaimAmount, + }); + // fund to distributor from root + await fundMerkleDistributorFromRoot({ + admin, + distributorRoot, + distributorRootVault, + distributor, + distributorVault: tokenVault, + }); + + while (true) { + const currentTime = await getBlockTime(provider.connection); + if (currentTime > activationPoint.toNumber()) { + break; + } else { + await sleep(1000); + console.log("Wait until activationPoint"); + } + } + + console.log("claim"); + for (let i = 0; i < maxNumNodes - 1; i++) { + console.log("claim index: ", i); + var proofBuffers = tree.getPartialProof( + whitelistedKPs[i].publicKey, + amountUnlockedArr[i], + amountLockedArr[i], + depth + ); + let proof = []; + proofBuffers.proof.forEach(function (value) { + proof.push(Array.from(new Uint8Array(value))); + }); + await claim({ + distributor, + claimant: whitelistedKPs[i], + amountUnlocked: amountUnlockedArr[i], + amountLocked: amountLockedArr[i], + proof, + operator, + leafIndex: proofBuffers.index, + }); + + } + + while (true) { + const currentTime = await getBlockTime(provider.connection); + if (currentTime > startVestingTs.toNumber()) { + break; + } else { + await sleep(1000); + console.log("Wait until startVestingTs"); + } + } + console.log("claim locked"); + for (let i = 0; i < maxNumNodes - 1; i++) { + console.log("claim locked index: ", i); + await claimLocked({ + distributor, + operator, + claimant: whitelistedKPs[i], + }); + } + + while (true) { + const currentTime = await getBlockTime(provider.connection); + if (currentTime > clawbackStartTs.toNumber()) { + break; + } else { + await sleep(1000); + console.log("Wait until clawbackStartTs"); + } + } + console.log("clawback"); + await clawBack({ + distributor, + payer: ADMIN, + }); + }); +}); diff --git a/tests/test_claim_permissionless.ts b/tests/test_claim_permissionless.ts index 8a45e53..a2708c7 100644 --- a/tests/test_claim_permissionless.ts +++ b/tests/test_claim_permissionless.ts @@ -1,160 +1,251 @@ import * as anchor from "@coral-xyz/anchor"; import { BalanceTree } from "./merkle_tree"; import { Wallet, web3 } from "@coral-xyz/anchor"; -import { ADMIN, claim, claimLocked, clawBack, createNewDistributor } from "./merkle_distributor"; -import { createAndFundWallet, getBlockTime, getOrCreateAssociatedTokenAccountWrap, getRandomInt, sleep } from "./common"; +import { + ADMIN, + claim, + claimLocked, + clawBack, + createCanopyTree, + createNewDistributor, + createNewDistributorRoot, + fundDistributorRoot, + fundMerkleDistributorFromRoot, +} from "./merkle_distributor"; +import { + createAndFundWallet, + getBlockTime, + getOrCreateAssociatedTokenAccountWrap, + getRandomInt, + sleep, +} from "./common"; import { BN } from "bn.js"; import { Keypair, PublicKey } from "@solana/web3.js"; import { createMint, mintTo, TOKEN_PROGRAM_ID } from "@solana/spl-token"; const provider = anchor.AnchorProvider.env(); describe("Claim permissionless", () => { - let admin = Keypair.generate(); - let tree: BalanceTree; - let maxNumNodes = 5; - let whitelistedKPs: web3.Keypair[] = []; - let amountUnlockedArr: anchor.BN[] = []; - let amountLockedArr: anchor.BN[] = []; - let totalClaim = new BN(0); - let mint: PublicKey; - before(async () => { - await createAndFundWallet(provider.connection, ADMIN); - await createAndFundWallet(provider.connection, admin); - - for (let i = 0; i < maxNumNodes; i++) { - const result = await createAndFundWallet(provider.connection); - whitelistedKPs.push(result.keypair); - let amountUnlocked = new BN(getRandomInt(1000, 20000)); - let amountLocked = new BN(getRandomInt(1000, 20000)); - - amountUnlockedArr.push(amountUnlocked); - amountLockedArr.push(amountLocked); - totalClaim = totalClaim.add(amountUnlocked).add(amountLocked); - } - - tree = new BalanceTree( - whitelistedKPs.map((kp, index) => { - return { account: kp.publicKey, amountUnlocked: amountUnlockedArr[index], amountLocked: amountLockedArr[index] }; - }) - ); - - - mint = await createMint( - provider.connection, - ADMIN, - ADMIN.publicKey, - null, - 6, - web3.Keypair.generate(), - { - commitment: "confirmed", - }, - TOKEN_PROGRAM_ID - ); - - - }) - it("Full flow", async () => { - console.log("create distributor"); - let currentTime = await getBlockTime(provider.connection); - let startVestingTs = new BN(currentTime + 3); - let endVestingTs = new BN(currentTime + 6); - let clawbackStartTs = new BN(currentTime + 7); - let activationType = 1; // timestamp - let activationPoint = new BN(currentTime + 2); - let closable = false; - let totalBonus = new BN(0); - let bonusVestingDuration = new BN(0); - let claimType = 0; - let operator = web3.SystemProgram.programId; - let locker = web3.SystemProgram.programId; - - let clawbackReceiver = await getOrCreateAssociatedTokenAccountWrap(provider.connection, ADMIN, mint, ADMIN.publicKey); - let { distributor, tokenVault } = await createNewDistributor({ - admin, - version: 0, - root: tree.getRoot(), - totalClaim, - maxNumNodes: new BN(maxNumNodes), - startVestingTs, - endVestingTs, - clawbackStartTs, - activationPoint, - activationType, - closable, - totalBonus, - bonusVestingDuration, - claimType, - operator, - locker, - mint, - clawbackReceiver, - }); - // mint - await mintTo(provider.connection, ADMIN, mint, tokenVault, ADMIN, totalClaim.toNumber()); - - while (true) { - const currentTime = await getBlockTime(provider.connection); - if (currentTime > activationPoint.toNumber()) { - break; - } else { - await sleep(1000); - console.log("Wait until activationPoint"); - } - } - - console.log("claim") - for (let i = 0; i < maxNumNodes - 1; i++) { - var proofBuffers = tree.getProof( - whitelistedKPs[i].publicKey, - amountUnlockedArr[i], - amountLockedArr[i] - ); - let proof = []; - proofBuffers.forEach(function (value) { - proof.push(Array.from(new Uint8Array(value))); - }); - console.log("claim index: ", i); - await claim({ - distributor, - claimant: whitelistedKPs[i], - amountUnlocked: amountUnlockedArr[i], - amountLocked: amountLockedArr[i], - proof, - }) - } - - while (true) { - const currentTime = await getBlockTime(provider.connection); - if (currentTime > startVestingTs.toNumber()) { - break; - } else { - await sleep(1000); - console.log("Wait until startVestingTs"); - } - } - console.log("claim locked") - for (let i = 0; i < maxNumNodes - 1; i++) { - console.log("claim locked index: ", i); - await claimLocked({ - distributor, - claimant: whitelistedKPs[i], - }) - } - - while (true) { - const currentTime = await getBlockTime(provider.connection); - if (currentTime > clawbackStartTs.toNumber()) { - break; - } else { - await sleep(1000); - console.log("Wait until clawbackStartTs"); - } - } - console.log("clawback") - await clawBack({ - distributor, - payer: ADMIN, - }) - }) -}) \ No newline at end of file + let admin = Keypair.generate(); + let tree: BalanceTree; + let maxNumNodes = 5; + let whitelistedKPs: web3.Keypair[] = []; + let amountUnlockedArr: anchor.BN[] = []; + let amountLockedArr: anchor.BN[] = []; + let totalClaim = new BN(0); + let mint: PublicKey; + let depth = 2; + let maxClaimAmount: anchor.BN; + let maxDistributor: anchor.BN; + + before(async () => { + await createAndFundWallet(provider.connection, ADMIN); + await createAndFundWallet(provider.connection, admin); + + for (let i = 0; i < maxNumNodes; i++) { + const result = await createAndFundWallet(provider.connection); + whitelistedKPs.push(result.keypair); + let amountUnlocked = new BN(getRandomInt(1000, 20000)); + let amountLocked = new BN(getRandomInt(1000, 20000)); + + amountUnlockedArr.push(amountUnlocked); + amountLockedArr.push(amountLocked); + totalClaim = totalClaim.add(amountUnlocked).add(amountLocked); + } + + maxClaimAmount = totalClaim; + maxDistributor = new anchor.BN(maxNumNodes); + + tree = new BalanceTree( + whitelistedKPs.map((kp, index) => { + return { + account: kp.publicKey, + amountUnlocked: amountUnlockedArr[index], + amountLocked: amountLockedArr[index], + }; + }) + ); + + mint = await createMint( + provider.connection, + ADMIN, + ADMIN.publicKey, + null, + 6, + web3.Keypair.generate(), + { + commitment: "confirmed", + }, + TOKEN_PROGRAM_ID + ); + + // mint to admin + console.log("Mint to admin"); + const adminTokenAccount = await getOrCreateAssociatedTokenAccountWrap( + provider.connection, + admin, + mint, + admin.publicKey + ); + + await mintTo( + provider.connection, + ADMIN, + mint, + adminTokenAccount, + ADMIN, + totalClaim.toNumber() + ); + }); + it("Full flow", async () => { + console.log("create distributor"); + let currentTime = await getBlockTime(provider.connection); + let startVestingTs = new BN(currentTime + 6); + let endVestingTs = new BN(currentTime + 9); + let clawbackStartTs = new BN(currentTime + 10); + let activationType = 1; // timestamp + let activationPoint = new BN(currentTime + 5); + let closable = false; + let totalBonus = new BN(0); + let bonusVestingDuration = new BN(0); + let claimType = 0; + let operator = web3.SystemProgram.programId; + let locker = web3.SystemProgram.programId; + let canopyBufNodes = tree.getCanopyNodes(depth); + //// + let canopyNodes = []; + canopyBufNodes.forEach(function (value) { + canopyNodes.push(Array.from(new Uint8Array(value))); + }); + + let clawbackReceiver = await getOrCreateAssociatedTokenAccountWrap( + provider.connection, + ADMIN, + mint, + ADMIN.publicKey + ); + + // create distributor root + let { distributorRoot, distributorRootVault } = + await createNewDistributorRoot({ + admin, + mint, + maxClaimAmount, + maxDistributor, + }); + + + let { distributor, tokenVault } = await createNewDistributor({ + admin, + version: 0, + totalClaim, + maxNumNodes: new BN(maxNumNodes), + startVestingTs, + endVestingTs, + clawbackStartTs, + activationPoint, + activationType, + closable, + totalBonus, + bonusVestingDuration, + claimType, + operator, + locker, + mint, + clawbackReceiver, + distributorRoot + }); + + + // create canopy tree correspond with distributor + await createCanopyTree({ + admin, + distributor, + depth, + root: Array.from(new Uint8Array(tree.getRoot())), + canopyNodes, + }); + + // fund to distributor root + await fundDistributorRoot({ + admin, + payer: admin, + distributorRoot, + mint, + maxAmount: maxClaimAmount, + }); + // fund to distributor from root + await fundMerkleDistributorFromRoot({ + admin, + distributorRoot, + distributorRootVault, + distributor, + distributorVault: tokenVault, + }); + + while (true) { + const currentTime = await getBlockTime(provider.connection); + if (currentTime > activationPoint.toNumber()) { + break; + } else { + await sleep(1000); + console.log("Wait until activationPoint"); + } + } + + for (let i = 0; i < maxNumNodes - 1; i++) { + console.log("claim index: ", i); + var proofBuffers = tree.getPartialProof( + whitelistedKPs[i].publicKey, + amountUnlockedArr[i], + amountLockedArr[i], + depth + ); + let proof = []; + proofBuffers.proof.forEach(function (value) { + proof.push(Array.from(new Uint8Array(value))); + }); + + await claim({ + distributor, + claimant: whitelistedKPs[i], + amountUnlocked: amountUnlockedArr[i], + amountLocked: amountLockedArr[i], + proof, + leafIndex: proofBuffers.index, + }); + } + + while (true) { + const currentTime = await getBlockTime(provider.connection); + if (currentTime > startVestingTs.toNumber()) { + break; + } else { + await sleep(1000); + console.log("Wait until startVestingTs"); + } + } + console.log("claim locked"); + for (let i = 0; i < maxNumNodes - 1; i++) { + console.log("claim locked index: ", i); + await claimLocked({ + distributor, + claimant: whitelistedKPs[i], + }); + } + + while (true) { + const currentTime = await getBlockTime(provider.connection); + if (currentTime > clawbackStartTs.toNumber()) { + break; + } else { + await sleep(1000); + console.log("Wait until clawbackStartTs"); + } + } + console.log("clawback"); + await clawBack({ + distributor, + payer: ADMIN, + }); + }); +}); diff --git a/tests/test_distributor_root.ts b/tests/test_distributor_root.ts new file mode 100644 index 0000000..0da5723 --- /dev/null +++ b/tests/test_distributor_root.ts @@ -0,0 +1,212 @@ +import * as anchor from "@coral-xyz/anchor"; +import { BalanceTree } from "./merkle_tree"; +import { web3 } from "@coral-xyz/anchor"; +import { + ADMIN, + createCanopyTree, + createNewDistributor, + createNewDistributorRoot, + fundDistributorRoot, + fundMerkleDistributorFromRoot, +} from "./merkle_distributor"; +import { + createAndFundWallet, + getBlockTime, + getOrCreateAssociatedTokenAccountWrap, + getRandomInt, +} from "./common"; +import { BN } from "bn.js"; +import { Keypair, PublicKey } from "@solana/web3.js"; +import { createMint, mintTo, TOKEN_PROGRAM_ID } from "@solana/spl-token"; +import { assert } from "console"; +const provider = anchor.AnchorProvider.env(); + +describe("Distribution Root", () => { + let admin = Keypair.generate(); + let trees: BalanceTree[] = []; + let maxNumNodes = 5; + let whitelistedKPs: web3.Keypair[] = []; + let amountUnlockedArr: anchor.BN[] = []; + let amountLockedArr: anchor.BN[] = []; + let totalClaimEachDistributor: anchor.BN[] = []; + let mint: PublicKey; + let depth = 2; + let ONE_DAY = 86_400; + let maxClaimAmountInAllDistributors = new BN(0); + let maxDistributor = new BN(5); + + before(async () => { + await createAndFundWallet(provider.connection, ADMIN); + await createAndFundWallet(provider.connection, admin); + + for (let i = 0; i < maxDistributor.toNumber(); i++) { + let totalClaim = new anchor.BN(0); + for (let i = 0; i < maxNumNodes; i++) { + const result = await createAndFundWallet(provider.connection); + whitelistedKPs.push(result.keypair); + let amountUnlocked = new BN(getRandomInt(1000, 20000)); + let amountLocked = new BN(getRandomInt(1000, 20000)); + + amountUnlockedArr.push(amountUnlocked); + amountLockedArr.push(amountLocked); + totalClaim = totalClaim.add(amountUnlocked).add(amountLocked); + } + totalClaimEachDistributor.push(totalClaim); + maxClaimAmountInAllDistributors = + maxClaimAmountInAllDistributors.add(totalClaim); + + trees.push( + new BalanceTree( + whitelistedKPs.map((kp, index) => { + return { + account: kp.publicKey, + amountUnlocked: amountUnlockedArr[index], + amountLocked: amountLockedArr[index], + }; + }) + ) + ); + } + + mint = await createMint( + provider.connection, + ADMIN, + ADMIN.publicKey, + null, + 6, + web3.Keypair.generate(), + { + commitment: "confirmed", + }, + TOKEN_PROGRAM_ID + ); + + // mint to admin + console.log("Mint to admin"); + const adminTokenAccount = await getOrCreateAssociatedTokenAccountWrap( + provider.connection, + admin, + mint, + admin.publicKey + ); + + await mintTo( + provider.connection, + ADMIN, + mint, + adminTokenAccount, + ADMIN, + maxClaimAmountInAllDistributors.toNumber() + ); + }); + it("Full flow", async () => { + console.log("create distributor"); + let currentTime = await getBlockTime(provider.connection); + let startVestingTs = new BN(currentTime + ONE_DAY); + let endVestingTs = new BN(currentTime + ONE_DAY * 7); + let clawbackStartTs = new BN(currentTime + ONE_DAY * 8); + let activationType = 1; // timestamp + let activationPoint = new BN(currentTime + ONE_DAY / 2); + let closable = false; + let totalBonus = new BN(0); + let bonusVestingDuration = new BN(0); + let claimType = 0; + let operator = web3.SystemProgram.programId; + let locker = web3.SystemProgram.programId; + + let clawbackReceiver = await getOrCreateAssociatedTokenAccountWrap( + provider.connection, + ADMIN, + mint, + ADMIN.publicKey + ); + + // create distributor root + let { distributorRoot, distributorRootVault } = + await createNewDistributorRoot({ + admin, + mint, + maxClaimAmount: maxClaimAmountInAllDistributors, + maxDistributor, + }); + + const distributors = []; + const distributorVaults = []; + for (let i = 0; i < maxDistributor.toNumber(); i++) { + let canopyBufNodes = trees[i].getCanopyNodes(depth); + let canopyNodes = []; + canopyBufNodes.forEach(function (value) { + canopyNodes.push(Array.from(new Uint8Array(value))); + }); + + let { distributor, tokenVault } = await createNewDistributor({ + admin, + version: 0, + totalClaim: totalClaimEachDistributor[i], + maxNumNodes: new BN(maxNumNodes), + startVestingTs, + endVestingTs, + clawbackStartTs, + activationPoint, + activationType, + closable, + totalBonus, + bonusVestingDuration, + claimType, + operator, + locker, + mint, + clawbackReceiver, + distributorRoot, + }); + + distributors.push(distributor); + distributorVaults.push(tokenVault); + + // create canopy tree correspond with distributor + await createCanopyTree({ + admin, + distributor, + depth, + root: Array.from(new Uint8Array(trees[i].getRoot())), + canopyNodes, + }); + } + + console.log("Fund to distributor root"); + // fund to distributor root + await fundDistributorRoot({ + admin, + payer: admin, + distributorRoot, + mint, + maxAmount: maxClaimAmountInAllDistributors, + }); + + for (let i = 0; i < maxDistributor.toNumber(); i++) { + // fund to distributor from root + await fundMerkleDistributorFromRoot({ + admin, + distributorRoot, + distributorRootVault, + distributor: distributors[i], + distributorVault: distributorVaults[i], + }); + + const distributorVaultBalance = Number( + (await provider.connection.getTokenAccountBalance(distributorVaults[i])) + .value.amount + ); + assert( + distributorVaultBalance == totalClaimEachDistributor[i].toNumber() + ); + } + + // + const distributorRootVaultBalance = Number( + (await provider.connection.getTokenAccountBalance(distributorRootVault)) + .value.amount + ); + assert(distributorRootVaultBalance == 0); + }); +}); diff --git a/verify/src/lib.rs b/verify/src/lib.rs index 1a86fdb..0069b4a 100644 --- a/verify/src/lib.rs +++ b/verify/src/lib.rs @@ -20,4 +20,4 @@ pub fn verify(proof: Vec<[u8; 32]>, root: [u8; 32], leaf: [u8; 32]) -> bool { } // Check if the computed hash (root) is equal to the provided root computed_hash == root -} +} \ No newline at end of file