Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Anchor.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ conditional_vault = "VLTX1ishMBbcX3rdBWGssxawAo1Q2X2qxYFYqiGodVg"
futarchy = "FUTARELBfJfQ8RDGhg1wdhddq1odMAJUePHFuBYfUxKq"
launchpad = "MooNyh4CBUYEKyXVnjGYQ8mEiJDpGvJMdvrZx1iGeHV"
launchpad_v7 = "moontUzsdepotRGe5xsfip7vLPTJnVuafqdUWexVnPM"
liquidation = "LiQnowFbFQdYyZhF4pUbpsrZCjxRTQ1upKJxZ2VXjde"
mint_governor = "gvnr27cVeyW3AVf3acL7VCJ5WjGAphytnsgcK1feHyH"
performance_package_v2 = "pPV2pfrxnmstSb9j7kEeCLny5BGj6SNwCWGd6xbGGzz"
price_based_performance_package = "pbPPQH7jyKoSLu8QYs3rSY3YkDRXEBojKbTgnUg7NDS"
Expand Down
3 changes: 3 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,9 @@ pub recipient_ata: Account<'info, TokenAccount>,
pub funder_token_account: Account<'info, TokenAccount>,
```

### Events
Always use CPI events (`#[event_cpi]` on accounts structs, `emit_cpi!` for emission) rather than regular `emit!`.

### Require Macros
When writing validation checks, prefer specific require macros over generic `require!`:
1. `require_keys_eq!` - when comparing two `Pubkey` values
Expand Down
9 changes: 9 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 21 additions & 0 deletions programs/liquidation/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[package]
name = "liquidation"
version = "0.1.0"
description = "Manages the orderly liquidation of a project's treasury back to token holders."
edition = "2021"

[lib]
crate-type = ["cdylib", "lib"]
name = "liquidation"

[features]
no-entrypoint = []
no-idl = []
no-log-ix-name = []
cpi = ["no-entrypoint"]
default = []

[dependencies]
anchor-lang = { version = "0.29.0", features = ["event-cpi", "init-if-needed"] }
anchor-spl = "0.29.0"
solana-security-txt = "1.1.1"
2 changes: 2 additions & 0 deletions programs/liquidation/Xargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[target.bpfel-unknown-unknown.dependencies.std]
features = []
27 changes: 27 additions & 0 deletions programs/liquidation/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
use super::*;

#[error_code]
pub enum LiquidationError {
#[msg("Refunding is not enabled")]
RefundingNotEnabled,
#[msg("Liquidation is already activated")]
AlreadyActivated,
#[msg("No quote tokens to fund")]
NothingToFund,
#[msg("No base tokens assigned")]
NoBaseAssigned,
#[msg("Refund window has expired")]
RefundWindowExpired,
#[msg("Refund window has not expired")]
RefundWindowNotExpired,
#[msg("Duration must be greater than zero")]
InvalidDuration,
#[msg("Nothing to refund")]
NothingToRefund,
#[msg("Invalid allocation")]
InvalidAllocation,
#[msg("Invalid authority")]
InvalidAuthority,
#[msg("Invalid mint")]
InvalidMint,
}
73 changes: 73 additions & 0 deletions programs/liquidation/src/events.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
use anchor_lang::prelude::*;

#[derive(AnchorSerialize, AnchorDeserialize)]
pub struct CommonFields {
pub slot: u64,
pub unix_timestamp: i64,
pub liquidation_seq_num: u64,
}

impl CommonFields {
pub fn new(clock: &Clock, liquidation_seq_num: u64) -> Self {
Self {
slot: clock.slot,
unix_timestamp: clock.unix_timestamp,
liquidation_seq_num,
}
}
}

#[event]
pub struct LiquidationCreatedEvent {
pub common: CommonFields,
pub liquidation: Pubkey,
pub record_authority: Pubkey,
pub liquidation_authority: Pubkey,
pub base_mint: Pubkey,
pub quote_mint: Pubkey,
pub duration_seconds: u32,
pub pda_bump: u8,
}

#[event]
pub struct LiquidationActivatedEvent {
pub common: CommonFields,
pub liquidation: Pubkey,
pub total_quote_funded: u64,
pub started_at: i64,
}

#[event]
pub struct RefundRecordSetEvent {
pub common: CommonFields,
pub liquidation: Pubkey,
pub refund_record: Pubkey,
pub recipient: Pubkey,
pub base_assigned: u64,
pub quote_refundable: u64,
pub liquidation_total_base_assigned: u64,
pub liquidation_total_quote_refundable: u64,
pub pda_bump: u8,
}

#[event]
pub struct RefundEvent {
pub common: CommonFields,
pub liquidation: Pubkey,
pub refund_record: Pubkey,
pub recipient: Pubkey,
pub base_burned: u64,
pub quote_refunded: u64,
pub post_record_base_burned: u64,
pub post_record_quote_refunded: u64,
pub post_liquidation_total_base_burned: u64,
pub post_liquidation_total_quote_refunded: u64,
}

#[event]
pub struct WithdrawRemainingQuoteEvent {
pub common: CommonFields,
pub liquidation: Pubkey,
pub liquidation_authority: Pubkey,
pub amount: u64,
}
99 changes: 99 additions & 0 deletions programs/liquidation/src/instructions/activate_liquidation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
use anchor_lang::prelude::*;
use anchor_spl::token::{self, Mint, Token, TokenAccount, Transfer};

use crate::{
error::LiquidationError,
events::{CommonFields, LiquidationActivatedEvent},
state::Liquidation,
};

#[event_cpi]
#[derive(Accounts)]
pub struct ActivateLiquidation<'info> {
pub liquidation_authority: Signer<'info>,

#[account(
mut,
has_one = liquidation_authority @ LiquidationError::InvalidAuthority,
has_one = quote_mint @ LiquidationError::InvalidMint,
)]
pub liquidation: Account<'info, Liquidation>,

#[account(
mut,
token::mint = quote_mint,
token::authority = liquidation_authority,
)]
pub liquidation_authority_quote_account: Account<'info, TokenAccount>,

#[account(
mut,
associated_token::mint = quote_mint,
associated_token::authority = liquidation,
)]
pub liquidation_quote_vault: Account<'info, TokenAccount>,

pub quote_mint: Account<'info, Mint>,

pub token_program: Program<'info, Token>,
}

impl ActivateLiquidation<'_> {
pub fn validate(&self) -> Result<()> {
require!(
!self.liquidation.is_refunding,
LiquidationError::AlreadyActivated
);

require_gt!(
self.liquidation.total_base_assigned,
0,
LiquidationError::NoBaseAssigned
);

require_gt!(
self.liquidation.total_quote_refundable,
0,
LiquidationError::NothingToFund
);

Ok(())
}

pub fn handle(ctx: Context<Self>) -> Result<()> {
let clock = Clock::get()?;
let liquidation = &mut ctx.accounts.liquidation;

// Transfer total_quote_refundable from authority to vault
token::transfer(
CpiContext::new(
ctx.accounts.token_program.to_account_info(),
Transfer {
from: ctx
.accounts
.liquidation_authority_quote_account
.to_account_info(),
to: ctx.accounts.liquidation_quote_vault.to_account_info(),
authority: ctx.accounts.liquidation_authority.to_account_info(),
},
),
liquidation.total_quote_refundable,
)?;

// Permanently enable refunding
liquidation.started_at = clock.unix_timestamp;
liquidation.is_refunding = true;

// Emit event
liquidation.seq_num += 1;

emit_cpi!(LiquidationActivatedEvent {
common: CommonFields::new(&clock, liquidation.seq_num),
liquidation: liquidation.key(),
total_quote_funded: liquidation.total_quote_refundable,
started_at: liquidation.started_at,
});

Ok(())
}
}
96 changes: 96 additions & 0 deletions programs/liquidation/src/instructions/initialize_liquidation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
use anchor_lang::prelude::*;
use anchor_spl::{
associated_token::AssociatedToken,
token::{Mint, Token, TokenAccount},
};

use crate::{
error::LiquidationError,
events::{CommonFields, LiquidationCreatedEvent},
state::{Liquidation, SEED_LIQUIDATION},
};

#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
pub struct InitializeLiquidationArgs {
pub duration_seconds: u32,
}

#[event_cpi]
#[derive(Accounts)]
pub struct InitializeLiquidation<'info> {
#[account(mut)]
pub payer: Signer<'info>,

pub create_key: Signer<'info>,

/// CHECK: Stored on the Liquidation account as the record authority.
pub record_authority: UncheckedAccount<'info>,
/// CHECK: Stored on the Liquidation account as the liquidation authority.
pub liquidation_authority: UncheckedAccount<'info>,

pub base_mint: Account<'info, Mint>,
pub quote_mint: Account<'info, Mint>,

#[account(
init,
payer = payer,
space = 8 + Liquidation::INIT_SPACE,
seeds = [SEED_LIQUIDATION, base_mint.key().as_ref(), quote_mint.key().as_ref(), create_key.key().as_ref()],
bump
)]
pub liquidation: Account<'info, Liquidation>,

#[account(
init,
payer = payer,
associated_token::mint = quote_mint,
associated_token::authority = liquidation,
)]
pub liquidation_quote_vault: Account<'info, TokenAccount>,

pub system_program: Program<'info, System>,
pub token_program: Program<'info, Token>,
pub associated_token_program: Program<'info, AssociatedToken>,
}

impl InitializeLiquidation<'_> {
pub fn validate(&self, args: &InitializeLiquidationArgs) -> Result<()> {
// Refund window must have a nonzero duration
require_gt!(args.duration_seconds, 0, LiquidationError::InvalidDuration);
Ok(())
}

pub fn handle(ctx: Context<Self>, args: InitializeLiquidationArgs) -> Result<()> {
let clock = Clock::get()?;

ctx.accounts.liquidation.set_inner(Liquidation {
create_key: ctx.accounts.create_key.key(),
record_authority: ctx.accounts.record_authority.key(),
liquidation_authority: ctx.accounts.liquidation_authority.key(),
base_mint: ctx.accounts.base_mint.key(),
quote_mint: ctx.accounts.quote_mint.key(),
total_quote_refundable: 0,
total_quote_refunded: 0,
total_base_assigned: 0,
total_base_burned: 0,
started_at: 0,
duration_seconds: args.duration_seconds,
seq_num: 0,
is_refunding: false,
pda_bump: ctx.bumps.liquidation,
});

emit_cpi!(LiquidationCreatedEvent {
common: CommonFields::new(&clock, ctx.accounts.liquidation.seq_num),
liquidation: ctx.accounts.liquidation.key(),
record_authority: ctx.accounts.record_authority.key(),
liquidation_authority: ctx.accounts.liquidation_authority.key(),
base_mint: ctx.accounts.base_mint.key(),
quote_mint: ctx.accounts.quote_mint.key(),
duration_seconds: args.duration_seconds,
pda_bump: ctx.bumps.liquidation,
});

Ok(())
}
}
11 changes: 11 additions & 0 deletions programs/liquidation/src/instructions/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
pub mod activate_liquidation;
pub mod initialize_liquidation;
pub mod refund;
pub mod set_refund_record;
pub mod withdraw_remaining_quote;

pub use activate_liquidation::*;
pub use initialize_liquidation::*;
pub use refund::*;
pub use set_refund_record::*;
pub use withdraw_remaining_quote::*;
Loading
Loading