1use std::{
2 convert::{TryFrom, TryInto},
3 u64,
4};
5
6use arrayref::{array_mut_ref, array_ref, array_refs, mut_array_refs};
7use solana_program::{
8 clock::Slot,
9 entrypoint::ProgramResult,
10 msg,
11 program_error::ProgramError,
12 program_pack::{IsInitialized, Pack, Sealed},
13 pubkey::{Pubkey, PUBKEY_BYTES},
14};
15use solana_program::program_option::COption;
16
17use crate::{
18 error::LendingError,
19 math::{Decimal, Rate, TryAdd, TryDiv, TryMul, TrySub},
20};
21
22use super::*;
23
24pub const LIQUIDATION_CLOSE_FACTOR: u8 = 50;
26
27pub const LIQUIDATION_CLOSE_AMOUNT: u64 = 2;
29
30#[derive(Clone, Debug, Default, PartialEq)]
32pub struct Reserve {
33 pub version: u8,
35 pub last_update: LastUpdate,
37 pub lending_market: Pubkey,
39 pub liquidity: ReserveLiquidity,
41 pub collateral: ReserveCollateral,
43 pub config: ReserveConfig,
45}
46
47impl Reserve {
48 pub fn current_borrow_rate(&self) -> Result<Rate, ProgramError> {
50 let utilization_rate = self.liquidity.utilization_rate()?;
51 let optimal_utilization_rate = Rate::from_percent(self.config.optimal_utilization_rate);
52 let low_utilization = utilization_rate < optimal_utilization_rate;
53 if low_utilization || self.config.optimal_utilization_rate == 100 {
54 let normalized_rate = utilization_rate.try_div(optimal_utilization_rate)?;
55 let min_rate = Rate::from_percent(self.config.min_borrow_rate);
56 let rate_range = Rate::from_percent(
57 self.config
58 .optimal_borrow_rate
59 .checked_sub(self.config.min_borrow_rate)
60 .ok_or(LendingError::MathOverflow)?,
61 );
62
63 Ok(normalized_rate.try_mul(rate_range)?.try_add(min_rate)?)
64 } else {
65 let normalized_rate = utilization_rate
66 .try_sub(optimal_utilization_rate)?
67 .try_div(Rate::from_percent(
68 100u8
69 .checked_sub(self.config.optimal_utilization_rate)
70 .ok_or(LendingError::MathOverflow)?,
71 ))?;
72 let min_rate = Rate::from_percent(self.config.optimal_borrow_rate);
73 let rate_range = Rate::from_percent(
74 self.config
75 .max_borrow_rate
76 .checked_sub(self.config.optimal_borrow_rate)
77 .ok_or(LendingError::MathOverflow)?,
78 );
79
80 Ok(normalized_rate.try_mul(rate_range)?.try_add(min_rate)?)
81 }
82 }
83
84 pub fn collateral_exchange_rate(&self) -> Result<CollateralExchangeRate, ProgramError> {
86 let total_liquidity = self.liquidity.total_supply()?;
87 self.collateral.exchange_rate(total_liquidity)
88 }
89
90 pub fn accrue_interest(&mut self, current_slot: Slot) -> ProgramResult {
92 let slots_elapsed = self.last_update.slots_elapsed(current_slot)?;
93 if slots_elapsed > 0 {
94 let current_borrow_rate = self.current_borrow_rate()?;
95 self.liquidity
96 .compound_interest(current_borrow_rate, slots_elapsed)?;
97 }
98 Ok(())
99 }
100}
101
102pub struct InitReserveParams {
104 pub current_slot: Slot,
106 pub lending_market: Pubkey,
108 pub liquidity: ReserveLiquidity,
110 pub collateral: ReserveCollateral,
112 pub config: ReserveConfig,
114}
115
116#[derive(Debug)]
118pub struct CalculateBorrowResult {
119 pub borrow_amount: Decimal,
121 pub receive_amount: u64,
123 pub borrow_fee: u64,
125 pub host_fee: u64,
127}
128
129#[derive(Debug)]
131pub struct CalculateRepayResult {
132 pub settle_amount: Decimal,
134 pub repay_amount: u64,
136}
137
138#[derive(Debug)]
140pub struct CalculateLiquidationResult {
141 pub settle_amount: Decimal,
144 pub repay_amount: u64,
146 pub withdraw_amount: u64,
148}
149
150#[derive(Clone, Debug, Default, PartialEq)]
152pub struct ReserveLiquidity {
153 pub mint_pubkey: Pubkey,
155 pub mint_decimals: u8,
157 pub supply_pubkey: Pubkey,
159 pub fee_receiver: Pubkey,
161 pub oracle_pubkey: COption<Pubkey>,
163 pub available_amount: u64,
165 pub borrowed_amount_wads: Decimal,
167 pub cumulative_borrow_rate_wads: Decimal,
169 pub market_price: Decimal,
171}
172
173impl ReserveLiquidity {
174 pub fn total_supply(&self) -> Result<Decimal, ProgramError> {
176 Decimal::from(self.available_amount).try_add(self.borrowed_amount_wads)
177 }
178
179 pub fn utilization_rate(&self) -> Result<Rate, ProgramError> {
181 let total_supply = self.total_supply()?;
182 if total_supply == Decimal::zero() {
183 return Ok(Rate::zero());
184 }
185 self.borrowed_amount_wads.try_div(total_supply)?.try_into()
186 }
187
188 fn compound_interest(
190 &mut self,
191 current_borrow_rate: Rate,
192 slots_elapsed: u64,
193 ) -> ProgramResult {
194 let slot_interest_rate = current_borrow_rate.try_div(SLOTS_PER_YEAR)?;
195 let compounded_interest_rate = Rate::one()
196 .try_add(slot_interest_rate)?
197 .try_pow(slots_elapsed)?;
198 self.cumulative_borrow_rate_wads = self
199 .cumulative_borrow_rate_wads
200 .try_mul(compounded_interest_rate)?;
201 self.borrowed_amount_wads = self
202 .borrowed_amount_wads
203 .try_mul(compounded_interest_rate)?;
204 Ok(())
205 }
206}
207
208pub struct NewReserveLiquidityParams {
210 pub mint_pubkey: Pubkey,
212 pub mint_decimals: u8,
214 pub supply_pubkey: Pubkey,
216 pub fee_receiver: Pubkey,
218 pub oracle_pubkey: COption<Pubkey>,
220 pub market_price: Decimal,
222}
223
224#[derive(Clone, Debug, Default, PartialEq)]
226pub struct ReserveCollateral {
227 pub mint_pubkey: Pubkey,
229 pub mint_total_supply: u64,
231 pub supply_pubkey: Pubkey,
233}
234
235impl ReserveCollateral {
236 fn exchange_rate(
238 &self,
239 total_liquidity: Decimal,
240 ) -> Result<CollateralExchangeRate, ProgramError> {
241 let rate = if self.mint_total_supply == 0 || total_liquidity == Decimal::zero() {
242 Rate::from_scaled_val(INITIAL_COLLATERAL_RATE)
243 } else {
244 let mint_total_supply = Decimal::from(self.mint_total_supply);
245 Rate::try_from(mint_total_supply.try_div(total_liquidity)?)?
246 };
247
248 Ok(CollateralExchangeRate(rate))
249 }
250}
251
252pub struct NewReserveCollateralParams {
254 pub mint_pubkey: Pubkey,
256 pub supply_pubkey: Pubkey,
258}
259
260#[derive(Clone, Copy, Debug)]
262pub struct CollateralExchangeRate(Rate);
263
264impl CollateralExchangeRate {
265 pub fn collateral_to_liquidity(&self, collateral_amount: u64) -> Result<u64, ProgramError> {
267 self.decimal_collateral_to_liquidity(collateral_amount.into())?
268 .try_floor_u64()
269 }
270
271 pub fn decimal_collateral_to_liquidity(
273 &self,
274 collateral_amount: Decimal,
275 ) -> Result<Decimal, ProgramError> {
276 collateral_amount.try_div(self.0)
277 }
278
279 pub fn liquidity_to_collateral(&self, liquidity_amount: u64) -> Result<u64, ProgramError> {
281 self.decimal_liquidity_to_collateral(liquidity_amount.into())?
282 .try_floor_u64()
283 }
284
285 pub fn decimal_liquidity_to_collateral(
287 &self,
288 liquidity_amount: Decimal,
289 ) -> Result<Decimal, ProgramError> {
290 liquidity_amount.try_mul(self.0)
291 }
292}
293
294impl From<CollateralExchangeRate> for Rate {
295 fn from(exchange_rate: CollateralExchangeRate) -> Self {
296 exchange_rate.0
297 }
298}
299
300#[derive(Clone, Copy, Debug, Default, PartialEq)]
302pub struct ReserveConfig {
303 pub optimal_utilization_rate: u8,
305 pub loan_to_value_ratio: u8,
308 pub liquidation_bonus: u8,
310 pub liquidation_threshold: u8,
312 pub min_borrow_rate: u8,
314 pub optimal_borrow_rate: u8,
316 pub max_borrow_rate: u8,
318 pub fees: ReserveFees,
320 pub deposit_staking_pool: COption<Pubkey>,
322}
323
324#[derive(Clone, Copy, Debug, Default, PartialEq)]
330pub struct ReserveFees {
331 pub borrow_fee_wad: u64,
338 pub flash_loan_fee_wad: u64,
340 pub host_fee_percentage: u8,
342}
343
344impl ReserveFees {
345 pub fn calculate_borrow_fees(
347 &self,
348 borrow_amount: Decimal,
349 fee_calculation: FeeCalculation,
350 ) -> Result<(u64, u64), ProgramError> {
351 self.calculate_fees(borrow_amount, self.borrow_fee_wad, fee_calculation)
352 }
353
354 pub fn calculate_flash_loan_fees(
356 &self,
357 flash_loan_amount: Decimal,
358 ) -> Result<(u64, u64), ProgramError> {
359 self.calculate_fees(
360 flash_loan_amount,
361 self.flash_loan_fee_wad,
362 FeeCalculation::Exclusive,
363 )
364 }
365
366 fn calculate_fees(
367 &self,
368 amount: Decimal,
369 fee_wad: u64,
370 fee_calculation: FeeCalculation,
371 ) -> Result<(u64, u64), ProgramError> {
372 let borrow_fee_rate = Rate::from_scaled_val(fee_wad);
373 let host_fee_rate = Rate::from_percent(self.host_fee_percentage);
374 if borrow_fee_rate > Rate::zero() && amount > Decimal::zero() {
375 let need_to_assess_host_fee = host_fee_rate > Rate::zero();
376 let minimum_fee = if need_to_assess_host_fee {
377 2u64 } else {
379 1u64 };
381
382 let borrow_fee_amount = match fee_calculation {
383 FeeCalculation::Exclusive => amount.try_mul(borrow_fee_rate)?,
385 FeeCalculation::Inclusive => {
387 let borrow_fee_rate =
388 borrow_fee_rate.try_div(borrow_fee_rate.try_add(Rate::one())?)?;
389 amount.try_mul(borrow_fee_rate)?
390 }
391 };
392
393 let borrow_fee_decimal = borrow_fee_amount.max(minimum_fee.into());
394 if borrow_fee_decimal >= amount {
395 msg!("Borrow amount is too small to receive liquidity after fees");
396 return Err(LendingError::BorrowTooSmall.into());
397 }
398
399 let borrow_fee = borrow_fee_decimal.try_round_u64()?;
400 let host_fee = if need_to_assess_host_fee {
401 borrow_fee_decimal
402 .try_mul(host_fee_rate)?
403 .try_round_u64()?
404 .max(1u64)
405 } else {
406 0
407 };
408
409 Ok((borrow_fee, host_fee))
410 } else {
411 Ok((0, 0))
412 }
413 }
414}
415
416pub enum FeeCalculation {
418 Exclusive,
420 Inclusive,
422}
423
424impl Sealed for Reserve {}
425impl IsInitialized for Reserve {
426 fn is_initialized(&self) -> bool {
427 self.version != UNINITIALIZED_VERSION
428 }
429}
430
431const RESERVE_LEN: usize = 575; impl Pack for Reserve {
433 const LEN: usize = RESERVE_LEN;
434
435 fn pack_into_slice(&self, output: &mut [u8]) {
437 let output = array_mut_ref![output, 0, RESERVE_LEN];
438 #[allow(clippy::ptr_offset_with_cast)]
439 let (
440 version,
441 last_update_slot,
442 last_update_stale,
443 lending_market,
444 liquidity_mint_pubkey,
445 liquidity_mint_decimals,
446 liquidity_supply_pubkey,
447 liquidity_fee_receiver,
448 liquidity_oracle_pubkey,
449 liquidity_available_amount,
450 liquidity_borrowed_amount_wads,
451 liquidity_cumulative_borrow_rate_wads,
452 liquidity_market_price,
453 collateral_mint_pubkey,
454 collateral_mint_total_supply,
455 collateral_supply_pubkey,
456 config_optimal_utilization_rate,
457 config_loan_to_value_ratio,
458 config_liquidation_bonus,
459 config_liquidation_threshold,
460 config_min_borrow_rate,
461 config_optimal_borrow_rate,
462 config_max_borrow_rate,
463 config_fees_borrow_fee_wad,
464 config_fees_flash_loan_fee_wad,
465 config_fees_host_fee_percentage,
466 config_deposit_staking_pool,
467 _padding,
468 ) = mut_array_refs![
469 output,
470 1,
471 8,
472 1,
473 PUBKEY_BYTES,
474 PUBKEY_BYTES,
475 1,
476 PUBKEY_BYTES,
477 PUBKEY_BYTES,
478 4 + PUBKEY_BYTES,
479 8,
480 16,
481 16,
482 16,
483 PUBKEY_BYTES,
484 8,
485 PUBKEY_BYTES,
486 1,
487 1,
488 1,
489 1,
490 1,
491 1,
492 1,
493 8,
494 8,
495 1,
496 33,
497 215
498 ];
499
500 *version = self.version.to_le_bytes();
502 *last_update_slot = self.last_update.slot.to_le_bytes();
503 pack_bool(self.last_update.stale, last_update_stale);
504 lending_market.copy_from_slice(self.lending_market.as_ref());
505
506 liquidity_mint_pubkey.copy_from_slice(self.liquidity.mint_pubkey.as_ref());
508 *liquidity_mint_decimals = self.liquidity.mint_decimals.to_le_bytes();
509 liquidity_supply_pubkey.copy_from_slice(self.liquidity.supply_pubkey.as_ref());
510 liquidity_fee_receiver.copy_from_slice(self.liquidity.fee_receiver.as_ref());
511 pack_coption_key(&self.liquidity.oracle_pubkey, liquidity_oracle_pubkey);
512 *liquidity_available_amount = self.liquidity.available_amount.to_le_bytes();
513 pack_decimal(
514 self.liquidity.borrowed_amount_wads,
515 liquidity_borrowed_amount_wads,
516 );
517 pack_decimal(
518 self.liquidity.cumulative_borrow_rate_wads,
519 liquidity_cumulative_borrow_rate_wads,
520 );
521 pack_decimal(self.liquidity.market_price, liquidity_market_price);
522
523 collateral_mint_pubkey.copy_from_slice(self.collateral.mint_pubkey.as_ref());
525 *collateral_mint_total_supply = self.collateral.mint_total_supply.to_le_bytes();
526 collateral_supply_pubkey.copy_from_slice(self.collateral.supply_pubkey.as_ref());
527
528 *config_optimal_utilization_rate = self.config.optimal_utilization_rate.to_le_bytes();
530 *config_loan_to_value_ratio = self.config.loan_to_value_ratio.to_le_bytes();
531 *config_liquidation_bonus = self.config.liquidation_bonus.to_le_bytes();
532 *config_liquidation_threshold = self.config.liquidation_threshold.to_le_bytes();
533 *config_min_borrow_rate = self.config.min_borrow_rate.to_le_bytes();
534 *config_optimal_borrow_rate = self.config.optimal_borrow_rate.to_le_bytes();
535 *config_max_borrow_rate = self.config.max_borrow_rate.to_le_bytes();
536 *config_fees_borrow_fee_wad = self.config.fees.borrow_fee_wad.to_le_bytes();
537 *config_fees_flash_loan_fee_wad = self.config.fees.flash_loan_fee_wad.to_le_bytes();
538 *config_fees_host_fee_percentage = self.config.fees.host_fee_percentage.to_le_bytes();
539 pack_coption_key_compact(
540 &self.config.deposit_staking_pool,
541 config_deposit_staking_pool,
542 );
543 }
544
545 fn unpack_from_slice(input: &[u8]) -> Result<Self, ProgramError> {
547 let input = array_ref![input, 0, RESERVE_LEN];
548 #[allow(clippy::ptr_offset_with_cast)]
549 let (
550 version,
551 last_update_slot,
552 last_update_stale,
553 lending_market,
554 liquidity_mint_pubkey,
555 liquidity_mint_decimals,
556 liquidity_supply_pubkey,
557 liquidity_fee_receiver,
558 liquidity_oracle_pubkey,
559 liquidity_available_amount,
560 liquidity_borrowed_amount_wads,
561 liquidity_cumulative_borrow_rate_wads,
562 liquidity_market_price,
563 collateral_mint_pubkey,
564 collateral_mint_total_supply,
565 collateral_supply_pubkey,
566 config_optimal_utilization_rate,
567 config_loan_to_value_ratio,
568 config_liquidation_bonus,
569 config_liquidation_threshold,
570 config_min_borrow_rate,
571 config_optimal_borrow_rate,
572 config_max_borrow_rate,
573 config_fees_borrow_fee_wad,
574 config_fees_flash_loan_fee_wad,
575 config_fees_host_fee_percentage,
576 config_deposit_staking_pool,
577 _padding,
578 ) = array_refs![
579 input,
580 1,
581 8,
582 1,
583 PUBKEY_BYTES,
584 PUBKEY_BYTES,
585 1,
586 PUBKEY_BYTES,
587 PUBKEY_BYTES,
588 4 + PUBKEY_BYTES,
589 8,
590 16,
591 16,
592 16,
593 PUBKEY_BYTES,
594 8,
595 PUBKEY_BYTES,
596 1,
597 1,
598 1,
599 1,
600 1,
601 1,
602 1,
603 8,
604 8,
605 1,
606 33,
607 215
608 ];
609
610 let version = u8::from_le_bytes(*version);
611 if version > PROGRAM_VERSION {
612 msg!("Reserve version does not match lending program version");
613 return Err(ProgramError::InvalidAccountData);
614 }
615
616 Ok(Self {
617 version,
618 last_update: LastUpdate {
619 slot: u64::from_le_bytes(*last_update_slot),
620 stale: unpack_bool(last_update_stale)?,
621 },
622 lending_market: Pubkey::new_from_array(*lending_market),
623 liquidity: ReserveLiquidity {
624 mint_pubkey: Pubkey::new_from_array(*liquidity_mint_pubkey),
625 mint_decimals: u8::from_le_bytes(*liquidity_mint_decimals),
626 supply_pubkey: Pubkey::new_from_array(*liquidity_supply_pubkey),
627 fee_receiver: Pubkey::new_from_array(*liquidity_fee_receiver),
628 oracle_pubkey: unpack_coption_key(liquidity_oracle_pubkey)?,
629 available_amount: u64::from_le_bytes(*liquidity_available_amount),
630 borrowed_amount_wads: unpack_decimal(liquidity_borrowed_amount_wads),
631 cumulative_borrow_rate_wads: unpack_decimal(liquidity_cumulative_borrow_rate_wads),
632 market_price: unpack_decimal(liquidity_market_price),
633 },
634 collateral: ReserveCollateral {
635 mint_pubkey: Pubkey::new_from_array(*collateral_mint_pubkey),
636 mint_total_supply: u64::from_le_bytes(*collateral_mint_total_supply),
637 supply_pubkey: Pubkey::new_from_array(*collateral_supply_pubkey),
638 },
639 config: ReserveConfig {
640 optimal_utilization_rate: u8::from_le_bytes(*config_optimal_utilization_rate),
641 loan_to_value_ratio: u8::from_le_bytes(*config_loan_to_value_ratio),
642 liquidation_bonus: u8::from_le_bytes(*config_liquidation_bonus),
643 liquidation_threshold: u8::from_le_bytes(*config_liquidation_threshold),
644 min_borrow_rate: u8::from_le_bytes(*config_min_borrow_rate),
645 optimal_borrow_rate: u8::from_le_bytes(*config_optimal_borrow_rate),
646 max_borrow_rate: u8::from_le_bytes(*config_max_borrow_rate),
647 fees: ReserveFees {
648 borrow_fee_wad: u64::from_le_bytes(*config_fees_borrow_fee_wad),
649 flash_loan_fee_wad: u64::from_le_bytes(*config_fees_flash_loan_fee_wad),
650 host_fee_percentage: u8::from_le_bytes(*config_fees_host_fee_percentage),
651 },
652 deposit_staking_pool: unpack_coption_key_compact(config_deposit_staking_pool)?,
653 },
654 })
655 }
656}