Back to the Black Hole

SMART CONTRACT / SOURCE

The treasury vault.

Download source

Not deployed. This program implements vault initialization and token deposits. Its program ID is a placeholder. Trading fees, reward routing, and automatic buybacks are not implemented.

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<InitializeTreasury>,
        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<AdminTreasury>, keeper_authority: Pubkey) -> Result<()> {
        ctx.accounts.treasury.keeper_authority = keeper_authority;
        emit!(KeeperUpdated { keeper_authority });
        Ok(())
    }

    pub fn set_allowed_swap_program(
        ctx: Context<AdminTreasury>,
        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<AdminTreasury>, paused: bool) -> Result<()> {
        ctx.accounts.treasury.paused = paused;
        emit!(PauseUpdated { paused });
        Ok(())
    }

    pub fn set_allocations(
        ctx: Context<AdminTreasury>,
        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<CreateEcosystemPosition>,
        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<LockHole>, 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<AbsorbBoughtHole>, 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<SyncStonkRewards>) -> 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<SyncEcosystemPosition>) -> 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<ExecuteEcosystemPurchase>,
        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<ExecuteHoleBuyback>,
        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<u8>,
}

#[derive(Accounts)]
pub struct InitializeTreasury<'info> {
    #[account(mut)]
    pub authority: Signer<'info>,
    pub hole_mint: Box<InterfaceAccount<'info, Mint>>,
    pub stonk_mint: Box<InterfaceAccount<'info, Mint>>,
    #[account(
        init,
        payer = authority,
        space = 8 + BlackHoleTreasury::LEN,
        seeds = [TREASURY_SEED],
        bump
    )]
    pub treasury: Box<Account<'info, BlackHoleTreasury>>,
    #[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<InterfaceAccount<'info, TokenAccount>>,
    #[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<InterfaceAccount<'info, TokenAccount>>,
    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<Account<'info, BlackHoleTreasury>>,
}

#[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<Account<'info, BlackHoleTreasury>>,
    pub ecosystem_mint: Box<InterfaceAccount<'info, Mint>>,
    pub reward_mint: Box<InterfaceAccount<'info, Mint>>,
    #[account(
        init,
        payer = authority,
        space = 8 + EcosystemPosition::LEN,
        seeds = [POSITION_SEED, treasury.key().as_ref(), &[index]],
        bump
    )]
    pub position: Box<Account<'info, EcosystemPosition>>,
    #[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<InterfaceAccount<'info, TokenAccount>>,
    #[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<InterfaceAccount<'info, TokenAccount>>,
    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<InterfaceAccount<'info, Mint>>,
    #[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<InterfaceAccount<'info, TokenAccount>>,
    #[account(mut, seeds = [TREASURY_SEED], bump = treasury.bump)]
    pub treasury: Box<Account<'info, BlackHoleTreasury>>,
    #[account(
        mut,
        address = treasury.hole_vault @ BlackHoleError::InvalidVault,
        constraint = hole_vault.mint == treasury.hole_mint @ BlackHoleError::InvalidMint
    )]
    pub hole_vault: Box<InterfaceAccount<'info, TokenAccount>>,
    pub hole_token_program: Interface<'info, TokenInterface>,
}

#[derive(Accounts)]
pub struct AbsorbBoughtHole<'info> {
    #[account(mut)]
    pub keeper: Signer<'info>,
    pub hole_mint: Box<InterfaceAccount<'info, Mint>>,
    #[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<InterfaceAccount<'info, TokenAccount>>,
    #[account(mut, seeds = [TREASURY_SEED], bump = treasury.bump)]
    pub treasury: Box<Account<'info, BlackHoleTreasury>>,
    #[account(
        mut,
        address = treasury.hole_vault @ BlackHoleError::InvalidVault,
        constraint = hole_vault.mint == treasury.hole_mint @ BlackHoleError::InvalidMint
    )]
    pub hole_vault: Box<InterfaceAccount<'info, TokenAccount>>,
    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<'info, BlackHoleTreasury>>,
    #[account(
        address = treasury.stonk_vault @ BlackHoleError::InvalidVault,
        constraint = stonk_vault.mint == treasury.stonk_mint @ BlackHoleError::InvalidMint
    )]
    pub stonk_vault: Box<InterfaceAccount<'info, TokenAccount>>,
}

#[derive(Accounts)]
pub struct SyncEcosystemPosition<'info> {
    #[account(seeds = [TREASURY_SEED], bump = treasury.bump)]
    pub treasury: Box<Account<'info, BlackHoleTreasury>>,
    #[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<'info, EcosystemPosition>>,
    #[account(
        address = position.ecosystem_vault @ BlackHoleError::InvalidVault,
        constraint = ecosystem_vault.mint == position.ecosystem_mint @ BlackHoleError::InvalidMint
    )]
    pub ecosystem_vault: Box<InterfaceAccount<'info, TokenAccount>>,
    #[account(
        address = position.reward_vault @ BlackHoleError::InvalidVault,
        constraint = reward_vault.mint == position.reward_mint @ BlackHoleError::InvalidMint
    )]
    pub reward_vault: Box<InterfaceAccount<'info, TokenAccount>>,
}

#[derive(Accounts)]
pub struct ExecuteEcosystemPurchase<'info> {
    #[account(mut)]
    pub keeper: Signer<'info>,
    #[account(mut, seeds = [TREASURY_SEED], bump = treasury.bump)]
    pub treasury: Box<Account<'info, BlackHoleTreasury>>,
    #[account(
        mut,
        address = treasury.stonk_vault @ BlackHoleError::InvalidVault,
        constraint = stonk_vault.mint == treasury.stonk_mint @ BlackHoleError::InvalidMint
    )]
    pub stonk_vault: Box<InterfaceAccount<'info, TokenAccount>>,
    #[account(
        address = treasury.hole_vault @ BlackHoleError::InvalidVault,
        constraint = hole_vault.mint == treasury.hole_mint @ BlackHoleError::InvalidMint
    )]
    pub hole_vault: Box<InterfaceAccount<'info, TokenAccount>>,
    #[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<'info, EcosystemPosition>>,
    #[account(
        mut,
        address = position.ecosystem_vault @ BlackHoleError::InvalidVault,
        constraint = ecosystem_vault.mint == position.ecosystem_mint @ BlackHoleError::InvalidMint
    )]
    pub ecosystem_vault: Box<InterfaceAccount<'info, TokenAccount>>,
    /// 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<InterfaceAccount<'info, Mint>>,
    #[account(mut, seeds = [TREASURY_SEED], bump = treasury.bump)]
    pub treasury: Box<Account<'info, BlackHoleTreasury>>,
    #[account(
        mut,
        address = treasury.hole_vault @ BlackHoleError::InvalidVault,
        constraint = hole_vault.mint == treasury.hole_mint @ BlackHoleError::InvalidMint
    )]
    pub hole_vault: Box<InterfaceAccount<'info, TokenAccount>>,
    #[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<'info, EcosystemPosition>>,
    #[account(
        mut,
        address = position.reward_vault @ BlackHoleError::InvalidVault,
        constraint = reward_vault.mint == position.reward_mint @ BlackHoleError::InvalidMint
    )]
    pub reward_vault: Box<InterfaceAccount<'info, TokenAccount>>,
    /// 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<u8>,
) -> 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::<Vec<_>>();
    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<u64> {
    left.checked_add(right)
        .ok_or(BlackHoleError::MathOverflow.into())
}

fn checked_sub(left: u64, right: u64) -> Result<u64> {
    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
}