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
3 changes: 2 additions & 1 deletion Cargo.lock

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

3 changes: 2 additions & 1 deletion programs/price_based_performance_package/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "price_based_performance_package"
version = "0.6.0"
version = "0.6.1"
description = "Created with Anchor"
edition = "2021"

Expand All @@ -19,4 +19,5 @@ production = []
[dependencies]
anchor-lang = { version = "=0.29.0", features = ["init-if-needed", "event-cpi"] }
anchor-spl = "=0.29.0"
futarchy = { path = "../futarchy", features = ["cpi"] }
solana-security-txt = "=1.1.1"
4 changes: 4 additions & 0 deletions programs/price_based_performance_package/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,7 @@ use anchor_lang::prelude::*;

#[constant]
pub const MAX_TRANCHES: usize = 10;

/// Scale of oracle prices: quote atoms per base atom, times 1e12
#[constant]
pub const PRICE_SCALE: u128 = 1_000_000_000_000;
22 changes: 22 additions & 0 deletions programs/price_based_performance_package/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,26 @@ pub enum PriceBasedPerformancePackageError {
TotalTokenAmountOverflow,
#[msg("Recipient and performance package authority must be different keys")]
RecipientAuthorityMustDiffer,
#[msg("Withdrawal limits must have non-zero caps, a future end, and a window of at least one second")]
InvalidWithdrawalLimits,
#[msg("Amount exceeds the withdrawable balance")]
InsufficientWithdrawableBalance,
#[msg("Token cap for the current window exceeded")]
TokenWindowLimitExceeded,
#[msg("Quote cap for the current window exceeded")]
QuoteWindowLimitExceeded,
#[msg("Oracle price observation is missing or zero")]
InvalidPriceObservation,
#[msg("Token withdrawals are disabled by the withdrawal mode")]
WithdrawTokensDisabled,
#[msg("Sell withdrawals are disabled by the withdrawal mode")]
WithdrawViaSellDisabled,
#[msg("Performance package has not been resized to the current layout")]
AccountNotMigrated,
#[msg("Quote mint must differ from the package's token mint")]
InvalidQuoteMint,
#[msg("The package's quote account and the quote destination must be passed together")]
QuoteSweepAccountsIncomplete,
#[msg("Oracle Dao's base mint must be the package's token mint")]
OracleMintMismatch,
}
35 changes: 34 additions & 1 deletion programs/price_based_performance_package/src/events.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::ChangeType;
use crate::{ChangeType, WindowUsage};
use anchor_lang::prelude::*;

#[derive(AnchorSerialize, AnchorDeserialize)]
Expand Down Expand Up @@ -43,6 +43,39 @@ pub struct UnlockCompleted {
pub twap_price: u128,
}

/// Present on a withdrawal that ran under active limits
#[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone, Copy)]
pub struct CappedWithdrawal {
/// The price the withdrawal was valued at: the higher of the spot pool's observation and its reserve price
pub price: u128,
/// `amount` valued at that price, in quote atoms
pub quote_value: u64,
/// Window usage after this withdrawal
pub usage: WindowUsage,
}

#[event]
pub struct TokensWithdrawn {
pub common: CommonFields,
pub performance_package: Pubkey,
pub recipient: Pubkey,
pub amount: u64,
/// `None` when no limits were active
pub capped: Option<CappedWithdrawal>,
}

#[event]
pub struct TokensSold {
pub common: CommonFields,
pub performance_package: Pubkey,
pub recipient: Pubkey,
pub amount: u64,
pub quote_received: u64,
pub min_quote_out: u64,
/// Window usage after this sale; `None` when no limits were active
pub capped: Option<WindowUsage>,
}

#[event]
pub struct ChangeProposed {
pub common: CommonFields,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
use anchor_lang::prelude::*;
use anchor_spl::token::{self, Burn, Mint, Token, TokenAccount};
use anchor_spl::{
associated_token::AssociatedToken,
token::{self, Burn, CloseAccount, Mint, Token, TokenAccount},
};

use super::*;

Expand All @@ -15,18 +18,32 @@ pub struct BurnPerformancePackage<'info> {
#[account(
mut,
close = spill_account,
has_one = recipient,
has_one = token_mint,
has_one = performance_package_token_vault
)]
pub performance_package: Box<Account<'info, PerformancePackage>>,

/// Emptied by the payout and the burn, then closed to the spill account
#[account(
mut,
associated_token::mint = token_mint,
associated_token::authority = performance_package
)]
pub performance_package_token_vault: Box<Account<'info, TokenAccount>>,

/// CHECK: Pinned to the package's recipient by `has_one`
pub recipient: UncheckedAccount<'info>,

/// The recipient's ATA that receives the unlocked balance - created if needed
#[account(
init_if_needed,
payer = admin,
associated_token::mint = token_mint,
associated_token::authority = recipient
)]
pub recipient_token_account: Box<Account<'info, TokenAccount>>,

#[account(mut)]
pub admin: Signer<'info>,

Expand All @@ -35,25 +52,80 @@ pub struct BurnPerformancePackage<'info> {
pub spill_account: UncheckedAccount<'info>,

#[account(mut, address = performance_package.token_mint)]
pub token_mint: Account<'info, Mint>,
pub token_mint: Box<Account<'info, Mint>>,

/// The mint of the package's quote ATA; any mint other than the package's token mint
pub quote_mint: Option<Box<Account<'info, Mint>>>,

/// The package's quote ATA, swept into `quote_destination` and closed when passed
#[account(
mut,
associated_token::mint = quote_mint,
associated_token::authority = performance_package
)]
pub package_quote_account: Option<Box<Account<'info, TokenAccount>>>,

/// Where the quote balance goes, chosen by the admin
#[account(mut, token::mint = quote_mint)]
pub quote_destination: Option<Box<Account<'info, TokenAccount>>>,

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

impl BurnPerformancePackage<'_> {
pub fn validate(&self) -> Result<()> {
PerformancePackage::assert_migrated(&self.performance_package.to_account_info())?;

#[cfg(feature = "production")]
require_keys_eq!(
self.admin.key(),
admin::ID,
PriceBasedPerformancePackageError::InvalidAdmin
);

// Ensure the quote mint is not the package's token mint.
if let Some(quote_mint) = &self.quote_mint {
require_keys_neq!(
quote_mint.key(),
self.token_mint.key(),
PriceBasedPerformancePackageError::InvalidQuoteMint
);
}

// Ensure the quote account and its destination are passed together.
require_eq!(
self.package_quote_account.is_some(),
self.quote_destination.is_some(),
PriceBasedPerformancePackageError::QuoteSweepAccountsIncomplete
);

Ok(())
}

pub fn handle(ctx: Context<Self>) -> Result<()> {
let performance_package = &ctx.accounts.performance_package;
let Self {
performance_package,
performance_package_token_vault,
recipient: _,
recipient_token_account,
admin: _,
spill_account,
token_mint,
quote_mint: _,
package_quote_account,
quote_destination,
system_program: _,
token_program,
associated_token_program: _,
} = ctx.accounts;

let vault_amount = performance_package_token_vault.amount;
let withdrawable = performance_package.withdrawable(vault_amount)?;
let locked = vault_amount
.checked_sub(withdrawable)
.ok_or(PriceBasedPerformancePackageError::InvariantViolated)?;

let seeds = &[
b"performance_package",
Expand All @@ -62,25 +134,78 @@ impl BurnPerformancePackage<'_> {
];
let signer = &[&seeds[..]];

// Burn any remaining tokens in the performance package token vault
if ctx.accounts.performance_package_token_vault.amount > 0 {
// Hand the recipient what is already unlocked before burning the rest
if withdrawable > 0 {
token::transfer(
CpiContext::new_with_signer(
token_program.to_account_info(),
token::Transfer {
from: performance_package_token_vault.to_account_info(),
to: recipient_token_account.to_account_info(),
authority: performance_package.to_account_info(),
},
signer,
),
withdrawable,
)?;
}

if locked > 0 {
token::burn(
CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
token_program.to_account_info(),
Burn {
mint: ctx.accounts.token_mint.to_account_info(),
from: ctx
.accounts
.performance_package_token_vault
.to_account_info(),
mint: token_mint.to_account_info(),
from: performance_package_token_vault.to_account_info(),
authority: performance_package.to_account_info(),
},
signer,
),
ctx.accounts.performance_package_token_vault.amount,
locked,
)?;
}

// The vault is empty now, so its rent goes to the spill account
token::close_account(CpiContext::new_with_signer(
token_program.to_account_info(),
CloseAccount {
account: performance_package_token_vault.to_account_info(),
destination: spill_account.to_account_info(),
authority: performance_package.to_account_info(),
},
signer,
))?;

// Move whatever sits in the quote account to the admin's destination, then close it
if let (Some(package_quote_account), Some(quote_destination)) =
(package_quote_account, quote_destination)
{
if package_quote_account.amount > 0 {
token::transfer(
CpiContext::new_with_signer(
token_program.to_account_info(),
token::Transfer {
from: package_quote_account.to_account_info(),
to: quote_destination.to_account_info(),
authority: performance_package.to_account_info(),
},
signer,
),
package_quote_account.amount,
)?;
}

token::close_account(CpiContext::new_with_signer(
token_program.to_account_info(),
CloseAccount {
account: package_quote_account.to_account_info(),
destination: spill_account.to_account_info(),
authority: performance_package.to_account_info(),
},
signer,
))?;
}

// Performance package account gets closed using close constraint

Ok(())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ pub struct ChangePerformancePackageAuthority<'info> {

impl<'info> ChangePerformancePackageAuthority<'info> {
pub fn validate(&self, params: &ChangePerformancePackageAuthorityParams) -> Result<()> {
PerformancePackage::assert_migrated(&self.performance_package.to_account_info())?;

require_keys_neq!(
params.new_performance_package_authority,
self.performance_package.recipient,
Expand Down
Loading
Loading