1#![deny(rustdoc::all)]
3#![allow(rustdoc::missing_doc_code_examples)]
4
5mod account_validators;
6mod macros;
7
8pub mod events;
9pub mod state;
10
11use anchor_lang::prelude::*;
12use anchor_lang::solana_program;
13use anchor_spl::token::{self, Mint, Token, TokenAccount};
14use static_pubkey::static_pubkey;
15use vipers::prelude::*;
16
17use events::*;
18pub use state::*;
19
20declare_id!("CRATwLpu6YZEeiVq9ajjxs61wPQ9f29s1UoQR9siJCRs");
21
22pub static FEE_TO_ADDRESS: Pubkey = static_pubkey!("AAqAKWdsUPepSgXf7Msbp1pQ7yCPgYkBvXmNfTFBGAqp");
24
25pub static ISSUE_FEE_BPS: u16 = 2_000;
27
28pub static WITHDRAW_FEE_BPS: u16 = 2_000;
30
31pub const MAX_FEE_BPS: u16 = 10_000;
33
34#[program]
36pub mod crate_token {
37 use super::*;
38
39 #[access_control(ctx.accounts.validate())]
41 pub fn new_crate(ctx: Context<NewCrate>, _bump: u8) -> Result<()> {
42 let info = &mut ctx.accounts.crate_token;
43 info.mint = ctx.accounts.crate_mint.key();
44 info.bump = unwrap_bump!(ctx, "crate_token");
45
46 info.fee_to_setter = ctx.accounts.fee_to_setter.key();
47 info.fee_setter_authority = ctx.accounts.fee_setter_authority.key();
48 info.issue_authority = ctx.accounts.issue_authority.key();
49 info.withdraw_authority = ctx.accounts.withdraw_authority.key();
50 info.author_fee_to = ctx.accounts.author_fee_to.key();
51
52 info.issue_fee_bps = 0;
53 info.withdraw_fee_bps = 0;
54
55 emit!(NewCrateEvent {
56 issue_authority: ctx.accounts.issue_authority.key(),
57 withdraw_authority: ctx.accounts.withdraw_authority.key(),
58 crate_key: ctx.accounts.crate_token.key(),
59 });
60
61 Ok(())
62 }
63
64 #[access_control(ctx.accounts.validate())]
67 pub fn set_issue_fee(ctx: Context<SetFees>, issue_fee_bps: u16) -> Result<()> {
68 invariant!(issue_fee_bps <= MAX_FEE_BPS, MaxFeeExceeded);
69 let crate_token = &mut ctx.accounts.crate_token;
70 crate_token.issue_fee_bps = issue_fee_bps;
71 Ok(())
72 }
73
74 #[access_control(ctx.accounts.validate())]
77 pub fn set_withdraw_fee(ctx: Context<SetFees>, withdraw_fee_bps: u16) -> Result<()> {
78 invariant!(withdraw_fee_bps <= MAX_FEE_BPS, MaxFeeExceeded);
79 let crate_token = &mut ctx.accounts.crate_token;
80 crate_token.withdraw_fee_bps = withdraw_fee_bps;
81 Ok(())
82 }
83
84 #[access_control(ctx.accounts.validate())]
87 pub fn set_fee_to(ctx: Context<SetFeeTo>) -> Result<()> {
88 let crate_token = &mut ctx.accounts.crate_token;
89 crate_token.author_fee_to = ctx.accounts.author_fee_to.key();
90 Ok(())
91 }
92
93 #[access_control(ctx.accounts.validate())]
96 pub fn set_fee_to_setter(ctx: Context<SetFeeToSetter>) -> Result<()> {
97 let crate_token = &mut ctx.accounts.crate_token;
98 crate_token.fee_to_setter = ctx.accounts.next_fee_to_setter.key();
99 Ok(())
100 }
101
102 #[access_control(ctx.accounts.validate())]
105 pub fn issue(ctx: Context<Issue>, amount: u64) -> Result<()> {
106 if amount == 0 {
108 return Ok(());
109 }
110
111 let seeds: &[&[u8]] = gen_crate_signer_seeds!(ctx.accounts.crate_token);
112 let crate_token = &ctx.accounts.crate_token;
113 let state::Fees {
114 amount,
115 author_fee,
116 protocol_fee,
117 } = crate_token.apply_issue_fee(amount)?;
118
119 token::mint_to(
120 CpiContext::new_with_signer(
121 ctx.accounts.token_program.to_account_info(),
122 token::MintTo {
123 mint: ctx.accounts.crate_mint.to_account_info(),
124 to: ctx.accounts.mint_destination.to_account_info(),
125 authority: ctx.accounts.crate_token.to_account_info(),
126 },
127 &[seeds],
128 ),
129 amount,
130 )?;
131
132 if author_fee > 0 {
133 token::mint_to(
134 CpiContext::new_with_signer(
135 ctx.accounts.token_program.to_account_info(),
136 token::MintTo {
137 mint: ctx.accounts.crate_mint.to_account_info(),
138 to: ctx.accounts.author_fee_destination.to_account_info(),
139 authority: ctx.accounts.crate_token.to_account_info(),
140 },
141 &[seeds],
142 ),
143 author_fee,
144 )?;
145 }
146
147 if protocol_fee > 0 {
148 token::mint_to(
149 CpiContext::new_with_signer(
150 ctx.accounts.token_program.to_account_info(),
151 token::MintTo {
152 mint: ctx.accounts.crate_mint.to_account_info(),
153 to: ctx.accounts.protocol_fee_destination.to_account_info(),
154 authority: ctx.accounts.crate_token.to_account_info(),
155 },
156 &[seeds],
157 ),
158 protocol_fee,
159 )?;
160 }
161
162 emit!(IssueEvent {
163 crate_key: ctx.accounts.crate_token.key(),
164 destination: ctx.accounts.mint_destination.key(),
165 amount,
166 author_fee,
167 protocol_fee
168 });
169
170 Ok(())
171 }
172
173 #[access_control(ctx.accounts.validate())]
176 pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
177 if amount == 0 {
179 return Ok(());
180 }
181
182 let token_program = ctx.accounts.token_program.to_account_info();
183 let seeds = gen_crate_signer_seeds!(ctx.accounts.crate_token);
184 let signer_seeds: &[&[&[u8]]] = &[seeds];
185 let crate_token = &ctx.accounts.crate_token;
186 let state::Fees {
187 amount,
188 author_fee,
189 protocol_fee,
190 } = crate_token.apply_withdraw_fee(amount)?;
191
192 token::transfer(
194 CpiContext::new_with_signer(
195 token_program.clone(),
196 token::Transfer {
197 from: ctx.accounts.crate_underlying.to_account_info(),
198 to: ctx.accounts.withdraw_destination.to_account_info(),
199 authority: ctx.accounts.crate_token.to_account_info(),
200 },
201 signer_seeds,
202 ),
203 amount,
204 )?;
205
206 if author_fee > 0 {
207 token::transfer(
208 CpiContext::new_with_signer(
209 token_program.clone(),
210 token::Transfer {
211 from: ctx.accounts.crate_underlying.to_account_info(),
212 to: ctx.accounts.author_fee_destination.to_account_info(),
213 authority: ctx.accounts.crate_token.to_account_info(),
214 },
215 signer_seeds,
216 ),
217 author_fee,
218 )?;
219 }
220
221 if protocol_fee > 0 {
222 token::transfer(
223 CpiContext::new_with_signer(
224 token_program.clone(),
225 token::Transfer {
226 from: ctx.accounts.crate_underlying.to_account_info(),
227 to: ctx.accounts.protocol_fee_destination.to_account_info(),
228 authority: ctx.accounts.crate_token.to_account_info(),
229 },
230 signer_seeds,
231 ),
232 protocol_fee,
233 )?;
234 }
235
236 emit!(WithdrawEvent {
237 crate_key: ctx.accounts.crate_token.key(),
238 token: ctx.accounts.crate_underlying.mint,
239 destination: ctx.accounts.withdraw_destination.key(),
240 amount,
241 author_fee,
242 protocol_fee,
243 });
244
245 Ok(())
246 }
247}
248
249#[derive(Accounts)]
255pub struct NewCrate<'info> {
256 #[account(
258 init,
259 seeds = [
260 b"CrateToken".as_ref(),
261 crate_mint.key().to_bytes().as_ref()
262 ],
263 bump,
264 space = 8 + CrateToken::LEN,
265 payer = payer
266 )]
267 pub crate_token: Account<'info, CrateToken>,
268
269 pub crate_mint: Account<'info, Mint>,
271
272 pub fee_to_setter: UncheckedAccount<'info>,
275
276 pub fee_setter_authority: UncheckedAccount<'info>,
279
280 pub issue_authority: UncheckedAccount<'info>,
283
284 pub withdraw_authority: UncheckedAccount<'info>,
287
288 pub author_fee_to: UncheckedAccount<'info>,
291
292 #[account(mut)]
294 pub payer: Signer<'info>,
295
296 pub system_program: Program<'info, System>,
298}
299
300#[derive(Accounts)]
302#[instruction(bump: u8)]
303pub struct SetFees<'info> {
304 #[account(mut)]
306 pub crate_token: Account<'info, CrateToken>,
307
308 pub fee_setter: Signer<'info>,
310}
311
312#[derive(Accounts)]
314#[instruction(bump: u8)]
315pub struct SetFeeTo<'info> {
316 #[account(mut)]
318 pub crate_token: Account<'info, CrateToken>,
319 pub fee_to_setter: Signer<'info>,
321 pub author_fee_to: UncheckedAccount<'info>,
324}
325
326#[derive(Accounts)]
328#[instruction(bump: u8)]
329pub struct SetFeeToSetter<'info> {
330 #[account(mut)]
332 pub crate_token: Account<'info, CrateToken>,
333 pub fee_to_setter: Signer<'info>,
335 pub next_fee_to_setter: UncheckedAccount<'info>,
338}
339
340#[derive(Accounts)]
342pub struct Issue<'info> {
343 pub crate_token: Account<'info, CrateToken>,
345
346 #[account(mut)]
348 pub crate_mint: Account<'info, Mint>,
349
350 pub issue_authority: Signer<'info>,
352
353 #[account(mut)]
355 pub mint_destination: Account<'info, TokenAccount>,
356
357 #[account(mut)]
359 pub author_fee_destination: Account<'info, TokenAccount>,
360
361 #[account(mut)]
363 pub protocol_fee_destination: Account<'info, TokenAccount>,
364
365 pub token_program: Program<'info, Token>,
367}
368
369#[derive(Accounts)]
371pub struct Withdraw<'info> {
372 pub crate_token: Account<'info, CrateToken>,
374
375 #[account(mut)]
377 pub crate_underlying: Account<'info, TokenAccount>,
378
379 pub withdraw_authority: Signer<'info>,
381
382 #[account(mut)]
384 pub withdraw_destination: Account<'info, TokenAccount>,
385
386 #[account(mut)]
388 pub author_fee_destination: Account<'info, TokenAccount>,
389
390 #[account(mut)]
392 pub protocol_fee_destination: Account<'info, TokenAccount>,
393
394 pub token_program: Program<'info, Token>,
396}
397
398#[error_code]
399pub enum ErrorCode {
401 #[msg("Maximum fee exceeded.")]
402 MaxFeeExceeded,
403 #[msg("Freeze authority must either be the issuer or the Crate itself.")]
404 InvalidFreezeAuthority,
405}
406
407#[cfg(test)]
408mod tests {
409 use super::*;
410 #[test]
411 fn test_fee_to_address() {
412 let (key, bump) = Pubkey::find_program_address(&[b"CrateFees"], &crate::ID);
413 assert_eq!(key, FEE_TO_ADDRESS);
414 assert_eq!(bump, 254);
415 }
416}