1#![deny(missing_docs)]
3#![deny(clippy::integer_arithmetic)]
4
5use anchor_lang::solana_program::pubkey::PUBKEY_BYTES;
6use num_traits::ToPrimitive;
7
8use crate::*;
9
10#[account(zero_copy)]
14#[derive(Debug, Default)]
15pub struct YiToken {
16 pub mint: Pubkey,
18 pub bump: u8,
20 pub _padding: [u8; 7],
22
23 pub underlying_token_mint: Pubkey,
25 pub underlying_tokens: Pubkey,
27
28 pub stake_fee_millibps: u32,
30 pub unstake_fee_millibps: u32,
32}
33
34impl YiToken {
35 pub const SIZE: usize = PUBKEY_BYTES + 1 + 7 + PUBKEY_BYTES * 2 + 4 + 4;
37
38 pub fn calculate_underlying_for_yitokens(
40 &self,
41 yitoken_amount: u64,
42 total_underlying_tokens: u64,
43 total_supply: u64,
44 ) -> Option<u64> {
45 if yitoken_amount == 0 {
46 return Some(0);
47 }
48 if yitoken_amount > total_supply {
50 return None;
51 }
52 if yitoken_amount == total_supply {
54 return Some(total_underlying_tokens);
55 }
56 let amt_no_fee = (yitoken_amount as u128)
57 .checked_mul(total_underlying_tokens.into())?
58 .checked_div(total_supply.into())?
59 .to_u64()?;
60 if self.unstake_fee_millibps == 0 {
61 Some(amt_no_fee)
62 } else {
63 (amt_no_fee as u128)
64 .checked_mul(self.unstake_fee_millibps.into())?
65 .checked_div(MILLIBPS_PER_WHOLE.into())?
66 .to_u64()
67 }
68 }
69
70 pub fn calculate_yitokens_for_underlying(
72 &self,
73 underlying_amount: u64,
74 total_underlying_tokens: u64,
75 total_supply: u64,
76 ) -> Option<u64> {
77 if underlying_amount == 0 {
78 return Some(0);
79 }
80 if total_underlying_tokens == 0 {
82 return Some(underlying_amount);
83 }
84 let amt_no_fee = (underlying_amount as u128)
85 .checked_mul(total_supply.into())?
86 .checked_div(total_underlying_tokens.into())?
87 .to_u64()?;
88 if self.stake_fee_millibps == 0 {
89 Some(amt_no_fee)
90 } else {
91 (amt_no_fee as u128)
92 .checked_mul(self.stake_fee_millibps.into())?
93 .checked_div(MILLIBPS_PER_WHOLE.into())?
94 .to_u64()
95 }
96 }
97}
98
99#[cfg(test)]
100#[allow(clippy::unwrap_used, clippy::integer_arithmetic)]
101mod tests {
102 use std::mem::size_of;
103
104 use super::*;
105 use proptest::prelude::*;
106
107 #[test]
108 fn test_yitoken_size() {
109 assert_eq!(YiToken::SIZE, size_of::<YiToken>());
110 }
111
112 #[test]
113 fn test_calculate_yitokens_for_underlying_init() {
114 let yi_token: YiToken = YiToken::default();
115
116 let amount = yi_token
117 .calculate_yitokens_for_underlying(700_000, 0, 0)
118 .unwrap();
119 assert_eq!(amount, 700_000);
120 }
121
122 #[test]
123 fn test_calculate_yitokens_for_underlying_no_fees() {
124 let yi_token: YiToken = YiToken::default();
125 let amount = yi_token
126 .calculate_yitokens_for_underlying(100_000, 700_000, 700_000)
127 .unwrap();
128 assert_eq!(amount, 100_000);
129 }
130
131 #[test]
132 fn test_calculate_underlying_for_yitokens_init() {
133 let yi_token: YiToken = YiToken::default();
134 let amount = yi_token.calculate_underlying_for_yitokens(700_000, 0, 0);
135 assert_eq!(amount, None);
136 }
137
138 #[test]
139 fn test_calculate_underlying_for_yitokens_no_fees() {
140 let yi_token: YiToken = YiToken::default();
141 let amount = yi_token
142 .calculate_underlying_for_yitokens(100_000, 700_000, 700_000)
143 .unwrap();
144 assert_eq!(amount, 100_000);
145 }
146
147 fn perform_test_cannot_increase_no_fees(
148 initial_underlying_tokens: u64,
149 initial_total_underlying_tokens: u64,
150 initial_total_supply: u64,
151 ratios: [f64; 32],
152 ) {
153 let yi_token: YiToken = YiToken::default();
154
155 let mut my_yitokens = 0;
156 let mut my_underlying_tokens = initial_underlying_tokens;
157
158 let mut total_underlying_tokens: u64 = initial_total_underlying_tokens;
159 let mut total_supply: u64 = initial_total_supply;
160
161 for ratio in ratios {
162 match ratio.partial_cmp(&0f64) {
164 Some(std::cmp::Ordering::Less) => {
165 let underlying_stake_amount =
166 ((my_underlying_tokens as f64) * -ratio).to_u64().unwrap();
167 let mint_yitokens = yi_token
168 .calculate_yitokens_for_underlying(
169 underlying_stake_amount,
170 total_underlying_tokens,
171 total_supply,
172 )
173 .unwrap();
174
175 my_yitokens += mint_yitokens;
176 my_underlying_tokens -= underlying_stake_amount;
177
178 total_underlying_tokens += underlying_stake_amount;
179 total_supply += mint_yitokens;
180 }
181 Some(std::cmp::Ordering::Greater) => {
182 let yitoken_amount = ((my_yitokens as f64) * ratio).to_u64().unwrap();
183 let withdraw_underlying_tokens = yi_token
184 .calculate_underlying_for_yitokens(
185 yitoken_amount,
186 total_underlying_tokens,
187 total_supply,
188 )
189 .unwrap();
190
191 my_yitokens -= yitoken_amount;
192 my_underlying_tokens += withdraw_underlying_tokens;
193
194 total_underlying_tokens -= withdraw_underlying_tokens;
195 total_supply -= yitoken_amount;
196 }
197 _ => {
198 }
200 }
201 }
202
203 assert!(my_underlying_tokens <= initial_underlying_tokens);
204
205 {
206 let final_yitokens = my_yitokens;
208 let withdraw_underlying_tokens = yi_token
209 .calculate_underlying_for_yitokens(
210 final_yitokens,
211 total_underlying_tokens,
212 total_supply,
213 )
214 .unwrap();
215
216 my_yitokens -= final_yitokens;
217 my_underlying_tokens += withdraw_underlying_tokens;
218
219 total_underlying_tokens -= withdraw_underlying_tokens;
220 total_supply -= my_yitokens;
221 }
222
223 assert_eq!(my_yitokens, 0);
224 assert!(my_underlying_tokens <= initial_underlying_tokens);
226
227 assert!(total_underlying_tokens >= initial_total_underlying_tokens);
229 assert!(total_supply >= initial_total_supply);
230 }
231
232 proptest! {
233 #[test]
234 fn cannot_increase_no_fees(
235 initial_underlying_tokens in 0..=u32::MAX,
236 initial_total_underlying_tokens in 0..=u32::MAX,
237 initial_total_supply in 0..=u32::MAX,
238 amounts in prop::array::uniform32(-1.0..=1.0)
239 ) {
240 perform_test_cannot_increase_no_fees(
241 initial_underlying_tokens.into(),
242 initial_total_underlying_tokens.into(),
243 initial_total_supply.into(),
244 amounts
245 )
246 }
247 }
248}