#![deny(rustdoc::all)]
#![allow(rustdoc::missing_doc_code_examples)]
mod account_validators;
mod macros;
pub mod events;
pub mod state;
use anchor_lang::prelude::*;
use anchor_lang::solana_program;
use anchor_spl::token::{self, Mint, Token, TokenAccount};
use static_pubkey::static_pubkey;
use vipers::prelude::*;
use events::*;
pub use state::*;
declare_id!("CRATwLpu6YZEeiVq9ajjxs61wPQ9f29s1UoQR9siJCRs");
pub static FEE_TO_ADDRESS: Pubkey = static_pubkey!("AAqAKWdsUPepSgXf7Msbp1pQ7yCPgYkBvXmNfTFBGAqp");
pub static ISSUE_FEE_BPS: u16 = 2_000;
pub static WITHDRAW_FEE_BPS: u16 = 2_000;
pub const MAX_FEE_BPS: u16 = 10_000;
#[program]
pub mod crate_token {
use super::*;
#[access_control(ctx.accounts.validate())]
pub fn new_crate(ctx: Context<NewCrate>, _bump: u8) -> Result<()> {
let info = &mut ctx.accounts.crate_token;
info.mint = ctx.accounts.crate_mint.key();
info.bump = unwrap_bump!(ctx, "crate_token");
info.fee_to_setter = ctx.accounts.fee_to_setter.key();
info.fee_setter_authority = ctx.accounts.fee_setter_authority.key();
info.issue_authority = ctx.accounts.issue_authority.key();
info.withdraw_authority = ctx.accounts.withdraw_authority.key();
info.author_fee_to = ctx.accounts.author_fee_to.key();
info.issue_fee_bps = 0;
info.withdraw_fee_bps = 0;
emit!(NewCrateEvent {
issue_authority: ctx.accounts.issue_authority.key(),
withdraw_authority: ctx.accounts.withdraw_authority.key(),
crate_key: ctx.accounts.crate_token.key(),
});
Ok(())
}
#[access_control(ctx.accounts.validate())]
pub fn set_issue_fee(ctx: Context<SetFees>, issue_fee_bps: u16) -> Result<()> {
invariant!(issue_fee_bps <= MAX_FEE_BPS, MaxFeeExceeded);
let crate_token = &mut ctx.accounts.crate_token;
crate_token.issue_fee_bps = issue_fee_bps;
Ok(())
}
#[access_control(ctx.accounts.validate())]
pub fn set_withdraw_fee(ctx: Context<SetFees>, withdraw_fee_bps: u16) -> Result<()> {
invariant!(withdraw_fee_bps <= MAX_FEE_BPS, MaxFeeExceeded);
let crate_token = &mut ctx.accounts.crate_token;
crate_token.withdraw_fee_bps = withdraw_fee_bps;
Ok(())
}
#[access_control(ctx.accounts.validate())]
pub fn set_fee_to(ctx: Context<SetFeeTo>) -> Result<()> {
let crate_token = &mut ctx.accounts.crate_token;
crate_token.author_fee_to = ctx.accounts.author_fee_to.key();
Ok(())
}
#[access_control(ctx.accounts.validate())]
pub fn set_fee_to_setter(ctx: Context<SetFeeToSetter>) -> Result<()> {
let crate_token = &mut ctx.accounts.crate_token;
crate_token.fee_to_setter = ctx.accounts.next_fee_to_setter.key();
Ok(())
}
#[access_control(ctx.accounts.validate())]
pub fn issue(ctx: Context<Issue>, amount: u64) -> Result<()> {
if amount == 0 {
return Ok(());
}
let seeds: &[&[u8]] = gen_crate_signer_seeds!(ctx.accounts.crate_token);
let crate_token = &ctx.accounts.crate_token;
let state::Fees {
amount,
author_fee,
protocol_fee,
} = crate_token.apply_issue_fee(amount)?;
token::mint_to(
CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
token::MintTo {
mint: ctx.accounts.crate_mint.to_account_info(),
to: ctx.accounts.mint_destination.to_account_info(),
authority: ctx.accounts.crate_token.to_account_info(),
},
&[seeds],
),
amount,
)?;
if author_fee > 0 {
token::mint_to(
CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
token::MintTo {
mint: ctx.accounts.crate_mint.to_account_info(),
to: ctx.accounts.author_fee_destination.to_account_info(),
authority: ctx.accounts.crate_token.to_account_info(),
},
&[seeds],
),
author_fee,
)?;
}
if protocol_fee > 0 {
token::mint_to(
CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
token::MintTo {
mint: ctx.accounts.crate_mint.to_account_info(),
to: ctx.accounts.protocol_fee_destination.to_account_info(),
authority: ctx.accounts.crate_token.to_account_info(),
},
&[seeds],
),
protocol_fee,
)?;
}
emit!(IssueEvent {
crate_key: ctx.accounts.crate_token.key(),
destination: ctx.accounts.mint_destination.key(),
amount,
author_fee,
protocol_fee
});
Ok(())
}
#[access_control(ctx.accounts.validate())]
pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
if amount == 0 {
return Ok(());
}
let token_program = ctx.accounts.token_program.to_account_info();
let seeds = gen_crate_signer_seeds!(ctx.accounts.crate_token);
let signer_seeds: &[&[&[u8]]] = &[seeds];
let crate_token = &ctx.accounts.crate_token;
let state::Fees {
amount,
author_fee,
protocol_fee,
} = crate_token.apply_withdraw_fee(amount)?;
token::transfer(
CpiContext::new_with_signer(
token_program.clone(),
token::Transfer {
from: ctx.accounts.crate_underlying.to_account_info(),
to: ctx.accounts.withdraw_destination.to_account_info(),
authority: ctx.accounts.crate_token.to_account_info(),
},
signer_seeds,
),
amount,
)?;
if author_fee > 0 {
token::transfer(
CpiContext::new_with_signer(
token_program.clone(),
token::Transfer {
from: ctx.accounts.crate_underlying.to_account_info(),
to: ctx.accounts.author_fee_destination.to_account_info(),
authority: ctx.accounts.crate_token.to_account_info(),
},
signer_seeds,
),
author_fee,
)?;
}
if protocol_fee > 0 {
token::transfer(
CpiContext::new_with_signer(
token_program.clone(),
token::Transfer {
from: ctx.accounts.crate_underlying.to_account_info(),
to: ctx.accounts.protocol_fee_destination.to_account_info(),
authority: ctx.accounts.crate_token.to_account_info(),
},
signer_seeds,
),
protocol_fee,
)?;
}
emit!(WithdrawEvent {
crate_key: ctx.accounts.crate_token.key(),
token: ctx.accounts.crate_underlying.mint,
destination: ctx.accounts.withdraw_destination.key(),
amount,
author_fee,
protocol_fee,
});
Ok(())
}
}
#[derive(Accounts)]
pub struct NewCrate<'info> {
#[account(
init,
seeds = [
b"CrateToken".as_ref(),
crate_mint.key().to_bytes().as_ref()
],
bump,
space = 8 + CrateToken::LEN,
payer = payer
)]
pub crate_token: Account<'info, CrateToken>,
pub crate_mint: Account<'info, Mint>,
pub fee_to_setter: UncheckedAccount<'info>,
pub fee_setter_authority: UncheckedAccount<'info>,
pub issue_authority: UncheckedAccount<'info>,
pub withdraw_authority: UncheckedAccount<'info>,
pub author_fee_to: UncheckedAccount<'info>,
#[account(mut)]
pub payer: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
#[instruction(bump: u8)]
pub struct SetFees<'info> {
#[account(mut)]
pub crate_token: Account<'info, CrateToken>,
pub fee_setter: Signer<'info>,
}
#[derive(Accounts)]
#[instruction(bump: u8)]
pub struct SetFeeTo<'info> {
#[account(mut)]
pub crate_token: Account<'info, CrateToken>,
pub fee_to_setter: Signer<'info>,
pub author_fee_to: UncheckedAccount<'info>,
}
#[derive(Accounts)]
#[instruction(bump: u8)]
pub struct SetFeeToSetter<'info> {
#[account(mut)]
pub crate_token: Account<'info, CrateToken>,
pub fee_to_setter: Signer<'info>,
pub next_fee_to_setter: UncheckedAccount<'info>,
}
#[derive(Accounts)]
pub struct Issue<'info> {
pub crate_token: Account<'info, CrateToken>,
#[account(mut)]
pub crate_mint: Account<'info, Mint>,
pub issue_authority: Signer<'info>,
#[account(mut)]
pub mint_destination: Account<'info, TokenAccount>,
#[account(mut)]
pub author_fee_destination: Account<'info, TokenAccount>,
#[account(mut)]
pub protocol_fee_destination: Account<'info, TokenAccount>,
pub token_program: Program<'info, Token>,
}
#[derive(Accounts)]
pub struct Withdraw<'info> {
pub crate_token: Account<'info, CrateToken>,
#[account(mut)]
pub crate_underlying: Account<'info, TokenAccount>,
pub withdraw_authority: Signer<'info>,
#[account(mut)]
pub withdraw_destination: Account<'info, TokenAccount>,
#[account(mut)]
pub author_fee_destination: Account<'info, TokenAccount>,
#[account(mut)]
pub protocol_fee_destination: Account<'info, TokenAccount>,
pub token_program: Program<'info, Token>,
}
#[error_code]
pub enum ErrorCode {
#[msg("Maximum fee exceeded.")]
MaxFeeExceeded,
#[msg("Freeze authority must either be the issuer or the Crate itself.")]
InvalidFreezeAuthority,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fee_to_address() {
let (key, bump) = Pubkey::find_program_address(&[b"CrateFees"], &crate::ID);
assert_eq!(key, FEE_TO_ADDRESS);
assert_eq!(bump, 254);
}
}