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
}