#![deny(clippy::unwrap_used)]
#![deny(rustdoc::all)]
#![allow(rustdoc::missing_doc_code_examples)]
use anchor_lang::{prelude::*, solana_program::pubkey::PUBKEY_BYTES};
use anchor_spl::token::{Mint, Token, TokenAccount};
use continuation_router::{ActionType, RouterActionProcessor};
use vipers::prelude::*;
use vipers::program_err;
mod events;
mod transfer;
pub use events::*;
declare_id!("DecZY86MU5Gj7kppfUCEmd4LbXXuyZH1yHaP2NTqdiZB");
#[allow(deprecated)]
#[program]
pub mod add_decimals {
use super::*;
#[access_control(ctx.accounts.validate())]
pub fn initialize_wrapper(ctx: Context<InitializeWrapper>, _nonce: u8) -> Result<()> {
let decimals = ctx.accounts.wrapper_mint.decimals;
require!(
decimals >= ctx.accounts.underlying_mint.decimals,
InitWrapperDecimalsTooLow
);
let added_decimals =
unwrap_int!(decimals.checked_sub(ctx.accounts.underlying_mint.decimals));
let multiplier = unwrap_int!(10u64.checked_pow(added_decimals as u32));
let wrapper = &mut ctx.accounts.wrapper;
wrapper.__nonce = unwrap_bump!(ctx, "wrapper");
wrapper.decimals = decimals;
wrapper.multiplier = multiplier;
wrapper.wrapper_underlying_mint = ctx.accounts.underlying_mint.key();
wrapper.wrapper_underlying_tokens = ctx.accounts.wrapper_underlying_tokens.key();
wrapper.wrapper_mint = ctx.accounts.wrapper_mint.key();
emit!(InitEvent {
payer: ctx.accounts.payer.key(),
decimals,
multiplier,
wrapper_underlying_mint: wrapper.wrapper_underlying_mint,
wrapper_underlying_tokens: wrapper.wrapper_underlying_tokens,
wrapper_mint: wrapper.wrapper_mint,
});
Ok(())
}
#[access_control(ctx.accounts.validate())]
pub fn deposit(ctx: Context<UserStake>, deposit_amount: u64) -> Result<()> {
require!(deposit_amount > 0, ZeroAmount);
require!(
ctx.accounts.user_underlying_tokens.amount >= deposit_amount,
InsufficientUnderlyingBalance
);
let mint_amount = unwrap_int!(ctx.accounts.wrapper.to_wrapped_amount(deposit_amount));
ctx.accounts.deposit_underlying(deposit_amount)?;
ctx.accounts.mint_wrapped(mint_amount)?;
emit!(DepositEvent {
owner: ctx.accounts.user_underlying_tokens.owner,
underlying_mint: ctx.accounts.user_underlying_tokens.mint,
wrapped_mint: ctx.accounts.user_wrapped_tokens.mint,
deposit_amount,
mint_amount
});
Ok(())
}
#[access_control(ctx.accounts.validate())]
pub fn withdraw(ctx: Context<UserStake>, max_burn_amount: u64) -> Result<()> {
require!(max_burn_amount > 0, ZeroAmount);
require!(
ctx.accounts.user_wrapped_tokens.amount >= max_burn_amount,
InsufficientWrappedBalance
);
let withdraw_amount =
unwrap_int!(ctx.accounts.wrapper.to_underlying_amount(max_burn_amount),);
let burn_amount = unwrap_int!(ctx.accounts.wrapper.to_wrapped_amount(withdraw_amount),);
let dust_amount = unwrap_int!(max_burn_amount.checked_sub(burn_amount));
ctx.accounts.burn_wrapped(burn_amount)?;
ctx.accounts.withdraw_underlying(withdraw_amount)?;
emit!(WithdrawEvent {
owner: ctx.accounts.user_underlying_tokens.owner,
underlying_mint: ctx.accounts.user_underlying_tokens.mint,
wrapped_mint: ctx.accounts.user_wrapped_tokens.mint,
withdraw_amount,
burn_amount,
dust_amount,
});
Ok(())
}
pub fn withdraw_all(ctx: Context<UserStake>) -> Result<()> {
let max_burn_amount = ctx.accounts.user_wrapped_tokens.amount;
withdraw(ctx, max_burn_amount)
}
#[state]
pub struct AddDecimals;
impl<'info> RouterActionProcessor<'info, UserStake<'info>> for AddDecimals {
fn process_action(
ctx: Context<UserStake>,
action: u16,
amount_in: u64,
_minimum_amount_out: u64,
) -> Result<()> {
let action_type = try_or_err!(ActionType::try_from(action), UnknownAction);
msg!("Router action received: {:?}", action_type);
match action_type {
ActionType::ADWithdraw => withdraw(ctx, amount_in),
ActionType::ADDeposit => deposit(ctx, amount_in),
_ => program_err!(UnknownAction),
}
}
}
}
#[derive(Accounts)]
pub struct InitializeWrapper<'info> {
#[account(
init,
seeds = [
b"anchor".as_ref(),
underlying_mint.to_account_info().key.as_ref(),
&[wrapper_mint.decimals]
],
bump,
space = 8 + WrappedToken::LEN,
payer = payer
)]
pub wrapper: Account<'info, WrappedToken>,
pub wrapper_underlying_tokens: Account<'info, TokenAccount>,
pub underlying_mint: Account<'info, Mint>,
pub wrapper_mint: Account<'info, Mint>,
#[account(mut)]
pub payer: Signer<'info>,
pub rent: Sysvar<'info, Rent>,
pub system_program: Program<'info, System>,
}
impl<'info> InitializeWrapper<'info> {
pub fn validate(&self) -> Result<()> {
require!(
self.wrapper_underlying_tokens.amount == 0,
InitNonEmptyAccount
);
assert_keys_eq!(
self.wrapper_underlying_tokens.owner,
self.wrapper,
InitWrapperUnderlyingOwnerMismatch
);
assert_keys_eq!(
self.wrapper_underlying_tokens.mint,
self.underlying_mint,
InitWrapperUnderlyingMintMismatch
);
invariant!(self.wrapper_underlying_tokens.delegate.is_none());
invariant!(self.wrapper_underlying_tokens.close_authority.is_none());
assert_keys_eq!(
self.wrapper_mint.mint_authority.unwrap(),
self.wrapper,
InitMintAuthorityMismatch
);
assert_keys_eq!(
self.wrapper_mint.freeze_authority.unwrap(),
self.wrapper,
InitFreezeAuthorityMismatch
);
require!(self.wrapper_mint.supply == 0, InitWrapperSupplyNonZero);
Ok(())
}
}
#[derive(Accounts)]
pub struct UserStake<'info> {
pub wrapper: Account<'info, WrappedToken>,
#[account(mut)]
pub wrapper_mint: Account<'info, Mint>,
#[account(mut)]
pub wrapper_underlying_tokens: Account<'info, TokenAccount>,
pub owner: Signer<'info>,
#[account(mut)]
pub user_underlying_tokens: Account<'info, TokenAccount>,
#[account(mut)]
pub user_wrapped_tokens: Account<'info, TokenAccount>,
pub token_program: Program<'info, Token>,
}
impl<'info> Validate<'info> for UserStake<'info> {
fn validate(&self) -> Result<()> {
assert_keys_eq!(self.wrapper.wrapper_mint, self.wrapper_mint);
assert_keys_eq!(
self.wrapper.wrapper_underlying_tokens,
self.wrapper_underlying_tokens
);
assert_keys_eq!(self.user_underlying_tokens.owner, self.owner);
assert_keys_eq!(
self.user_underlying_tokens.mint,
self.wrapper.wrapper_underlying_mint
);
assert_keys_eq!(self.user_wrapped_tokens.owner, self.owner);
assert_keys_eq!(self.user_wrapped_tokens.mint, self.wrapper_mint);
Ok(())
}
}
#[account]
#[derive(Copy, Debug, Default)]
pub struct WrappedToken {
pub decimals: u8,
pub multiplier: u64,
pub wrapper_underlying_mint: Pubkey,
pub wrapper_underlying_tokens: Pubkey,
pub wrapper_mint: Pubkey,
__nonce: u8,
}
impl WrappedToken {
pub const LEN: usize = 1 + 8 + PUBKEY_BYTES * 3 + 1;
pub fn to_wrapped_amount(&self, amount: u64) -> Option<u64> {
self.multiplier.checked_mul(amount)
}
pub fn to_underlying_amount(&self, amount: u64) -> Option<u64> {
amount.checked_div(self.multiplier)
}
pub fn nonce(&self) -> u8 {
self.__nonce
}
}
#[error_code]
#[derive(Eq, PartialEq)]
pub enum ErrorCode {
#[msg("Wrapper underlying tokens account must be empty.")]
InitNonEmptyAccount,
#[msg("Supply of the wrapper mint is non-zero")]
InitWrapperSupplyNonZero,
#[msg("Owner of the wrapper underlying tokens account must be the wrapper")]
InitWrapperUnderlyingOwnerMismatch,
#[msg("Underlying mint does not match underlying tokens account mint")]
InitWrapperUnderlyingMintMismatch,
#[msg("Mint authority mismatch")]
InitMintAuthorityMismatch,
#[msg("Initial decimals too high")]
InitMultiplierOverflow,
#[msg("The number of target decimals must be greater than or equal to the underlying asset's decimals.")]
InitWrapperDecimalsTooLow,
#[msg("Mint amount overflow. This error happens when the token cannot support this many decimals added to the token.")]
MintAmountOverflow,
#[msg("Failed to convert burn amount from withdraw amount.")]
InvalidBurnAmount,
#[msg("Failed to convert withdraw amount from wrapped amount.")]
InvalidWithdrawAmount,
#[msg("User does not have enough underlying tokens")]
InsufficientUnderlyingBalance,
#[msg("User does not have enough wrapped tokens")]
InsufficientWrappedBalance,
#[msg("Cannot send zero tokens")]
ZeroAmount,
#[msg("Unknown router action")]
UnknownAction,
#[msg("Freeze authority mismatch")]
InitFreezeAuthorityMismatch,
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod test {
use super::*;
use proptest::prelude::*;
const MAX_TOKEN_DECIMALS: u8 = 9;
proptest! {
#[test]
fn test_wrapped_token(
nonce in 0..u8::MAX,
amount in 0..u64::MAX,
(underlying, desired) in underlying_and_desired(),
) {
let added_decimals = desired - underlying;
let multiplier = 10u64.checked_pow(added_decimals as u32);
prop_assume!(multiplier.is_some());
let wrapped_token = WrappedToken {
__nonce: nonce,
decimals: desired,
multiplier: multiplier.unwrap(),
wrapper_underlying_mint: Pubkey::default(),
wrapper_underlying_tokens: Pubkey::default(),
wrapper_mint: Pubkey::default(),
};
let wrapped_amount = wrapped_token.to_wrapped_amount(amount);
if wrapped_amount.is_some() {
assert_eq!(wrapped_amount.unwrap() / amount, wrapped_token.multiplier);
assert_eq!(wrapped_token.to_underlying_amount(wrapped_amount.unwrap()).unwrap(), amount);
}
}
}
prop_compose! {
fn underlying_and_desired()
(desired in 0..=MAX_TOKEN_DECIMALS)
(underlying in 0..=desired, desired in Just(desired)) -> (u8, u8) {
(underlying, desired)
}
}
}