|
#![allow(unexpected_cfgs)] |
|
use anchor_lang::{ |
|
prelude::*, |
|
solana_program::{instruction::Instruction, program::invoke, system_instruction}, |
|
}; |
|
use anchor_spl::{ |
|
token::spl_token, |
|
}; |
|
|
|
mod events; |
|
mod state; |
|
mod contexts; |
|
|
|
use contexts::*; |
|
use events::*; |
|
use state::*; |
|
|
|
declare_id!("EeT88L4VvDieXZgko1LuG1UYLc2jGaqy5Wichda7EfG5"); |
|
|
|
#[program] |
|
pub mod vault { |
|
use super::*; |
|
|
|
pub fn initialize(ctx: Context<Initialize>) -> Result<()> { |
|
|
|
ctx.accounts.vault.set_inner(Vault::new(ctx.accounts.caller.key())); |
|
|
|
emit!(VaultInitialized { |
|
authority: ctx.accounts.caller.key(), |
|
vault: ctx.accounts.vault.key(), |
|
}); |
|
|
|
let vault = &mut ctx.accounts.vault; |
|
msg!("vault amount to buy = {}", vault.next_round_buy_amount); |
|
|
|
Ok(()) |
|
} |
|
|
|
pub fn set_executor(ctx: Context<SetExecutor>, new_executor: Option<Pubkey>) -> Result<()> { |
|
let vault = &mut ctx.accounts.vault; |
|
let caller = &ctx.accounts.caller.key(); |
|
|
|
require_authority_role(caller, vault)?; |
|
|
|
vault.executor = new_executor; |
|
|
|
emit!(ExecutorChanged { |
|
new_executor, |
|
authority: *caller, |
|
}); |
|
|
|
Ok(()) |
|
} |
|
|
|
pub fn set_user_investment( |
|
ctx: Context<SetUserInvestment>, |
|
user: Pubkey, |
|
investment_per_token: u64, |
|
is_active: bool, |
|
) -> Result<()> { |
|
let vault = &mut ctx.accounts.vault; |
|
let caller = &ctx.accounts.caller.key(); |
|
|
|
require_executor_role(caller, vault)?; |
|
|
|
let user_account = &mut ctx.accounts.user_account; |
|
require!(user_account.user == user, VaultErrorCode::UserMismatch); |
|
|
|
// === CALCULATE OLD AND NEW CONTRIBUTION === |
|
let old_contribution = if user_account.sol_balance >= user_account.investment_per_token |
|
&& user_account.is_active { |
|
user_account.investment_per_token |
|
} else { |
|
0 |
|
}; |
|
|
|
let new_contribution = if user_account.sol_balance >= investment_per_token && is_active { |
|
investment_per_token |
|
} else { |
|
0 |
|
}; |
|
|
|
// === UPDATE USER SETTINGS === |
|
user_account.investment_per_token = investment_per_token; |
|
user_account.is_active = is_active; |
|
|
|
// === UPDATE VAULT BUY AMOUNT === |
|
// Only update during Deposit stage |
|
if vault.stage == VaultStage::Deposit { |
|
vault.update_next_round_amount(old_contribution, new_contribution)?; |
|
} |
|
|
|
emit!(UserSettingsUpdated { |
|
user, |
|
investment_per_token, |
|
is_active, |
|
}); |
|
|
|
Ok(()) |
|
} |
|
|
|
pub fn deposit( |
|
ctx: Context<Deposit>, |
|
user: Pubkey, |
|
) -> Result<()> { |
|
// === SETUP === |
|
let vault = &mut ctx.accounts.vault; |
|
let user_account = &mut ctx.accounts.user_account; |
|
let caller = &ctx.accounts.caller.key(); |
|
|
|
// === VALIDATION === |
|
require!(vault.stage == VaultStage::Deposit, VaultErrorCode::WrongStage); |
|
require_executor_role(caller, vault)?; |
|
|
|
// === ACCOUNT INITIALIZATION (this will deduct rent automatically) === |
|
if user_account.is_uninitialized() { |
|
user_account.set_inner(UserAccount::new(user)); |
|
} |
|
|
|
// === CALCULATE DEPOSIT AMOUNT AFTER ACCOUNT CREATION === |
|
// Now check what's actually left after account creation |
|
let deposit_amount = ctx.accounts.deposit_account.lamports(); |
|
require!(deposit_amount > 0, VaultErrorCode::InsufficientBalance); |
|
|
|
// === CALCULATE & UPDATE BUY AMOUNT === |
|
// If user becomes eligible to participate, add to buy amount |
|
if user_account.sol_balance >= user_account.investment_per_token && user_account.is_active { // TODO: consider removing active/inactive |
|
vault.add_to_next_round(user_account.investment_per_token)?; |
|
} |
|
|
|
// === BALANCE UPDATES === |
|
user_account.add_sol_balance(deposit_amount)?; |
|
vault.add_deposits(deposit_amount)?; |
|
|
|
// === SOL TRANSFER === |
|
let transfer_instruction = system_instruction::transfer( |
|
&ctx.accounts.deposit_account.key(), |
|
&vault.key(), |
|
deposit_amount, |
|
); |
|
|
|
invoke( |
|
&transfer_instruction, |
|
&[ |
|
ctx.accounts.deposit_account.to_account_info(), |
|
vault.to_account_info(), |
|
ctx.accounts.system_program.to_account_info(), |
|
], |
|
)?; |
|
|
|
emit!(DepositMade { |
|
user, |
|
amount: deposit_amount, |
|
new_user_balance: user_account.sol_balance, |
|
total_vault_deposits: vault.deposits, |
|
total_buy_amount: vault.next_round_buy_amount, |
|
}); |
|
|
|
Ok(()) |
|
} |
|
|
|
pub fn buy_tokens_collective( |
|
ctx: Context<BuyTokensCollective>, |
|
target_token: Pubkey, |
|
jupiter_swap_data: Vec<u8>, |
|
min_amount_out: u64, |
|
) -> Result<()> { |
|
// === SETUP & VALIDATION === |
|
let vault = &mut ctx.accounts.vault; |
|
require_executor_role(&ctx.accounts.caller.key(), vault)?; |
|
let buy_amount = vault.next_round_buy_amount; |
|
require!(buy_amount > 0, VaultErrorCode::InsufficientFunds); |
|
|
|
msg!("Instant buy for {} with {} SOL", target_token, buy_amount); |
|
|
|
require!(vault.stage == VaultStage::Buy, VaultErrorCode::WrongStage); |
|
|
|
// === TRACK STARTING BALANCE === |
|
let caller_token_balance_before = ctx.accounts.authority_token_account.amount; // TODO: consider changing the name of the token account |
|
msg!("Authority token balance before: {}", caller_token_balance_before); |
|
|
|
// === EXECUTE JUPITER SWAP === |
|
// Preparing account metadata: Jupiter's swap instruction expects accounts in a specific format (Vec<AccountMeta>), |
|
// but Anchor gives you AccountInfo. This code converts between the two formats. |
|
// remaining_accounts -> "all the other accounts Jupiter needs" (token mints, ATAs, market accounts, etc). |
|
let accounts: Vec<AccountMeta> = ctx |
|
.remaining_accounts |
|
.iter() |
|
.map(|acc| { |
|
AccountMeta { |
|
pubkey: *acc.key, // The account's address |
|
is_signer: acc.key == &ctx.accounts.caller.key(), // Is this account signing? |
|
is_writable: acc.is_writable, // Can this account be modified? |
|
} |
|
}) |
|
.collect(); |
|
|
|
// TODO: test with let account_infos: Vec<AccountInfo> = ctx.remaining_accounts.to_vec(); |
|
// or with invoke(&instruction, ctx.remaining_accounts)?; |
|
let account_infos: Vec<AccountInfo> = ctx |
|
.remaining_accounts |
|
.iter() |
|
.map(|acc| AccountInfo { ..acc.clone() }) |
|
.collect(); |
|
|
|
invoke( |
|
&Instruction { |
|
program_id: ctx.accounts.jupiter_program.key(), |
|
accounts, |
|
data: jupiter_swap_data, |
|
}, |
|
&account_infos, |
|
)?; |
|
|
|
msg!("Jupiter swap completed"); |
|
|
|
// === CALCULATE & VALIDATE TOKENS RECEIVED === |
|
ctx.accounts.authority_token_account.reload()?; |
|
let caller_token_balance_after = ctx.accounts.authority_token_account.amount; |
|
let tokens_received = caller_token_balance_after |
|
.checked_sub(caller_token_balance_before) |
|
.ok_or(VaultErrorCode::InvalidCalculation)?; |
|
|
|
require!( |
|
tokens_received >= min_amount_out, |
|
VaultErrorCode::InsufficientTokensReceived |
|
); |
|
msg!("Tokens received by authority: {}", tokens_received); |
|
|
|
// === TRANSFER TOKENS TO VAULT === |
|
if tokens_received > 0 { |
|
let transfer_cpi_accounts = anchor_spl::token::Transfer { |
|
from: ctx.accounts.authority_token_account.to_account_info(), |
|
to: ctx.accounts.vault_token_account.to_account_info(), |
|
authority: ctx.accounts.caller.to_account_info(), |
|
}; |
|
let transfer_cpi_program = ctx.accounts.output_token_program.to_account_info(); |
|
let transfer_cpi_ctx = CpiContext::new( |
|
transfer_cpi_program, |
|
transfer_cpi_accounts |
|
); |
|
|
|
anchor_spl::token::transfer(transfer_cpi_ctx, tokens_received)?; |
|
msg!("Transferred {} tokens from authority to vault", tokens_received); |
|
} |
|
|
|
// === CLEANUP WSOL: Handle any remaining WSOL === |
|
ctx.accounts.user_wsol_token_account.reload()?; |
|
let remaining_wsol = ctx.accounts.user_wsol_token_account.amount; |
|
|
|
if remaining_wsol > 0 { |
|
msg!("Closing WSOL account with {} remaining", remaining_wsol); |
|
|
|
let close_ix = spl_token::instruction::close_account( |
|
&spl_token::ID, |
|
&ctx.accounts.user_wsol_token_account.key(), |
|
&ctx.accounts.caller.key(), |
|
&ctx.accounts.caller.key(), |
|
&[], |
|
)?; |
|
|
|
invoke( |
|
&close_ix, |
|
&[ |
|
ctx.accounts.user_wsol_token_account.to_account_info(), |
|
ctx.accounts.caller.to_account_info(), |
|
ctx.accounts.caller.to_account_info(), |
|
], |
|
)?; |
|
|
|
msg!("WSOL account closed, SOL returned to caller"); |
|
} |
|
|
|
// === UPDATE VAULT STATE === |
|
vault.add_or_update_token_balance(target_token, tokens_received)?; |
|
|
|
emit!(CollectiveBuyCompleted { |
|
swap_amount: 1u64, // TODO: fix this |
|
target_token, |
|
tokens_received, |
|
effective_price: if tokens_received > 0 { |
|
(buy_amount as u128 * 1_000_000) / tokens_received as u128 |
|
} else { 0 }, |
|
}); |
|
|
|
Ok(()) |
|
} |
|
|
|
pub fn sell_tokens( |
|
ctx: Context<SellTokens>, |
|
token_mint: Pubkey, |
|
token_amount_to_sell: u64, |
|
jupiter_swap_data: Vec<u8>, |
|
min_sol_out: u64, |
|
) -> Result<()> { |
|
// === SETUP & VALIDATION === |
|
let vault = &mut ctx.accounts.vault; |
|
require_executor_role(&ctx.accounts.caller.key(), vault)?; |
|
|
|
require!(vault.stage == VaultStage::Buy, VaultErrorCode::WrongStage); |
|
|
|
// Check vault has enough tokens to sell |
|
let vault_token_balance = vault.get_token_balance(token_mint)?; |
|
require!( |
|
vault_token_balance >= token_amount_to_sell, |
|
VaultErrorCode::InsufficientTokenBalance |
|
); |
|
require!(token_amount_to_sell > 0, VaultErrorCode::InsufficientFunds); // TODO; change this error |
|
|
|
msg!("Collective sell: {} tokens of mint {}", token_amount_to_sell, token_mint); |
|
|
|
// === TRACK STARTING BALANCE === |
|
let authority_wsol_balance_before = ctx.accounts.authority_wsol_account.amount; |
|
msg!("Authority WSOL balance before: {}", authority_wsol_balance_before); |
|
|
|
// === PREPARE TOKENS FOR SWAP === |
|
// Transfer tokens from vault to authority for the swap |
|
let vault_seeds = &[VAULT_SEED, &[ctx.bumps.vault]]; |
|
let signer_seeds = &[&vault_seeds[..]]; |
|
|
|
let transfer_to_authority_accounts = anchor_spl::token::Transfer { |
|
from: ctx.accounts.vault_token_account.to_account_info(), |
|
to: ctx.accounts.authority_token_account.to_account_info(), |
|
authority: vault.to_account_info(), |
|
}; |
|
let transfer_to_authority_ctx = CpiContext::new_with_signer( |
|
ctx.accounts.input_token_program.to_account_info(), |
|
transfer_to_authority_accounts, |
|
signer_seeds, |
|
); |
|
|
|
anchor_spl::token::transfer(transfer_to_authority_ctx, token_amount_to_sell)?; |
|
msg!("Transferred {} tokens from vault to authority for swap", token_amount_to_sell); |
|
|
|
// === EXECUTE JUPITER SWAP === |
|
let accounts: Vec<AccountMeta> = ctx |
|
.remaining_accounts |
|
.iter() |
|
.map(|acc| { |
|
AccountMeta { |
|
pubkey: *acc.key, |
|
is_signer: acc.key == &ctx.accounts.caller.key(), |
|
is_writable: acc.is_writable, |
|
} |
|
}) |
|
.collect(); |
|
|
|
let account_infos: Vec<AccountInfo> = ctx |
|
.remaining_accounts |
|
.iter() |
|
.map(|acc| AccountInfo { ..acc.clone() }) |
|
.collect(); |
|
|
|
invoke( |
|
&Instruction { |
|
program_id: ctx.accounts.jupiter_program.key(), |
|
accounts, |
|
data: jupiter_swap_data, |
|
}, |
|
&account_infos, |
|
)?; |
|
|
|
msg!("Jupiter swap completed"); |
|
|
|
// === CALCULATE & VALIDATE SOL RECEIVED === |
|
ctx.accounts.authority_wsol_account.reload()?; |
|
let authority_wsol_balance_after = ctx.accounts.authority_wsol_account.amount; |
|
let wsol_received = authority_wsol_balance_after |
|
.checked_sub(authority_wsol_balance_before) |
|
.ok_or(VaultErrorCode::InvalidCalculation)?; |
|
|
|
require!( |
|
wsol_received >= min_sol_out, |
|
VaultErrorCode::InsufficientTokensReceived |
|
); |
|
msg!("WSOL received from swap: {}", wsol_received); |
|
|
|
// === CLEANUP WSOL: Convert back to SOL and send to vault === |
|
if wsol_received > 0 { |
|
let close_ix = spl_token::instruction::close_account( |
|
&spl_token::ID, |
|
&ctx.accounts.authority_wsol_account.key(), |
|
&vault.key(), // SOL goes to vault |
|
&ctx.accounts.caller.key(), |
|
&[], |
|
)?; |
|
|
|
invoke( |
|
&close_ix, |
|
&[ |
|
ctx.accounts.authority_wsol_account.to_account_info(), |
|
vault.to_account_info(), // SOL recipient is the vault |
|
ctx.accounts.caller.to_account_info(), |
|
], |
|
)?; |
|
|
|
msg!("WSOL account closed, {} SOL sent to vault", wsol_received); |
|
} |
|
|
|
// === UPDATE VAULT STATE === |
|
vault.subtract_token_balance(token_mint, token_amount_to_sell)?; |
|
|
|
emit!(CollectiveSellCompleted { |
|
token_mint, |
|
tokens_sold: token_amount_to_sell, |
|
sol_received: wsol_received, |
|
effective_price: if token_amount_to_sell > 0 { |
|
(wsol_received as u128 * 1_000_000) / token_amount_to_sell as u128 |
|
} else { 0 }, |
|
}); |
|
|
|
Ok(()) |
|
} |
|
|
|
pub fn emergency_withdraw_tokens( |
|
ctx: Context<EmergencyWithdrawTokens>, |
|
token_mint: Pubkey, |
|
) -> Result<()> { |
|
let vault = &ctx.accounts.vault; |
|
|
|
// Only vault authority can call this |
|
require_authority_role(&ctx.accounts.authority.key(), vault)?; |
|
|
|
let token_balance = ctx.accounts.vault_token_account.amount; |
|
|
|
if token_balance > 0 { |
|
// Create signer seeds for vault PDA |
|
let vault_seeds = &[VAULT_SEED, &[ctx.bumps.vault]]; |
|
let signer_seeds = &[&vault_seeds[..]]; |
|
|
|
// Transfer all tokens from vault to authority |
|
let cpi_accounts = anchor_spl::token::Transfer { |
|
from: ctx.accounts.vault_token_account.to_account_info(), |
|
to: ctx.accounts.authority_token_account.to_account_info(), |
|
authority: vault.to_account_info(), |
|
}; |
|
|
|
let cpi_ctx = CpiContext::new_with_signer( |
|
ctx.accounts.token_program.to_account_info(), |
|
cpi_accounts, |
|
signer_seeds, |
|
); |
|
|
|
anchor_spl::token::transfer(cpi_ctx, token_balance)?; |
|
|
|
msg!("🚨 Emergency: Withdrew {} tokens of mint {} to authority", |
|
token_balance, token_mint); |
|
} |
|
|
|
Ok(()) |
|
} |
|
|
|
pub fn emergency_withdraw_sol( |
|
ctx: Context<EmergencyWithdrawSol>, |
|
amount: u64, |
|
) -> Result<()> { |
|
let vault = &mut ctx.accounts.vault; |
|
|
|
require_authority_role(&ctx.accounts.authority.key(), vault)?; |
|
|
|
let vault_balance = vault.to_account_info().lamports(); |
|
let withdraw_amount = if amount == 0 { |
|
vault_balance.saturating_sub(5_000_000) // Leave 0.005 SOL for rent |
|
} else { |
|
amount |
|
}; |
|
|
|
require!(vault_balance >= withdraw_amount, VaultErrorCode::InsufficientFunds); |
|
|
|
// Transfer SOL from vault to authority |
|
**vault.to_account_info().try_borrow_mut_lamports()? -= withdraw_amount; |
|
**ctx.accounts.authority.to_account_info().try_borrow_mut_lamports()? += withdraw_amount; |
|
|
|
msg!("🚨 Emergency: Withdrew {} SOL from vault to authority", |
|
withdraw_amount as f64 / 1_000_000_000.0); |
|
|
|
Ok(()) |
|
} |
|
|
|
pub fn start_buy_stage(ctx: Context<StartBuyStage>) -> Result<()> { |
|
let vault = &mut ctx.accounts.vault; |
|
require_executor_role(&ctx.accounts.caller.key(), vault)?; |
|
require_stage(vault, VaultStage::Deposit)?; |
|
require!(vault.next_round_buy_amount > 0, VaultErrorCode::SetupNotCompleted); |
|
|
|
// ✅ Calculate fees here (only once when entering Buy stage) |
|
let total_investment = vault.next_round_buy_amount; |
|
let fee_amount = (total_investment * 2) / 100; |
|
let net_buy_amount = total_investment - fee_amount; // TODO: check this, the fee was already applied? |
|
|
|
// ✅ Track fees and update buy amount |
|
vault.add_fees(fee_amount)?; |
|
vault.set_current_spend(net_buy_amount); |
|
|
|
// ✅ Change stage |
|
vault.stage = VaultStage::Buy; |
|
|
|
emit!(SolFeesCollected { |
|
fee_amount, |
|
total_contribution: total_investment, |
|
net_buy_amount, |
|
fee_percentage: 2, |
|
round: vault.current_round, |
|
}); |
|
|
|
emit!(StageChanged { |
|
old_stage: VaultStage::Deposit, |
|
new_stage: VaultStage::Buy, |
|
round: vault.current_round, |
|
total_buy_amount: vault.next_round_buy_amount, |
|
}); |
|
|
|
Ok(()) |
|
} |
|
|
|
pub fn start_update_stage(ctx: Context<StartUpdateStage>) -> Result<()> { |
|
let vault = &mut ctx.accounts.vault; |
|
require_executor_role(&ctx.accounts.caller.key(), vault)?; |
|
require!(vault.stage == VaultStage::Buy, VaultErrorCode::WrongStage); |
|
|
|
vault.stage = VaultStage::Update; |
|
|
|
emit!(StageChanged { |
|
old_stage: VaultStage::Buy, |
|
new_stage: VaultStage::Update, |
|
round: vault.current_round, |
|
total_buy_amount: vault.next_round_buy_amount, |
|
}); |
|
|
|
Ok(()) |
|
} |
|
|
|
// === O(1) ALTERNATIVE: Just store round metadata === |
|
pub fn finalize_round_o1( |
|
ctx: Context<UpdateUserBalancesBatch>, |
|
token_mint: Pubkey, |
|
total_tokens_received: u64, |
|
) -> Result<()> { |
|
let vault = &mut ctx.accounts.vault; |
|
require_executor_role(&ctx.accounts.caller.key(), vault)?; |
|
require_stage(vault, VaultStage::Update)?; |
|
|
|
let total_sol_spent = vault.current_round_spend; |
|
require!(total_sol_spent > 0, VaultErrorCode::InsufficientFunds); |
|
|
|
// ✅ O(1): Just store the round snapshot - no user iteration |
|
vault.round_snapshots.push(RoundSnapshot { |
|
round: vault.current_round, |
|
total_sol_spent, |
|
total_tokens_received, |
|
token_mint, |
|
}); |
|
|
|
// Update vault's token holdings |
|
vault.add_or_update_token_balance(token_mint, total_tokens_received)?; |
|
|
|
emit!(RoundFinalized { |
|
round: vault.current_round, |
|
token_mint, |
|
total_sol_spent, |
|
total_tokens_received, |
|
}); |
|
|
|
Ok(()) |
|
} |
|
|
|
pub fn update_user_balances_batch( |
|
ctx: Context<UpdateUserBalancesBatch>, |
|
token_mint: Pubkey, |
|
total_tokens_received: u64, |
|
users: Vec<Pubkey>, |
|
) -> Result<()> { |
|
// === VALIDATION === |
|
let vault = &mut ctx.accounts.vault; |
|
require_executor_role(&ctx.accounts.caller.key(), vault)?; |
|
require_stage(vault, VaultStage::Update)?; |
|
require!(!users.is_empty(), VaultErrorCode::EmptyBatch); |
|
|
|
let total_sol_spent = vault.current_round_spend; |
|
require!(total_sol_spent > 0, VaultErrorCode::InsufficientFunds); |
|
|
|
// === PROCESS ALL USERS === |
|
for (account_index, user_pubkey) in users.iter().enumerate() { |
|
let user_account_info = &ctx.remaining_accounts[account_index]; |
|
|
|
modify_account::<UserAccount, _>(user_account_info, |user_account| { |
|
require!(user_account.user == *user_pubkey, VaultErrorCode::UserMismatch); |
|
|
|
let contribution = calculate_user_contribution(user_account); |
|
if contribution > 0 { |
|
|
|
let tokens = calculate_token_allocation(contribution, total_tokens_received, total_sol_spent); |
|
user_account.subtract_sol_balance(contribution)?; |
|
user_account.add_or_update_token_balance(token_mint, tokens)?; |
|
} |
|
|
|
// === NEXT ROUND: Calculate what user will contribute === |
|
// After the SOL deduction above, check if they can still participate next round |
|
let next_round_contribution = calculate_user_contribution(user_account); |
|
|
|
if next_round_contribution > 0 { |
|
// Add to next round's buy amount |
|
vault.add_to_next_round(next_round_contribution)?; |
|
} |
|
|
|
Ok(()) |
|
})?; |
|
} |
|
|
|
// === EMIT SINGLE BATCH EVENT === |
|
emit!(BatchProcessed { |
|
users_count: users.len() as u32, |
|
total_sol_spent, |
|
total_tokens_distributed: total_tokens_received, |
|
round: vault.current_round, |
|
}); |
|
|
|
// === EMIT NEXT ROUND PREPARATION EVENT === |
|
// emit!(NextRoundPrepared { |
|
// next_round: vault.current_round + 1, |
|
// estimated_buy_amount: vault.next_round_buy_amount, |
|
// users_processed: users.len() as u32, |
|
// }); |
|
|
|
Ok(()) |
|
} |
|
|
|
pub fn start_withdraw_stage(ctx: Context<StartWithdrawStage>) -> Result<()> { |
|
let vault = &mut ctx.accounts.vault; |
|
require_executor_role(&ctx.accounts.caller.key(), vault)?; |
|
require!(vault.stage == VaultStage::Update, VaultErrorCode::WrongStage); |
|
|
|
vault.stage = VaultStage::Withdraw; |
|
|
|
emit!(StageChanged { |
|
old_stage: VaultStage::Update, |
|
new_stage: VaultStage::Withdraw, |
|
round: vault.current_round, |
|
total_buy_amount: vault.next_round_buy_amount, |
|
}); |
|
|
|
Ok(()) |
|
} |
|
|
|
// TODO: deal with the reset of fees between rounds, in update balances function |
|
pub fn start_new_round(ctx: Context<StartNewRound>) -> Result<()> { |
|
let vault = &mut ctx.accounts.vault; |
|
require_executor_role(&ctx.accounts.caller.key(), vault)?; |
|
require!(vault.stage == VaultStage::Withdraw, VaultErrorCode::WrongStage); |
|
|
|
vault.stage = VaultStage::Deposit; // Back to Deposit for new round |
|
vault.current_round += 1; |
|
vault.accumulated_fees = 0; |
|
|
|
emit!(NewRoundStarted { |
|
round: vault.current_round, |
|
}); |
|
|
|
emit!(StageChanged { |
|
old_stage: VaultStage::Withdraw, |
|
new_stage: VaultStage::Deposit, |
|
round: vault.current_round, |
|
total_buy_amount: vault.next_round_buy_amount, |
|
}); |
|
|
|
Ok(()) |
|
} |
|
|
|
pub fn withdraw_sol( |
|
ctx: Context<WithdrawSol>, |
|
user: Pubkey, |
|
destination: Pubkey, // ← Add destination parameter |
|
amount: u64, // 0 means withdraw all |
|
) -> Result<()> { |
|
let vault = &mut ctx.accounts.vault; |
|
let user_account = &mut ctx.accounts.user_account; |
|
|
|
// === VALIDATION === |
|
require!(vault.stage == VaultStage::Withdraw, VaultErrorCode::WrongStage); |
|
require_executor_role(&ctx.accounts.caller.key(), vault)?; |
|
require!(user_account.user == user, VaultErrorCode::UserMismatch); |
|
|
|
// Calculate withdrawal amount |
|
let withdraw_amount = if amount == 0 { |
|
user_account.sol_balance // Withdraw all |
|
} else { |
|
amount |
|
}; |
|
|
|
require!(withdraw_amount > 0, VaultErrorCode::InsufficientFunds); |
|
require!(user_account.sol_balance >= withdraw_amount, VaultErrorCode::InsufficientBalance); |
|
|
|
// Check vault has enough SOL |
|
let vault_balance = vault.to_account_info().lamports(); |
|
require!(vault_balance >= withdraw_amount, VaultErrorCode::InsufficientVaultBalance); |
|
|
|
// Validate destination matches the account provided |
|
require!( |
|
destination == ctx.accounts.destination.key(), |
|
VaultErrorCode::InvalidDestination |
|
); |
|
|
|
// === UPDATE BALANCES === |
|
user_account.subtract_sol_balance(withdraw_amount)?; |
|
vault.deposits = vault.deposits |
|
.checked_sub(withdraw_amount) |
|
.ok_or(VaultErrorCode::MathOverflow)?; |
|
|
|
// === TRANSFER SOL === |
|
// Transfer SOL from vault to destination |
|
**vault.to_account_info().try_borrow_mut_lamports()? -= withdraw_amount; |
|
**ctx.accounts.destination.to_account_info().try_borrow_mut_lamports()? += withdraw_amount; |
|
|
|
emit!(WithdrawalCompleted { |
|
user, |
|
asset_type: "SOL".to_string(), |
|
amount: withdraw_amount, |
|
destination, |
|
new_user_sol_balance: user_account.sol_balance, |
|
remaining_vault_deposits: vault.deposits, |
|
}); |
|
|
|
Ok(()) |
|
} |
|
|
|
pub fn withdraw_tokens( |
|
ctx: Context<WithdrawTokens>, |
|
user: Pubkey, |
|
token_mint: Pubkey, |
|
destination_authority: Pubkey, // ← Add destination authority parameter |
|
amount: u64, // 0 means withdraw all |
|
) -> Result<()> { |
|
let vault = &ctx.accounts.vault; |
|
let user_account = &mut ctx.accounts.user_account; |
|
|
|
// === VALIDATION === |
|
require!(vault.stage == VaultStage::Withdraw, VaultErrorCode::WrongStage); |
|
require_executor_role(&ctx.accounts.caller.key(), vault)?; |
|
require!(user_account.user == user, VaultErrorCode::UserMismatch); |
|
|
|
// Validate destination authority matches the account provided |
|
require!( |
|
destination_authority == ctx.accounts.destination_authority.key(), |
|
VaultErrorCode::InvalidDestination |
|
); |
|
|
|
// ✅ LAZY EVALUATION: Update user balances only when needed |
|
user_account.apply_pending_updates(vault, vault.current_round)?; |
|
|
|
// Get user's token balance (now includes pending tokens) |
|
let user_token_balance = user_account.get_token_balance(token_mint)?; |
|
require!(user_token_balance > 0, VaultErrorCode::InsufficientTokenBalance); |
|
|
|
// Calculate withdrawal amount |
|
let withdraw_amount = if amount == 0 { |
|
user_token_balance // Withdraw all |
|
} else { |
|
amount |
|
}; |
|
|
|
require!(withdraw_amount > 0, VaultErrorCode::InsufficientFunds); |
|
require!(user_token_balance >= withdraw_amount, VaultErrorCode::InsufficientTokenBalance); |
|
|
|
// Check vault has enough tokens |
|
let vault_token_balance = ctx.accounts.vault_token_account.amount; |
|
require!(vault_token_balance >= withdraw_amount, VaultErrorCode::InsufficientTokenBalance); |
|
|
|
// === UPDATE USER BALANCE === |
|
user_account.subtract_token_balance(token_mint, withdraw_amount)?; |
|
|
|
// === TRANSFER TOKENS === |
|
// Create signer seeds for vault PDA |
|
let vault_seeds = &[VAULT_SEED, &[ctx.bumps.vault]]; |
|
let signer_seeds = &[&vault_seeds[..]]; |
|
|
|
let transfer_accounts = anchor_spl::token::Transfer { |
|
from: ctx.accounts.vault_token_account.to_account_info(), |
|
to: ctx.accounts.destination_token_account.to_account_info(), |
|
authority: vault.to_account_info(), |
|
}; |
|
|
|
let transfer_ctx = CpiContext::new_with_signer( |
|
ctx.accounts.token_program.to_account_info(), |
|
transfer_accounts, |
|
signer_seeds, |
|
); |
|
|
|
anchor_spl::token::transfer(transfer_ctx, withdraw_amount)?; |
|
|
|
emit!(WithdrawalCompleted { |
|
user, |
|
asset_type: format!("TOKEN:{}", token_mint), |
|
amount: withdraw_amount, |
|
destination: ctx.accounts.destination_token_account.key(), |
|
new_user_sol_balance: user_account.sol_balance, // SOL balance unchanged |
|
remaining_vault_deposits: vault.deposits, // Deposits unchanged for token withdrawal |
|
}); |
|
|
|
Ok(()) |
|
} |
|
|
|
pub fn withdraw_sol_fees( |
|
ctx: Context<WithdrawSolFees>, |
|
amount: u64, // 0 means withdraw all fees |
|
) -> Result<()> { |
|
let vault = &mut ctx.accounts.vault; |
|
|
|
// Only authority can withdraw fees |
|
require_authority_role(&ctx.accounts.authority.key(), vault)?; |
|
|
|
let withdraw_amount = if amount == 0 { |
|
vault.accumulated_fees // Withdraw all fees |
|
} else { |
|
amount |
|
}; |
|
|
|
require!(withdraw_amount > 0, VaultErrorCode::InsufficientFunds); |
|
require!(vault.accumulated_fees >= withdraw_amount, VaultErrorCode::InsufficientFunds); |
|
|
|
// Check vault has enough SOL |
|
let vault_balance = vault.to_account_info().lamports(); |
|
require!(vault_balance >= withdraw_amount, VaultErrorCode::InsufficientVaultBalance); |
|
|
|
// Update fee tracking |
|
vault.subtract_fees(withdraw_amount)?; |
|
|
|
// Transfer SOL from vault to authority |
|
**vault.to_account_info().try_borrow_mut_lamports()? -= withdraw_amount; |
|
**ctx.accounts.authority.to_account_info().try_borrow_mut_lamports()? += withdraw_amount; |
|
|
|
emit!(SolFeesWithdrawn { |
|
amount: withdraw_amount, |
|
remaining_fee_balance: vault.accumulated_fees, |
|
authority: ctx.accounts.authority.key(), |
|
}); |
|
|
|
Ok(()) |
|
} |
|
|
|
} |
|
|
|
// Only need these two helper functions: |
|
fn calculate_user_contribution(user_account: &UserAccount) -> u64 { |
|
if user_account.sol_balance >= user_account.investment_per_token && user_account.is_active { |
|
user_account.investment_per_token |
|
} else { |
|
0 |
|
} |
|
} |
|
|
|
fn calculate_token_allocation(user_sol_contribution: u64, total_tokens_received: u64, total_sol_spent: u64) -> u64 { |
|
if user_sol_contribution == 0 { |
|
return 0; |
|
} |
|
|
|
// Calculate user's net contribution (after 2% fee) |
|
let user_fee = (user_sol_contribution * 2) / 100; |
|
let user_net_contribution = user_sol_contribution - user_fee; |
|
|
|
// Calculate proportional share based on net contributions |
|
// Formula: (user_net_contribution / total_net_spent) * total_tokens |
|
(user_net_contribution as u128 * total_tokens_received as u128 / total_sol_spent as u128) as u64 |
|
} |