use anchor_lang::prelude::*; use anchor_lang::solana_program::{ instruction::{AccountMeta, Instruction}, program::invoke_signed, }; use anchor_spl::token_interface::{ self, Mint, TokenAccount, TokenInterface, TransferChecked, }; declare_id!("AynRWbxRpAix5Yt8QcL882kakPyR1dF9W5koQJNBujEt"); pub const MAX_ALLOCATIONS: usize = 10; pub const BPS_DENOMINATOR: u16 = 10_000; pub const TREASURY_SEED: &[u8] = b"black-hole-treasury"; pub const HOLE_VAULT_SEED: &[u8] = b"hole-vault"; pub const STONK_VAULT_SEED: &[u8] = b"stonk-vault"; pub const POSITION_SEED: &[u8] = b"ecosystem-position"; pub const ECOSYSTEM_VAULT_SEED: &[u8] = b"ecosystem-vault"; pub const REWARD_VAULT_SEED: &[u8] = b"reward-vault"; #[program] pub mod the_black_hole { use super::*; pub fn initialize_treasury( ctx: Context, args: InitializeTreasuryArgs, ) -> Result<()> { validate_fee_bps(args.trade_fee_bps)?; validate_fee_bps(args.initial_locked_bps)?; validate_allocations( args.allocation_count, &args.allocation_mints, &args.allocation_weights_bps, )?; let treasury = &mut ctx.accounts.treasury; treasury.authority = ctx.accounts.authority.key(); treasury.keeper_authority = args.keeper_authority; treasury.hole_mint = ctx.accounts.hole_mint.key(); treasury.stonk_mint = ctx.accounts.stonk_mint.key(); treasury.hole_vault = ctx.accounts.hole_vault.key(); treasury.stonk_vault = ctx.accounts.stonk_vault.key(); treasury.allowed_swap_program = args.allowed_swap_program; treasury.bump = ctx.bumps.treasury; treasury.hole_vault_bump = ctx.bumps.hole_vault; treasury.stonk_vault_bump = ctx.bumps.stonk_vault; treasury.trade_fee_bps = args.trade_fee_bps; treasury.initial_locked_bps = args.initial_locked_bps; treasury.treasury_reward_share_bps = args.initial_locked_bps; treasury.allocation_count = args.allocation_count; treasury.allocation_mints = args.allocation_mints; treasury.allocation_weights_bps = args.allocation_weights_bps; treasury.total_locked = 0; treasury.total_hole_bought = 0; treasury.total_hole_absorbed = 0; treasury.total_stonk_received = 0; treasury.last_stonk_balance = 0; treasury.total_ecosystem_deployed = 0; treasury.total_hole_buyback_spent = 0; treasury.cycle = 0; treasury.paused = false; emit!(TreasuryInitialized { authority: treasury.authority, keeper_authority: treasury.keeper_authority, hole_mint: treasury.hole_mint, stonk_mint: treasury.stonk_mint, hole_vault: treasury.hole_vault, stonk_vault: treasury.stonk_vault, trade_fee_bps: treasury.trade_fee_bps, initial_locked_bps: treasury.initial_locked_bps, }); Ok(()) } pub fn set_keeper(ctx: Context, keeper_authority: Pubkey) -> Result<()> { ctx.accounts.treasury.keeper_authority = keeper_authority; emit!(KeeperUpdated { keeper_authority }); Ok(()) } pub fn set_allowed_swap_program( ctx: Context, allowed_swap_program: Pubkey, ) -> Result<()> { ctx.accounts.treasury.allowed_swap_program = allowed_swap_program; emit!(SwapProgramUpdated { allowed_swap_program, }); Ok(()) } pub fn set_pause(ctx: Context, paused: bool) -> Result<()> { ctx.accounts.treasury.paused = paused; emit!(PauseUpdated { paused }); Ok(()) } pub fn set_allocations( ctx: Context, allocation_count: u8, allocation_mints: [Pubkey; MAX_ALLOCATIONS], allocation_weights_bps: [u16; MAX_ALLOCATIONS], ) -> Result<()> { validate_allocations(allocation_count, &allocation_mints, &allocation_weights_bps)?; let treasury = &mut ctx.accounts.treasury; treasury.allocation_count = allocation_count; treasury.allocation_mints = allocation_mints; treasury.allocation_weights_bps = allocation_weights_bps; emit!(AllocationsUpdated { allocation_count, allocation_mints, allocation_weights_bps, }); Ok(()) } pub fn create_ecosystem_position( ctx: Context, index: u8, ) -> Result<()> { let treasury = &ctx.accounts.treasury; require!(!treasury.paused, BlackHoleError::Paused); require!( (index as usize) < treasury.allocation_count as usize, BlackHoleError::InvalidAllocationIndex ); require_keys_eq!( treasury.allocation_mints[index as usize], ctx.accounts.ecosystem_mint.key(), BlackHoleError::InvalidMint ); let position = &mut ctx.accounts.position; position.treasury = treasury.key(); position.index = index; position.ecosystem_mint = ctx.accounts.ecosystem_mint.key(); position.ecosystem_vault = ctx.accounts.ecosystem_vault.key(); position.reward_mint = ctx.accounts.reward_mint.key(); position.reward_vault = ctx.accounts.reward_vault.key(); position.bump = ctx.bumps.position; position.ecosystem_vault_bump = ctx.bumps.ecosystem_vault; position.reward_vault_bump = ctx.bumps.reward_vault; position.total_ecosystem_tokens = 0; position.last_ecosystem_balance = 0; position.total_reward_received = 0; position.last_reward_balance = 0; position.active = true; emit!(EcosystemPositionCreated { index, ecosystem_mint: position.ecosystem_mint, ecosystem_vault: position.ecosystem_vault, reward_mint: position.reward_mint, reward_vault: position.reward_vault, }); Ok(()) } pub fn lock_hole(ctx: Context, amount: u64) -> Result<()> { require!(!ctx.accounts.treasury.paused, BlackHoleError::Paused); require!(amount > 0, BlackHoleError::ZeroAmount); transfer_checked( ctx.accounts.source_hole_account.to_account_info(), ctx.accounts.hole_vault.to_account_info(), ctx.accounts.holder.to_account_info(), ctx.accounts.hole_mint.to_account_info(), ctx.accounts.hole_token_program.to_account_info(), amount, ctx.accounts.hole_mint.decimals, None, )?; let treasury = &mut ctx.accounts.treasury; treasury.total_locked = checked_add(treasury.total_locked, amount)?; treasury.total_hole_absorbed = checked_add(treasury.total_hole_absorbed, amount)?; treasury.treasury_reward_share_bps = share_bps(ctx.accounts.hole_vault.amount, ctx.accounts.hole_mint.supply); emit!(HoleLocked { source: ctx.accounts.holder.key(), amount, total_locked: treasury.total_locked, treasury_reward_share_bps: treasury.treasury_reward_share_bps, }); Ok(()) } pub fn absorb_bought_hole(ctx: Context, amount: u64) -> Result<()> { require!(!ctx.accounts.treasury.paused, BlackHoleError::Paused); require_keeper(&ctx.accounts.treasury, &ctx.accounts.keeper)?; require!(amount > 0, BlackHoleError::ZeroAmount); transfer_checked( ctx.accounts.source_hole_account.to_account_info(), ctx.accounts.hole_vault.to_account_info(), ctx.accounts.keeper.to_account_info(), ctx.accounts.hole_mint.to_account_info(), ctx.accounts.hole_token_program.to_account_info(), amount, ctx.accounts.hole_mint.decimals, None, )?; let treasury = &mut ctx.accounts.treasury; treasury.total_hole_bought = checked_add(treasury.total_hole_bought, amount)?; treasury.total_hole_absorbed = checked_add(treasury.total_hole_absorbed, amount)?; treasury.treasury_reward_share_bps = share_bps(ctx.accounts.hole_vault.amount, ctx.accounts.hole_mint.supply); emit!(BoughtHoleAbsorbed { keeper: ctx.accounts.keeper.key(), amount, total_hole_bought: treasury.total_hole_bought, treasury_reward_share_bps: treasury.treasury_reward_share_bps, }); Ok(()) } pub fn sync_stonk_rewards(ctx: Context) -> Result<()> { let treasury = &mut ctx.accounts.treasury; let current_balance = ctx.accounts.stonk_vault.amount; require!( current_balance >= treasury.last_stonk_balance, BlackHoleError::TrackedBalanceDecreased ); let delta = current_balance - treasury.last_stonk_balance; treasury.total_stonk_received = checked_add(treasury.total_stonk_received, delta)?; treasury.last_stonk_balance = current_balance; emit!(StonkRewardsSynced { amount: delta, stonk_vault_balance: current_balance, total_stonk_received: treasury.total_stonk_received, }); Ok(()) } pub fn sync_ecosystem_position(ctx: Context) -> Result<()> { require!(ctx.accounts.position.active, BlackHoleError::InactivePosition); let position = &mut ctx.accounts.position; let ecosystem_balance = ctx.accounts.ecosystem_vault.amount; let reward_balance = ctx.accounts.reward_vault.amount; require!( ecosystem_balance >= position.last_ecosystem_balance, BlackHoleError::TrackedBalanceDecreased ); require!( reward_balance >= position.last_reward_balance, BlackHoleError::TrackedBalanceDecreased ); let ecosystem_delta = ecosystem_balance - position.last_ecosystem_balance; let reward_delta = reward_balance - position.last_reward_balance; position.total_ecosystem_tokens = checked_add(position.total_ecosystem_tokens, ecosystem_delta)?; position.total_reward_received = checked_add(position.total_reward_received, reward_delta)?; position.last_ecosystem_balance = ecosystem_balance; position.last_reward_balance = reward_balance; emit!(EcosystemPositionSynced { index: position.index, ecosystem_delta, reward_delta, ecosystem_balance, reward_balance, }); Ok(()) } pub fn execute_ecosystem_purchase( ctx: Context, args: SwapExecutionArgs, ) -> Result<()> { require!(!ctx.accounts.treasury.paused, BlackHoleError::Paused); require_keeper(&ctx.accounts.treasury, &ctx.accounts.keeper)?; require!(ctx.accounts.position.active, BlackHoleError::InactivePosition); require!( ctx.accounts.swap_program.key() == ctx.accounts.treasury.allowed_swap_program, BlackHoleError::InvalidSwapProgram ); require!(args.max_source_spend > 0, BlackHoleError::ZeroAmount); require!(args.min_destination_receive > 0, BlackHoleError::ZeroAmount); let stonk_before = ctx.accounts.stonk_vault.amount; let ecosystem_before = ctx.accounts.ecosystem_vault.amount; let hole_before = ctx.accounts.hole_vault.amount; invoke_signed_swap( ctx.accounts.swap_program.key(), &ctx.remaining_accounts, args.authority_remaining_index, ctx.accounts.treasury.key(), ctx.accounts.treasury.bump, args.swap_instruction_data, )?; ctx.accounts.stonk_vault.reload()?; ctx.accounts.ecosystem_vault.reload()?; ctx.accounts.hole_vault.reload()?; require!( ctx.accounts.hole_vault.amount >= hole_before, BlackHoleError::HoleVaultCannotDecrease ); let stonk_spent = checked_sub(stonk_before, ctx.accounts.stonk_vault.amount)?; let ecosystem_received = checked_sub(ctx.accounts.ecosystem_vault.amount, ecosystem_before)?; require!( stonk_spent <= args.max_source_spend, BlackHoleError::MaxSpendExceeded ); require!( ecosystem_received >= args.min_destination_receive, BlackHoleError::MinimumReceiveNotMet ); let treasury = &mut ctx.accounts.treasury; let position = &mut ctx.accounts.position; treasury.last_stonk_balance = ctx.accounts.stonk_vault.amount; treasury.total_ecosystem_deployed = checked_add(treasury.total_ecosystem_deployed, stonk_spent)?; treasury.cycle = checked_add(treasury.cycle, 1)?; position.total_ecosystem_tokens = checked_add(position.total_ecosystem_tokens, ecosystem_received)?; position.last_ecosystem_balance = ctx.accounts.ecosystem_vault.amount; emit!(EcosystemPurchaseExecuted { keeper: ctx.accounts.keeper.key(), index: position.index, stonk_spent, ecosystem_received, cycle: treasury.cycle, }); Ok(()) } pub fn execute_hole_buyback( ctx: Context, args: SwapExecutionArgs, ) -> Result<()> { require!(!ctx.accounts.treasury.paused, BlackHoleError::Paused); require_keeper(&ctx.accounts.treasury, &ctx.accounts.keeper)?; require!(ctx.accounts.position.active, BlackHoleError::InactivePosition); require!( ctx.accounts.swap_program.key() == ctx.accounts.treasury.allowed_swap_program, BlackHoleError::InvalidSwapProgram ); require!(args.max_source_spend > 0, BlackHoleError::ZeroAmount); require!(args.min_destination_receive > 0, BlackHoleError::ZeroAmount); let reward_before = ctx.accounts.reward_vault.amount; let hole_before = ctx.accounts.hole_vault.amount; invoke_signed_swap( ctx.accounts.swap_program.key(), &ctx.remaining_accounts, args.authority_remaining_index, ctx.accounts.treasury.key(), ctx.accounts.treasury.bump, args.swap_instruction_data, )?; ctx.accounts.reward_vault.reload()?; ctx.accounts.hole_vault.reload()?; let reward_spent = checked_sub(reward_before, ctx.accounts.reward_vault.amount)?; let hole_received = checked_sub(ctx.accounts.hole_vault.amount, hole_before)?; require!( reward_spent <= args.max_source_spend, BlackHoleError::MaxSpendExceeded ); require!( hole_received >= args.min_destination_receive, BlackHoleError::MinimumReceiveNotMet ); let treasury = &mut ctx.accounts.treasury; let position = &mut ctx.accounts.position; treasury.total_hole_buyback_spent = checked_add(treasury.total_hole_buyback_spent, reward_spent)?; treasury.total_hole_bought = checked_add(treasury.total_hole_bought, hole_received)?; treasury.total_hole_absorbed = checked_add(treasury.total_hole_absorbed, hole_received)?; treasury.treasury_reward_share_bps = share_bps(ctx.accounts.hole_vault.amount, ctx.accounts.hole_mint.supply); treasury.cycle = checked_add(treasury.cycle, 1)?; position.last_reward_balance = ctx.accounts.reward_vault.amount; emit!(HoleBuybackExecuted { keeper: ctx.accounts.keeper.key(), index: position.index, reward_spent, hole_received, treasury_reward_share_bps: treasury.treasury_reward_share_bps, cycle: treasury.cycle, }); Ok(()) } } #[derive(AnchorSerialize, AnchorDeserialize, Clone)] pub struct InitializeTreasuryArgs { pub keeper_authority: Pubkey, pub allowed_swap_program: Pubkey, pub trade_fee_bps: u16, pub initial_locked_bps: u16, pub allocation_count: u8, pub allocation_mints: [Pubkey; MAX_ALLOCATIONS], pub allocation_weights_bps: [u16; MAX_ALLOCATIONS], } #[derive(AnchorSerialize, AnchorDeserialize, Clone)] pub struct SwapExecutionArgs { pub max_source_spend: u64, pub min_destination_receive: u64, pub authority_remaining_index: u8, pub swap_instruction_data: Vec, } #[derive(Accounts)] pub struct InitializeTreasury<'info> { #[account(mut)] pub authority: Signer<'info>, pub hole_mint: Box>, pub stonk_mint: Box>, #[account( init, payer = authority, space = 8 + BlackHoleTreasury::LEN, seeds = [TREASURY_SEED], bump )] pub treasury: Box>, #[account( init, payer = authority, token::mint = hole_mint, token::authority = treasury, token::token_program = hole_token_program, seeds = [HOLE_VAULT_SEED, hole_mint.key().as_ref()], bump )] pub hole_vault: Box>, #[account( init, payer = authority, token::mint = stonk_mint, token::authority = treasury, token::token_program = stonk_token_program, seeds = [STONK_VAULT_SEED, stonk_mint.key().as_ref()], bump )] pub stonk_vault: Box>, pub hole_token_program: Interface<'info, TokenInterface>, pub stonk_token_program: Interface<'info, TokenInterface>, pub system_program: Program<'info, System>, } #[derive(Accounts)] pub struct AdminTreasury<'info> { #[account(mut, address = treasury.authority @ BlackHoleError::Unauthorized)] pub authority: Signer<'info>, #[account(mut, seeds = [TREASURY_SEED], bump = treasury.bump)] pub treasury: Box>, } #[derive(Accounts)] #[instruction(index: u8)] pub struct CreateEcosystemPosition<'info> { #[account(mut, address = treasury.authority @ BlackHoleError::Unauthorized)] pub authority: Signer<'info>, #[account(mut, seeds = [TREASURY_SEED], bump = treasury.bump)] pub treasury: Box>, pub ecosystem_mint: Box>, pub reward_mint: Box>, #[account( init, payer = authority, space = 8 + EcosystemPosition::LEN, seeds = [POSITION_SEED, treasury.key().as_ref(), &[index]], bump )] pub position: Box>, #[account( init, payer = authority, token::mint = ecosystem_mint, token::authority = treasury, token::token_program = ecosystem_token_program, seeds = [ECOSYSTEM_VAULT_SEED, treasury.key().as_ref(), &[index]], bump )] pub ecosystem_vault: Box>, #[account( init, payer = authority, token::mint = reward_mint, token::authority = treasury, token::token_program = reward_token_program, seeds = [REWARD_VAULT_SEED, treasury.key().as_ref(), &[index]], bump )] pub reward_vault: Box>, pub ecosystem_token_program: Interface<'info, TokenInterface>, pub reward_token_program: Interface<'info, TokenInterface>, pub system_program: Program<'info, System>, } #[derive(Accounts)] pub struct LockHole<'info> { #[account(mut)] pub holder: Signer<'info>, pub hole_mint: Box>, #[account( mut, constraint = source_hole_account.mint == treasury.hole_mint @ BlackHoleError::InvalidMint, constraint = source_hole_account.owner == holder.key() @ BlackHoleError::InvalidOwner )] pub source_hole_account: Box>, #[account(mut, seeds = [TREASURY_SEED], bump = treasury.bump)] pub treasury: Box>, #[account( mut, address = treasury.hole_vault @ BlackHoleError::InvalidVault, constraint = hole_vault.mint == treasury.hole_mint @ BlackHoleError::InvalidMint )] pub hole_vault: Box>, pub hole_token_program: Interface<'info, TokenInterface>, } #[derive(Accounts)] pub struct AbsorbBoughtHole<'info> { #[account(mut)] pub keeper: Signer<'info>, pub hole_mint: Box>, #[account( mut, constraint = source_hole_account.mint == treasury.hole_mint @ BlackHoleError::InvalidMint, constraint = source_hole_account.owner == keeper.key() @ BlackHoleError::InvalidOwner )] pub source_hole_account: Box>, #[account(mut, seeds = [TREASURY_SEED], bump = treasury.bump)] pub treasury: Box>, #[account( mut, address = treasury.hole_vault @ BlackHoleError::InvalidVault, constraint = hole_vault.mint == treasury.hole_mint @ BlackHoleError::InvalidMint )] pub hole_vault: Box>, pub hole_token_program: Interface<'info, TokenInterface>, } #[derive(Accounts)] pub struct SyncStonkRewards<'info> { #[account(mut, seeds = [TREASURY_SEED], bump = treasury.bump)] pub treasury: Box>, #[account( address = treasury.stonk_vault @ BlackHoleError::InvalidVault, constraint = stonk_vault.mint == treasury.stonk_mint @ BlackHoleError::InvalidMint )] pub stonk_vault: Box>, } #[derive(Accounts)] pub struct SyncEcosystemPosition<'info> { #[account(seeds = [TREASURY_SEED], bump = treasury.bump)] pub treasury: Box>, #[account( mut, seeds = [POSITION_SEED, treasury.key().as_ref(), &[position.index]], bump = position.bump, constraint = position.treasury == treasury.key() @ BlackHoleError::InvalidTreasury )] pub position: Box>, #[account( address = position.ecosystem_vault @ BlackHoleError::InvalidVault, constraint = ecosystem_vault.mint == position.ecosystem_mint @ BlackHoleError::InvalidMint )] pub ecosystem_vault: Box>, #[account( address = position.reward_vault @ BlackHoleError::InvalidVault, constraint = reward_vault.mint == position.reward_mint @ BlackHoleError::InvalidMint )] pub reward_vault: Box>, } #[derive(Accounts)] pub struct ExecuteEcosystemPurchase<'info> { #[account(mut)] pub keeper: Signer<'info>, #[account(mut, seeds = [TREASURY_SEED], bump = treasury.bump)] pub treasury: Box>, #[account( mut, address = treasury.stonk_vault @ BlackHoleError::InvalidVault, constraint = stonk_vault.mint == treasury.stonk_mint @ BlackHoleError::InvalidMint )] pub stonk_vault: Box>, #[account( address = treasury.hole_vault @ BlackHoleError::InvalidVault, constraint = hole_vault.mint == treasury.hole_mint @ BlackHoleError::InvalidMint )] pub hole_vault: Box>, #[account( mut, seeds = [POSITION_SEED, treasury.key().as_ref(), &[position.index]], bump = position.bump, constraint = position.treasury == treasury.key() @ BlackHoleError::InvalidTreasury )] pub position: Box>, #[account( mut, address = position.ecosystem_vault @ BlackHoleError::InvalidVault, constraint = ecosystem_vault.mint == position.ecosystem_mint @ BlackHoleError::InvalidMint )] pub ecosystem_vault: Box>, /// CHECK: Constrained by the treasury allowlist and used only as the CPI program id. pub swap_program: UncheckedAccount<'info>, } #[derive(Accounts)] pub struct ExecuteHoleBuyback<'info> { #[account(mut)] pub keeper: Signer<'info>, pub hole_mint: Box>, #[account(mut, seeds = [TREASURY_SEED], bump = treasury.bump)] pub treasury: Box>, #[account( mut, address = treasury.hole_vault @ BlackHoleError::InvalidVault, constraint = hole_vault.mint == treasury.hole_mint @ BlackHoleError::InvalidMint )] pub hole_vault: Box>, #[account( mut, seeds = [POSITION_SEED, treasury.key().as_ref(), &[position.index]], bump = position.bump, constraint = position.treasury == treasury.key() @ BlackHoleError::InvalidTreasury )] pub position: Box>, #[account( mut, address = position.reward_vault @ BlackHoleError::InvalidVault, constraint = reward_vault.mint == position.reward_mint @ BlackHoleError::InvalidMint )] pub reward_vault: Box>, /// CHECK: Constrained by the treasury allowlist and used only as the CPI program id. pub swap_program: UncheckedAccount<'info>, } #[account] pub struct BlackHoleTreasury { pub authority: Pubkey, pub keeper_authority: Pubkey, pub hole_mint: Pubkey, pub stonk_mint: Pubkey, pub hole_vault: Pubkey, pub stonk_vault: Pubkey, pub allowed_swap_program: Pubkey, pub bump: u8, pub hole_vault_bump: u8, pub stonk_vault_bump: u8, pub trade_fee_bps: u16, pub initial_locked_bps: u16, pub treasury_reward_share_bps: u16, pub allocation_count: u8, pub allocation_mints: [Pubkey; MAX_ALLOCATIONS], pub allocation_weights_bps: [u16; MAX_ALLOCATIONS], pub total_locked: u64, pub total_hole_bought: u64, pub total_hole_absorbed: u64, pub total_stonk_received: u64, pub last_stonk_balance: u64, pub total_ecosystem_deployed: u64, pub total_hole_buyback_spent: u64, pub cycle: u64, pub paused: bool, } impl BlackHoleTreasury { pub const LEN: usize = 32 * 7 + 3 + 2 * 3 + 1 + 32 * MAX_ALLOCATIONS + 2 * MAX_ALLOCATIONS + 8 * 8 + 1; } #[account] pub struct EcosystemPosition { pub treasury: Pubkey, pub index: u8, pub ecosystem_mint: Pubkey, pub ecosystem_vault: Pubkey, pub reward_mint: Pubkey, pub reward_vault: Pubkey, pub bump: u8, pub ecosystem_vault_bump: u8, pub reward_vault_bump: u8, pub total_ecosystem_tokens: u64, pub last_ecosystem_balance: u64, pub total_reward_received: u64, pub last_reward_balance: u64, pub active: bool, } impl EcosystemPosition { pub const LEN: usize = 32 + 1 + 32 * 4 + 3 + 8 * 4 + 1; } #[event] pub struct TreasuryInitialized { pub authority: Pubkey, pub keeper_authority: Pubkey, pub hole_mint: Pubkey, pub stonk_mint: Pubkey, pub hole_vault: Pubkey, pub stonk_vault: Pubkey, pub trade_fee_bps: u16, pub initial_locked_bps: u16, } #[event] pub struct KeeperUpdated { pub keeper_authority: Pubkey, } #[event] pub struct SwapProgramUpdated { pub allowed_swap_program: Pubkey, } #[event] pub struct PauseUpdated { pub paused: bool, } #[event] pub struct AllocationsUpdated { pub allocation_count: u8, pub allocation_mints: [Pubkey; MAX_ALLOCATIONS], pub allocation_weights_bps: [u16; MAX_ALLOCATIONS], } #[event] pub struct EcosystemPositionCreated { pub index: u8, pub ecosystem_mint: Pubkey, pub ecosystem_vault: Pubkey, pub reward_mint: Pubkey, pub reward_vault: Pubkey, } #[event] pub struct HoleLocked { pub source: Pubkey, pub amount: u64, pub total_locked: u64, pub treasury_reward_share_bps: u16, } #[event] pub struct BoughtHoleAbsorbed { pub keeper: Pubkey, pub amount: u64, pub total_hole_bought: u64, pub treasury_reward_share_bps: u16, } #[event] pub struct StonkRewardsSynced { pub amount: u64, pub stonk_vault_balance: u64, pub total_stonk_received: u64, } #[event] pub struct EcosystemPositionSynced { pub index: u8, pub ecosystem_delta: u64, pub reward_delta: u64, pub ecosystem_balance: u64, pub reward_balance: u64, } #[event] pub struct EcosystemPurchaseExecuted { pub keeper: Pubkey, pub index: u8, pub stonk_spent: u64, pub ecosystem_received: u64, pub cycle: u64, } #[event] pub struct HoleBuybackExecuted { pub keeper: Pubkey, pub index: u8, pub reward_spent: u64, pub hole_received: u64, pub treasury_reward_share_bps: u16, pub cycle: u64, } #[error_code] pub enum BlackHoleError { #[msg("Amount must be greater than zero.")] ZeroAmount, #[msg("Math overflow.")] MathOverflow, #[msg("Math underflow.")] MathUnderflow, #[msg("The treasury is paused.")] Paused, #[msg("Signer is not authorized.")] Unauthorized, #[msg("Signer is not the keeper authority.")] InvalidKeeper, #[msg("Basis points value is invalid.")] InvalidBasisPoints, #[msg("Allocation count is invalid.")] InvalidAllocationCount, #[msg("Allocation weights must sum to 10,000 bps.")] InvalidAllocationWeights, #[msg("Allocation mint is invalid.")] InvalidAllocationMint, #[msg("Allocation index is invalid.")] InvalidAllocationIndex, #[msg("Token account mint does not match the configured mint.")] InvalidMint, #[msg("Token account is not owned by the signer.")] InvalidOwner, #[msg("Vault does not match the configured Black Hole vault.")] InvalidVault, #[msg("Treasury account does not match the position.")] InvalidTreasury, #[msg("The ecosystem position is inactive.")] InactivePosition, #[msg("Tracked token balance decreased outside an execution instruction.")] TrackedBalanceDecreased, #[msg("Swap program is not the treasury allowlisted program.")] InvalidSwapProgram, #[msg("The PDA authority was not found in remaining accounts.")] MissingSwapAuthority, #[msg("Swap spent more than the allowed maximum.")] MaxSpendExceeded, #[msg("Swap received less than the required minimum.")] MinimumReceiveNotMet, #[msg("The HOLE vault cannot decrease during this instruction.")] HoleVaultCannotDecrease, } fn transfer_checked<'info>( from: AccountInfo<'info>, to: AccountInfo<'info>, authority: AccountInfo<'info>, mint: AccountInfo<'info>, token_program: AccountInfo<'info>, amount: u64, decimals: u8, signer_seeds: Option<&[&[&[u8]]]>, ) -> Result<()> { let accounts = TransferChecked { from, mint, to, authority, }; let cpi_ctx = match signer_seeds { Some(seeds) => CpiContext::new_with_signer(token_program, accounts, seeds), None => CpiContext::new(token_program, accounts), }; token_interface::transfer_checked(cpi_ctx, amount, decimals) } fn invoke_signed_swap<'info>( program_id: Pubkey, remaining_accounts: &[AccountInfo<'info>], authority_remaining_index: u8, treasury_key: Pubkey, treasury_bump: u8, instruction_data: Vec, ) -> Result<()> { let authority_index = authority_remaining_index as usize; require!( authority_index < remaining_accounts.len() && remaining_accounts[authority_index].key() == treasury_key, BlackHoleError::MissingSwapAuthority ); let metas = remaining_accounts .iter() .enumerate() .map(|(index, account)| { let is_signer = account.is_signer || index == authority_index; if account.is_writable { AccountMeta::new(account.key(), is_signer) } else { AccountMeta::new_readonly(account.key(), is_signer) } }) .collect::>(); let instruction = Instruction { program_id, accounts: metas, data: instruction_data, }; let signer_seeds: &[&[u8]] = &[TREASURY_SEED, &[treasury_bump]]; invoke_signed(&instruction, remaining_accounts, &[signer_seeds])?; Ok(()) } fn validate_fee_bps(value: u16) -> Result<()> { require!(value <= BPS_DENOMINATOR, BlackHoleError::InvalidBasisPoints); Ok(()) } fn validate_allocations( allocation_count: u8, allocation_mints: &[Pubkey; MAX_ALLOCATIONS], allocation_weights_bps: &[u16; MAX_ALLOCATIONS], ) -> Result<()> { require!( allocation_count as usize <= MAX_ALLOCATIONS, BlackHoleError::InvalidAllocationCount ); let count = allocation_count as usize; let mut total_weight: u16 = 0; for index in 0..MAX_ALLOCATIONS { if index < count { require!( allocation_mints[index] != Pubkey::default(), BlackHoleError::InvalidAllocationMint ); total_weight = total_weight .checked_add(allocation_weights_bps[index]) .ok_or(BlackHoleError::MathOverflow)?; } else { require!( allocation_mints[index] == Pubkey::default(), BlackHoleError::InvalidAllocationMint ); require!( allocation_weights_bps[index] == 0, BlackHoleError::InvalidAllocationWeights ); } } require!( count == 0 || total_weight == BPS_DENOMINATOR, BlackHoleError::InvalidAllocationWeights ); Ok(()) } fn require_keeper(treasury: &BlackHoleTreasury, keeper: &Signer<'_>) -> Result<()> { require_keys_eq!( keeper.key(), treasury.keeper_authority, BlackHoleError::InvalidKeeper ); Ok(()) } fn checked_add(left: u64, right: u64) -> Result { left.checked_add(right) .ok_or(BlackHoleError::MathOverflow.into()) } fn checked_sub(left: u64, right: u64) -> Result { left.checked_sub(right) .ok_or(BlackHoleError::MathUnderflow.into()) } fn share_bps(balance: u64, supply: u64) -> u16 { if supply == 0 { return 0; } let share = (balance as u128) .saturating_mul(BPS_DENOMINATOR as u128) .checked_div(supply as u128) .unwrap_or(0); share.min(BPS_DENOMINATOR as u128) as u16 }