crate_redeem_in_kind/
lib.rs

1//! In-kind distributions for redeeming Crate assets.
2#![deny(rustdoc::all)]
3#![allow(rustdoc::missing_doc_code_examples)]
4
5mod account_validators;
6
7pub mod events;
8
9use anchor_lang::prelude::*;
10use anchor_lang::solana_program;
11use anchor_lang::solana_program::account_info::next_account_infos;
12use anchor_spl::token::{self, Mint, Token, TokenAccount};
13use num_traits::cast::ToPrimitive;
14use static_pubkey::static_pubkey;
15use vipers::validate::Validate;
16use vipers::{invariant, unwrap_int};
17
18use events::*;
19
20declare_id!("1NKyU3qShZC3oJgvCCftAHDi5TFxcJwfyUz2FeZsiwE");
21
22/// Address of the withdraw authority to use for this Crate.
23pub static WITHDRAW_AUTHORITY_ADDRESS: Pubkey =
24    static_pubkey!("2amCDqmgpQ2qkryLArCcYeX8DzyNqvjuy7yKq6hsonqF");
25
26/// Bump seed of the above address.
27pub const WITHDRAW_AUTHORITY_ADDRESS_BUMP: u8 = 255;
28
29/// Signer seeds of the [WITHDRAW_AUTHORITY_ADDRESS].
30pub static WITHDRAW_AUTHORITY_SIGNER_SEEDS: &[&[&[u8]]] =
31    &[&[b"CrateRedeemInKind", &[WITHDRAW_AUTHORITY_ADDRESS_BUMP]]];
32
33/// [crate_redeem_in_kind] program.
34#[program]
35pub mod crate_redeem_in_kind {
36    use std::collections::BTreeMap;
37
38    use super::*;
39
40    /// Redeems Crate tokens for their underlying assets, in-kind.
41    /// This redemption limits the number of assets that can be redeemed,
42    /// but it ensures that all assets are redeemed equally.
43    #[access_control(ctx.accounts.validate())]
44    pub fn redeem<'info>(
45        ctx: Context<'_, '_, '_, 'info, Redeem<'info>>,
46        amount: u64,
47    ) -> Result<()> {
48        let burn = token::Burn {
49            mint: ctx.accounts.crate_mint.to_account_info(),
50            from: ctx.accounts.crate_source.to_account_info(),
51            authority: ctx.accounts.owner.to_account_info(),
52        };
53
54        token::burn(
55            CpiContext::new(ctx.accounts.token_program.to_account_info(), burn),
56            amount,
57        )?;
58
59        // calculate the fractional slice of each account
60        let num_remaining_accounts = ctx.remaining_accounts.len();
61        if num_remaining_accounts == 0 {
62            return Ok(());
63        }
64        invariant!(
65            num_remaining_accounts % 4 == 0,
66            "must have even number of tokens"
67        );
68        let num_tokens = unwrap_int!(num_remaining_accounts.checked_div(4));
69        // TODO: add check to make sure every single token in the crate was redeemed
70
71        let remaining_accounts_iter = &mut ctx.remaining_accounts.iter();
72
73        for _i in 0..num_tokens {
74            // none of these accounts need to be validated further, since
75            // [crate_token::cpi::withdraw] already handles it.
76            let bumps = &mut BTreeMap::new();
77            let asset: RedeemAsset = Accounts::try_accounts(
78                &crate::ID,
79                &mut next_account_infos(remaining_accounts_iter, 4)?,
80                &[],
81                bumps,
82            )?;
83
84            let share: u64 = unwrap_int!((asset.crate_underlying.amount as u128)
85                .checked_mul(amount.into())
86                .and_then(|num| num.checked_div(ctx.accounts.crate_mint.supply.into()))
87                .and_then(|num| num.to_u64()));
88
89            crate_token::cpi::withdraw(
90                CpiContext::new_with_signer(
91                    ctx.accounts.crate_token_program.to_account_info(),
92                    crate_token::cpi::accounts::Withdraw {
93                        crate_token: ctx.accounts.crate_token.to_account_info(),
94                        crate_underlying: asset.crate_underlying.to_account_info(),
95                        withdraw_authority: ctx.accounts.withdraw_authority.to_account_info(),
96                        withdraw_destination: asset.withdraw_destination.to_account_info(),
97                        author_fee_destination: asset.author_fee_destination.to_account_info(),
98                        protocol_fee_destination: asset.protocol_fee_destination.to_account_info(),
99                        token_program: ctx.accounts.token_program.to_account_info(),
100                    },
101                    WITHDRAW_AUTHORITY_SIGNER_SEEDS,
102                ),
103                share,
104            )?;
105        }
106
107        emit!(RedeemEvent {
108            crate_key: ctx.accounts.crate_token.key(),
109            source: ctx.accounts.crate_source.key(),
110            amount
111        });
112
113        Ok(())
114    }
115}
116
117// --------------------------------
118// Context Structs
119// --------------------------------
120
121/// Accounts for [crate_redeem_in_kind::redeem].
122#[derive(Accounts)]
123pub struct Redeem<'info> {
124    /// The withdraw authority PDA.
125    /// CHECK: Arbitrary.
126    pub withdraw_authority: UncheckedAccount<'info>,
127
128    /// Information about the crate.
129    #[account(has_one = withdraw_authority)]
130    pub crate_token: Account<'info, crate_token::CrateToken>,
131
132    /// [Mint] of the [crate_token::CrateToken].
133    #[account(mut)]
134    pub crate_mint: Account<'info, Mint>,
135
136    /// Source of the crate tokens.
137    #[account(mut)]
138    pub crate_source: Account<'info, TokenAccount>,
139
140    /// Owner of the crate source.
141    pub owner: Signer<'info>,
142
143    /// [Token] program.
144    pub token_program: Program<'info, Token>,
145
146    /// [crate_token] program.
147    pub crate_token_program: Program<'info, crate_token::program::CrateToken>,
148}
149
150/// Asset redeemed in [crate_redeem_in_kind::redeem].
151#[derive(Accounts)]
152pub struct RedeemAsset<'info> {
153    /// Crate account of the tokens
154    #[account(mut)]
155    pub crate_underlying: Account<'info, TokenAccount>,
156
157    /// Destination of the tokens to redeem
158    #[account(mut)]
159    pub withdraw_destination: Account<'info, TokenAccount>,
160
161    /// Author fee token destination
162    #[account(mut)]
163    pub author_fee_destination: Account<'info, TokenAccount>,
164
165    /// Protocol fee token destination
166    #[account(mut)]
167    pub protocol_fee_destination: Account<'info, TokenAccount>,
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173    #[test]
174    fn test_withdraw_authority_address() {
175        let (key, bump) = Pubkey::find_program_address(&[b"CrateRedeemInKind"], &crate::ID);
176        assert_eq!(key, WITHDRAW_AUTHORITY_ADDRESS);
177        assert_eq!(bump, WITHDRAW_AUTHORITY_ADDRESS_BUMP);
178    }
179}