mint_proxy/
lib.rs

1//! Manages the minting of new Saber tokens.
2#![allow(deprecated)]
3
4use anchor_lang::solana_program;
5use anchor_lang::{prelude::*, solana_program::pubkey::PUBKEY_BYTES};
6use anchor_spl::token::{self, Mint, SetAuthority, Token, TokenAccount};
7use vipers::prelude::*;
8
9mod proxy_seeds;
10
11declare_id!("UBEBk5idELqykEEaycYtQ7iBVrCg6NmvFSzMpdr22mL");
12
13/// Address of the mint proxy program's state associated account.
14pub const PROXY_STATE_ACCOUNT: Pubkey =
15    static_pubkey::static_pubkey!("9qRjwMQYrkd5JvsENaYYxSCgwEuVhK4qAo5kCFHSmdmL");
16
17/// Address of the proxy mint authority.
18pub const PROXY_MINT_AUTHORITY: Pubkey =
19    static_pubkey::static_pubkey!("GyktbGXbH9kvxP8RGfWsnFtuRgC7QCQo2WBqpo3ryk7L");
20
21/// Stub for invoking [mint_proxy::MintProxy::perform_mint].
22#[cfg(feature = "cpi")]
23pub fn invoke_perform_mint<'a, 'b, 'c, 'info>(
24    ctx: CpiContext<'a, 'b, 'c, 'info, crate::cpi::accounts::PerformMint<'info>>,
25    mint_proxy_state: AccountInfo<'info>,
26    amount: u64,
27) -> Result<()> {
28    let ix = {
29        let ix = crate::instruction::state::PerformMint { amount };
30        let data = anchor_lang::InstructionData::data(&ix);
31        let mut accounts = ctx.to_account_metas(None);
32        accounts.insert(0, AccountMeta::new_readonly(mint_proxy_state.key(), false));
33        anchor_lang::solana_program::instruction::Instruction {
34            program_id: crate::ID,
35            accounts,
36            data,
37        }
38    };
39    let mut acc_infos = ctx.to_account_infos();
40    acc_infos.insert(0, mint_proxy_state);
41    anchor_lang::solana_program::program::invoke_signed(&ix, &acc_infos, ctx.signer_seeds)?;
42
43    Ok(())
44}
45
46#[program]
47pub mod mint_proxy {
48    use super::*;
49
50    #[state]
51    pub struct MintProxy {
52        /// Nonce for allowing the proxy mint authority to sign.
53        pub nonce: u8,
54        /// Maximum number of tokens that can be issued.
55        pub hard_cap: u64,
56        /// Account which is the authority over minted tokens.
57        pub proxy_mint_authority: Pubkey,
58        /// Owner account which can perform admin operations.
59        pub owner: Pubkey,
60        /// Next owner account.
61        pub pending_owner: Pubkey,
62        /// Account key of the state struct.
63        pub state_associated_account: Pubkey,
64        /// Mint of the token to be minted
65        pub token_mint: Pubkey,
66    }
67
68    impl MintProxy {
69        pub fn new(ctx: Context<Initialize>, nonce: u8, hard_cap: u64) -> Result<Self> {
70            require!(
71                ctx.accounts.token_mint.freeze_authority.is_none(),
72                InvalidFreezeAuthority
73            );
74
75            let proxy_signer_seeds = proxy_seeds::gen_signer_seeds(&nonce, &PROXY_STATE_ACCOUNT);
76            require!(
77                vipers::validate_derived_address(
78                    ctx.accounts.proxy_mint_authority.key,
79                    ctx.program_id,
80                    &proxy_signer_seeds[..],
81                ),
82                InvalidProxyAuthority
83            );
84
85            let proxy_mint_authority = *ctx.accounts.proxy_mint_authority.key;
86            let cpi_ctx = new_set_authority_cpi_context(
87                &ctx.accounts.mint_authority,
88                &ctx.accounts.token_mint.to_account_info(),
89                &ctx.accounts.token_program,
90            );
91            token::set_authority(
92                cpi_ctx,
93                spl_token::instruction::AuthorityType::MintTokens,
94                Some(proxy_mint_authority),
95            )?;
96
97            Ok(Self {
98                nonce,
99                proxy_mint_authority,
100                owner: *ctx.accounts.owner.key,
101                pending_owner: Pubkey::default(),
102                state_associated_account: PROXY_STATE_ACCOUNT,
103                token_mint: *ctx.accounts.token_mint.to_account_info().key,
104                hard_cap,
105            })
106        }
107
108        /// Transfers ownership to another account.
109        #[access_control(only_owner(self, &ctx.accounts))]
110        pub fn transfer_ownership(&mut self, ctx: Context<Auth>, next_owner: Pubkey) -> Result<()> {
111            self.pending_owner = next_owner;
112            Ok(())
113        }
114
115        /// Accepts the new ownership.
116        pub fn accept_ownership(&mut self, ctx: Context<Auth>) -> Result<()> {
117            require!(ctx.accounts.owner.is_signer, Unauthorized);
118            require!(
119                self.pending_owner == *ctx.accounts.owner.key,
120                PendingOwnerMismatch
121            );
122            self.owner = self.pending_owner;
123            self.pending_owner = Pubkey::default();
124            Ok(())
125        }
126
127        /// Adds a minter to the mint proxy.
128        #[access_control(only_owner(self, &ctx.accounts.auth))]
129        pub fn minter_add(&self, ctx: Context<MinterAdd>, allowance: u64) -> Result<()> {
130            let minter_info = &mut ctx.accounts.minter_info;
131            minter_info.minter = ctx.accounts.minter.key();
132            minter_info.allowance = allowance;
133            minter_info.__nonce = *unwrap_int!(ctx.bumps.get("minter_info"));
134            Ok(())
135        }
136
137        /// Updates a mint's allowance.
138        #[access_control(only_owner(self, &ctx.accounts.auth))]
139        pub fn minter_update(&self, ctx: Context<MinterUpdate>, allowance: u64) -> Result<()> {
140            let minter_info = &mut ctx.accounts.minter_info;
141            minter_info.allowance = allowance;
142            Ok(())
143        }
144
145        /// Removes a minter from the list.
146        #[access_control(only_owner(self, &ctx.accounts.auth))]
147        pub fn minter_remove(&self, ctx: Context<MinterRemove>) -> Result<()> {
148            Ok(())
149        }
150
151        /// Performs a mint.
152        pub fn perform_mint(&self, ctx: Context<PerformMint>, amount: u64) -> Result<()> {
153            ctx.accounts.validate(self)?;
154
155            let minter_info = &mut ctx.accounts.minter_info;
156            require!(minter_info.allowance >= amount, MinterAllowanceExceeded);
157
158            let new_supply = unwrap_int!(ctx.accounts.token_mint.supply.checked_add(amount),);
159            require!(new_supply <= self.hard_cap, HardcapExceeded);
160
161            minter_info.allowance = unwrap_int!(minter_info.allowance.checked_sub(amount));
162            let seeds = proxy_seeds::gen_signer_seeds(&self.nonce, &self.state_associated_account);
163            let proxy_signer = &[&seeds[..]];
164            let cpi_ctx = CpiContext::new_with_signer(
165                ctx.accounts.token_program.to_account_info(),
166                token::MintTo {
167                    mint: ctx.accounts.token_mint.to_account_info(),
168                    to: ctx.accounts.destination.to_account_info(),
169                    authority: ctx.accounts.proxy_mint_authority.to_account_info(),
170                },
171                proxy_signer,
172            );
173            token::mint_to(cpi_ctx, amount)?;
174            Ok(())
175        }
176
177        /// Makes a different account the mint authority.
178        #[access_control(only_owner(self, &ctx.accounts.auth))]
179        pub fn set_mint_authority(
180            &self,
181            ctx: Context<SetMintAuthority>,
182            new_authority: Pubkey,
183        ) -> Result<()> {
184            let mut proxy_mint_authority = ctx.accounts.proxy_mint_authority.to_account_info();
185            proxy_mint_authority.is_signer = true;
186
187            let seeds = proxy_seeds::gen_signer_seeds(&self.nonce, &self.state_associated_account);
188            let proxy_signer = &[&seeds[..]];
189            let cpi_ctx = new_set_authority_cpi_context(
190                &proxy_mint_authority,
191                &ctx.accounts.token_mint.to_account_info(),
192                &ctx.accounts.token_program,
193            )
194            .with_signer(proxy_signer);
195
196            token::set_authority(
197                cpi_ctx,
198                spl_token::instruction::AuthorityType::MintTokens,
199                Some(new_authority),
200            )?;
201
202            Ok(())
203        }
204    }
205}
206
207#[derive(Accounts)]
208pub struct Auth<'info> {
209    pub owner: Signer<'info>,
210}
211
212#[derive(Accounts)]
213pub struct Initialize<'info> {
214    /// Current mint authority.
215    pub mint_authority: Signer<'info>,
216
217    /// New mint authority. PDA.
218    /// CHECK: Proxy mint authority
219    #[account(address = PROXY_MINT_AUTHORITY)]
220    pub proxy_mint_authority: UncheckedAccount<'info>,
221
222    /// Owner of the mint proxy.
223    /// CHECK: Arbitrary
224    pub owner: UncheckedAccount<'info>,
225
226    /// Token mint to mint.
227    #[account(mut)]
228    pub token_mint: Account<'info, Mint>,
229
230    /// Token program.
231    pub token_program: Program<'info, Token>,
232}
233
234#[derive(Accounts)]
235pub struct SetMintAuthority<'info> {
236    pub auth: Auth<'info>,
237    /// CHECK: This is actually checked
238    #[account(address = PROXY_MINT_AUTHORITY)]
239    pub proxy_mint_authority: UncheckedAccount<'info>,
240    #[account(mut)]
241    pub token_mint: Account<'info, Mint>,
242    /// The [Token] program.
243    pub token_program: Program<'info, Token>,
244}
245
246/// Adds a minter.
247#[derive(Accounts)]
248pub struct MinterAdd<'info> {
249    /// Owner of the mint proxy.
250    pub auth: Auth<'info>,
251
252    /// Account to authorize as a minter.
253    /// CHECK: Arbitrary.
254    pub minter: UncheckedAccount<'info>,
255
256    /// Information about the minter.
257    #[account(
258        init,
259        seeds = [
260            b"anchor".as_ref(),
261            minter.key().as_ref()
262        ],
263        bump,
264        space = 8 + MinterInfo::LEN,
265        payer = payer
266    )]
267    pub minter_info: Account<'info, MinterInfo>,
268
269    /// Payer for creating the minter.
270    #[account(mut)]
271    pub payer: Signer<'info>,
272
273    /// Rent sysvar.
274    pub rent: Sysvar<'info, Rent>,
275
276    /// System program.
277    pub system_program: Program<'info, System>,
278}
279
280/// Removes a minter.
281#[derive(Accounts)]
282pub struct MinterRemove<'info> {
283    /// Owner of the mint proxy.
284    pub auth: Auth<'info>,
285
286    /// Account to deauthorize as a minter.
287    /// CHECK: Arbitrary.
288    pub minter: UncheckedAccount<'info>,
289
290    /// Information about the minter.
291    #[account(mut, has_one = minter, close = payer)]
292    pub minter_info: Account<'info, MinterInfo>,
293
294    /// Account which receives the freed lamports
295    /// CHECK: Arbitrary.
296    #[account(mut)]
297    pub payer: UncheckedAccount<'info>,
298}
299
300/// Updates a minter.
301#[derive(Accounts)]
302pub struct MinterUpdate<'info> {
303    /// Owner of the mint proxy.
304    pub auth: Auth<'info>,
305    /// Information about the minter.
306    #[account(mut)]
307    pub minter_info: Account<'info, MinterInfo>,
308}
309
310/// Accounts for the perform_mint instruction.
311#[derive(Accounts)]
312pub struct PerformMint<'info> {
313    /// Mint authority of the proxy.
314    /// CHECK: Checked by Vipers.
315    pub proxy_mint_authority: UncheckedAccount<'info>,
316
317    /// Minter.
318    pub minter: Signer<'info>,
319
320    /// Token mint.
321    #[account(mut)]
322    pub token_mint: Account<'info, Mint>,
323
324    /// Destination account for minted tokens.
325    #[account(mut)]
326    pub destination: Account<'info, TokenAccount>,
327
328    /// Minter information.
329    #[account(mut, has_one = minter)]
330    pub minter_info: Account<'info, MinterInfo>,
331
332    /// SPL Token program.
333    pub token_program: Program<'info, Token>,
334}
335
336impl<'info> PerformMint<'info> {
337    fn validate(&self, state: &MintProxy) -> Result<()> {
338        assert_keys_eq!(self.proxy_mint_authority, PROXY_MINT_AUTHORITY);
339        require!(self.minter.is_signer, Unauthorized);
340        assert_keys_eq!(self.minter_info.minter, self.minter, Unauthorized);
341
342        assert_keys_eq!(state.token_mint, self.token_mint);
343
344        Ok(())
345    }
346}
347
348/// One who can mint.
349#[account]
350#[derive(Default)]
351pub struct MinterInfo {
352    /// Address that can mint.
353    pub minter: Pubkey,
354    /// Limit of number of tokens that this minter can mint.
355    /// Useful for guarded launch.
356    pub allowance: u64,
357    /// Nonce field to the struct to hold the bump seed for the program derived address,
358    /// sourced from `<https://github.com/project-serum/anchor/blob/ec6888a3b9f702bc41bd3266e7dd70116df3549c/lang/attribute/account/src/lib.rs#L220-L221.>`.
359    __nonce: u8,
360}
361
362impl MinterInfo {
363    pub const LEN: usize = PUBKEY_BYTES + 8 + 1;
364}
365
366/// Information about the mint proxy.
367/// Duplicate struct generated for the SDK to use.
368#[account]
369#[derive(Default)]
370pub struct MintProxyInfo {
371    /// Nonce for allowing the proxy mint authority to sign.
372    pub nonce: u8,
373    /// Maximum number of tokens that can be issued.
374    pub hard_cap: u64,
375    /// Account which is the authority over minted tokens.
376    pub proxy_mint_authority: Pubkey,
377    /// Owner account which can perform admin operations.
378    pub owner: Pubkey,
379    /// Next owner account.
380    pub pending_owner: Pubkey,
381    /// Account key of the state struct.
382    pub state_associated_account: Pubkey,
383    /// Mint of the token to be minted
384    pub token_mint: Pubkey,
385}
386
387/// Ensures the function is only called by the owner of the mint proxy.
388fn only_owner(state: &MintProxy, auth: &Auth) -> Result<()> {
389    require!(
390        auth.owner.is_signer && state.owner == *auth.owner.key,
391        Unauthorized
392    );
393    Ok(())
394}
395
396/// Sets the mint authority.
397fn new_set_authority_cpi_context<'a, 'b, 'c, 'info>(
398    current_authority: &AccountInfo<'info>,
399    mint: &AccountInfo<'info>,
400    token_program: &AccountInfo<'info>,
401) -> CpiContext<'a, 'b, 'c, 'info, SetAuthority<'info>> {
402    let cpi_accounts = SetAuthority {
403        account_or_mint: mint.clone(),
404        current_authority: current_authority.clone(),
405    };
406    let cpi_program = token_program.clone();
407    CpiContext::new(cpi_program, cpi_accounts)
408}
409
410/// Errors
411#[error_code]
412pub enum ErrorCode {
413    #[msg("You are not authorized to perform this action.")]
414    Unauthorized,
415    #[msg("Cannot mint over hard cap.")]
416    HardcapExceeded,
417    #[msg("Provided token mint has a freeze authority")]
418    InvalidFreezeAuthority,
419    #[msg("Provided token mint was invalid.")]
420    InvalidTokenMint,
421    #[msg("Provided proxy authority was invalid.")]
422    InvalidProxyAuthority,
423    #[msg("Not enough remaining accounts in relay context.")]
424    NotEnoughAccounts,
425    #[msg("Whitelist entry already exists.")]
426    WhitelistEntryAlreadyExists,
427    #[msg("Whitelist entry not found.")]
428    WhitelistEntryNotFound,
429    #[msg("Whitelist is full.")]
430    WhitelistFull,
431    #[msg("Invalid token program ID.")]
432    TokenProgramIDMismatch,
433    #[msg("Pending owner mismatch.")]
434    PendingOwnerMismatch,
435    #[msg("Minter allowance exceeded.")]
436    MinterAllowanceExceeded,
437    #[msg("U64 overflow.")]
438    U64Overflow,
439}