1#![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
13pub const PROXY_STATE_ACCOUNT: Pubkey =
15 static_pubkey::static_pubkey!("9qRjwMQYrkd5JvsENaYYxSCgwEuVhK4qAo5kCFHSmdmL");
16
17pub const PROXY_MINT_AUTHORITY: Pubkey =
19 static_pubkey::static_pubkey!("GyktbGXbH9kvxP8RGfWsnFtuRgC7QCQo2WBqpo3ryk7L");
20
21#[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 pub nonce: u8,
54 pub hard_cap: u64,
56 pub proxy_mint_authority: Pubkey,
58 pub owner: Pubkey,
60 pub pending_owner: Pubkey,
62 pub state_associated_account: Pubkey,
64 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 #[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 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 #[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 #[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 #[access_control(only_owner(self, &ctx.accounts.auth))]
147 pub fn minter_remove(&self, ctx: Context<MinterRemove>) -> Result<()> {
148 Ok(())
149 }
150
151 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 #[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 pub mint_authority: Signer<'info>,
216
217 #[account(address = PROXY_MINT_AUTHORITY)]
220 pub proxy_mint_authority: UncheckedAccount<'info>,
221
222 pub owner: UncheckedAccount<'info>,
225
226 #[account(mut)]
228 pub token_mint: Account<'info, Mint>,
229
230 pub token_program: Program<'info, Token>,
232}
233
234#[derive(Accounts)]
235pub struct SetMintAuthority<'info> {
236 pub auth: Auth<'info>,
237 #[account(address = PROXY_MINT_AUTHORITY)]
239 pub proxy_mint_authority: UncheckedAccount<'info>,
240 #[account(mut)]
241 pub token_mint: Account<'info, Mint>,
242 pub token_program: Program<'info, Token>,
244}
245
246#[derive(Accounts)]
248pub struct MinterAdd<'info> {
249 pub auth: Auth<'info>,
251
252 pub minter: UncheckedAccount<'info>,
255
256 #[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 #[account(mut)]
271 pub payer: Signer<'info>,
272
273 pub rent: Sysvar<'info, Rent>,
275
276 pub system_program: Program<'info, System>,
278}
279
280#[derive(Accounts)]
282pub struct MinterRemove<'info> {
283 pub auth: Auth<'info>,
285
286 pub minter: UncheckedAccount<'info>,
289
290 #[account(mut, has_one = minter, close = payer)]
292 pub minter_info: Account<'info, MinterInfo>,
293
294 #[account(mut)]
297 pub payer: UncheckedAccount<'info>,
298}
299
300#[derive(Accounts)]
302pub struct MinterUpdate<'info> {
303 pub auth: Auth<'info>,
305 #[account(mut)]
307 pub minter_info: Account<'info, MinterInfo>,
308}
309
310#[derive(Accounts)]
312pub struct PerformMint<'info> {
313 pub proxy_mint_authority: UncheckedAccount<'info>,
316
317 pub minter: Signer<'info>,
319
320 #[account(mut)]
322 pub token_mint: Account<'info, Mint>,
323
324 #[account(mut)]
326 pub destination: Account<'info, TokenAccount>,
327
328 #[account(mut, has_one = minter)]
330 pub minter_info: Account<'info, MinterInfo>,
331
332 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#[account]
350#[derive(Default)]
351pub struct MinterInfo {
352 pub minter: Pubkey,
354 pub allowance: u64,
357 __nonce: u8,
360}
361
362impl MinterInfo {
363 pub const LEN: usize = PUBKEY_BYTES + 8 + 1;
364}
365
366#[account]
369#[derive(Default)]
370pub struct MintProxyInfo {
371 pub nonce: u8,
373 pub hard_cap: u64,
375 pub proxy_mint_authority: Pubkey,
377 pub owner: Pubkey,
379 pub pending_owner: Pubkey,
381 pub state_associated_account: Pubkey,
383 pub token_mint: Pubkey,
385}
386
387fn 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
396fn 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#[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}