#![deny(missing_docs)]
#![deny(clippy::integer_arithmetic)]
use anchor_lang::solana_program::pubkey::PUBKEY_BYTES;
use num_traits::ToPrimitive;
use crate::*;
#[account(zero_copy)]
#[derive(Debug, Default)]
pub struct YiToken {
pub mint: Pubkey,
pub bump: u8,
pub _padding: [u8; 7],
pub underlying_token_mint: Pubkey,
pub underlying_tokens: Pubkey,
pub stake_fee_millibps: u32,
pub unstake_fee_millibps: u32,
}
impl YiToken {
pub const SIZE: usize = PUBKEY_BYTES + 1 + 7 + PUBKEY_BYTES * 2 + 4 + 4;
pub fn calculate_underlying_for_yitokens(
&self,
yitoken_amount: u64,
total_underlying_tokens: u64,
total_supply: u64,
) -> Option<u64> {
if yitoken_amount == 0 {
return Some(0);
}
if yitoken_amount > total_supply {
return None;
}
if yitoken_amount == total_supply {
return Some(total_underlying_tokens);
}
let amt_no_fee = (yitoken_amount as u128)
.checked_mul(total_underlying_tokens.into())?
.checked_div(total_supply.into())?
.to_u64()?;
if self.unstake_fee_millibps == 0 {
Some(amt_no_fee)
} else {
(amt_no_fee as u128)
.checked_mul(self.unstake_fee_millibps.into())?
.checked_div(MILLIBPS_PER_WHOLE.into())?
.to_u64()
}
}
pub fn calculate_yitokens_for_underlying(
&self,
underlying_amount: u64,
total_underlying_tokens: u64,
total_supply: u64,
) -> Option<u64> {
if underlying_amount == 0 {
return Some(0);
}
if total_underlying_tokens == 0 {
return Some(underlying_amount);
}
let amt_no_fee = (underlying_amount as u128)
.checked_mul(total_supply.into())?
.checked_div(total_underlying_tokens.into())?
.to_u64()?;
if self.stake_fee_millibps == 0 {
Some(amt_no_fee)
} else {
(amt_no_fee as u128)
.checked_mul(self.stake_fee_millibps.into())?
.checked_div(MILLIBPS_PER_WHOLE.into())?
.to_u64()
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::integer_arithmetic)]
mod tests {
use std::mem::size_of;
use super::*;
use proptest::prelude::*;
#[test]
fn test_yitoken_size() {
assert_eq!(YiToken::SIZE, size_of::<YiToken>());
}
#[test]
fn test_calculate_yitokens_for_underlying_init() {
let yi_token: YiToken = YiToken::default();
let amount = yi_token
.calculate_yitokens_for_underlying(700_000, 0, 0)
.unwrap();
assert_eq!(amount, 700_000);
}
#[test]
fn test_calculate_yitokens_for_underlying_no_fees() {
let yi_token: YiToken = YiToken::default();
let amount = yi_token
.calculate_yitokens_for_underlying(100_000, 700_000, 700_000)
.unwrap();
assert_eq!(amount, 100_000);
}
#[test]
fn test_calculate_underlying_for_yitokens_init() {
let yi_token: YiToken = YiToken::default();
let amount = yi_token.calculate_underlying_for_yitokens(700_000, 0, 0);
assert_eq!(amount, None);
}
#[test]
fn test_calculate_underlying_for_yitokens_no_fees() {
let yi_token: YiToken = YiToken::default();
let amount = yi_token
.calculate_underlying_for_yitokens(100_000, 700_000, 700_000)
.unwrap();
assert_eq!(amount, 100_000);
}
fn perform_test_cannot_increase_no_fees(
initial_underlying_tokens: u64,
initial_total_underlying_tokens: u64,
initial_total_supply: u64,
ratios: [f64; 32],
) {
let yi_token: YiToken = YiToken::default();
let mut my_yitokens = 0;
let mut my_underlying_tokens = initial_underlying_tokens;
let mut total_underlying_tokens: u64 = initial_total_underlying_tokens;
let mut total_supply: u64 = initial_total_supply;
for ratio in ratios {
match ratio.partial_cmp(&0f64) {
Some(std::cmp::Ordering::Less) => {
let underlying_stake_amount =
((my_underlying_tokens as f64) * -ratio).to_u64().unwrap();
let mint_yitokens = yi_token
.calculate_yitokens_for_underlying(
underlying_stake_amount,
total_underlying_tokens,
total_supply,
)
.unwrap();
my_yitokens += mint_yitokens;
my_underlying_tokens -= underlying_stake_amount;
total_underlying_tokens += underlying_stake_amount;
total_supply += mint_yitokens;
}
Some(std::cmp::Ordering::Greater) => {
let yitoken_amount = ((my_yitokens as f64) * ratio).to_u64().unwrap();
let withdraw_underlying_tokens = yi_token
.calculate_underlying_for_yitokens(
yitoken_amount,
total_underlying_tokens,
total_supply,
)
.unwrap();
my_yitokens -= yitoken_amount;
my_underlying_tokens += withdraw_underlying_tokens;
total_underlying_tokens -= withdraw_underlying_tokens;
total_supply -= yitoken_amount;
}
_ => {
}
}
}
assert!(my_underlying_tokens <= initial_underlying_tokens);
{
let final_yitokens = my_yitokens;
let withdraw_underlying_tokens = yi_token
.calculate_underlying_for_yitokens(
final_yitokens,
total_underlying_tokens,
total_supply,
)
.unwrap();
my_yitokens -= final_yitokens;
my_underlying_tokens += withdraw_underlying_tokens;
total_underlying_tokens -= withdraw_underlying_tokens;
total_supply -= my_yitokens;
}
assert_eq!(my_yitokens, 0);
assert!(my_underlying_tokens <= initial_underlying_tokens);
assert!(total_underlying_tokens >= initial_total_underlying_tokens);
assert!(total_supply >= initial_total_supply);
}
proptest! {
#[test]
fn cannot_increase_no_fees(
initial_underlying_tokens in 0..=u32::MAX,
initial_total_underlying_tokens in 0..=u32::MAX,
initial_total_supply in 0..=u32::MAX,
amounts in prop::array::uniform32(-1.0..=1.0)
) {
perform_test_cannot_increase_no_fees(
initial_underlying_tokens.into(),
initial_total_underlying_tokens.into(),
initial_total_supply.into(),
amounts
)
}
}
}