1use super::*;
2use crate::{
3 error::LendingError,
4 math::{Decimal, Rate, TryAdd, TryDiv, TryMul, TrySub},
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 std::{
16 cmp::Ordering,
17 convert::{TryFrom, TryInto},
18};
19
20pub const MAX_OBLIGATION_RESERVES: usize = 10;
22
23#[derive(Clone, Debug, Default, PartialEq)]
25pub struct Obligation {
26 pub version: u8,
28 pub last_update: LastUpdate,
30 pub lending_market: Pubkey,
32 pub owner: Pubkey,
34 pub deposits: Vec<ObligationCollateral>,
36 pub borrows: Vec<ObligationLiquidity>,
38 pub deposited_value: Decimal,
40 pub borrowed_value: Decimal,
42 pub allowed_borrow_value: Decimal,
44 pub unhealthy_borrow_value: Decimal,
46}
47
48impl Obligation {
49 pub fn new(params: InitObligationParams) -> Self {
51 let mut obligation = Self::default();
52 Self::init(&mut obligation, params);
53 obligation
54 }
55
56 pub fn init(&mut self, params: InitObligationParams) {
58 self.version = PROGRAM_VERSION;
59 self.last_update = LastUpdate::new(params.current_slot);
60 self.lending_market = params.lending_market;
61 self.owner = params.owner;
62 self.deposits = params.deposits;
63 self.borrows = params.borrows;
64 }
65
66 pub fn loan_to_value(&self) -> Result<Decimal, ProgramError> {
68 self.borrowed_value.try_div(self.deposited_value)
69 }
70
71 pub fn repay(&mut self, settle_amount: Decimal, liquidity_index: usize) -> ProgramResult {
73 let liquidity = &mut self.borrows[liquidity_index];
74 if settle_amount == liquidity.borrowed_amount_wads {
75 self.borrows.remove(liquidity_index);
76 } else {
77 liquidity.repay(settle_amount)?;
78 }
79 Ok(())
80 }
81
82 pub fn withdraw(&mut self, withdraw_amount: u64, collateral_index: usize) -> ProgramResult {
84 let collateral = &mut self.deposits[collateral_index];
85 if withdraw_amount == collateral.deposited_amount {
86 self.deposits.remove(collateral_index);
87 } else {
88 collateral.withdraw(withdraw_amount)?;
89 }
90 Ok(())
91 }
92
93 pub fn max_withdraw_value(
95 &self,
96 withdraw_collateral_ltv: Rate,
97 ) -> Result<Decimal, ProgramError> {
98 if self.allowed_borrow_value <= self.borrowed_value {
99 return Ok(Decimal::zero());
100 }
101 if withdraw_collateral_ltv == Rate::zero() {
102 return Ok(self.deposited_value);
103 }
104 self.allowed_borrow_value
105 .try_sub(self.borrowed_value)?
106 .try_div(withdraw_collateral_ltv)
107 }
108
109 pub fn remaining_borrow_value(&self) -> Result<Decimal, ProgramError> {
111 self.allowed_borrow_value.try_sub(self.borrowed_value)
112 }
113
114 pub fn max_liquidation_amount(
116 &self,
117 liquidity: &ObligationLiquidity,
118 ) -> Result<Decimal, ProgramError> {
119 let max_liquidation_value = self
120 .borrowed_value
121 .try_mul(Rate::from_percent(LIQUIDATION_CLOSE_FACTOR))?
122 .min(liquidity.market_value);
123 let max_liquidation_pct = max_liquidation_value.try_div(liquidity.market_value)?;
124 liquidity.borrowed_amount_wads.try_mul(max_liquidation_pct)
125 }
126
127 pub fn find_collateral_in_deposits(
129 &self,
130 deposit_reserve: Pubkey,
131 ) -> Result<(&ObligationCollateral, usize), ProgramError> {
132 if self.deposits.is_empty() {
133 msg!("Obligation has no deposits");
134 return Err(LendingError::ObligationDepositsEmpty.into());
135 }
136 let collateral_index = self
137 ._find_collateral_index_in_deposits(deposit_reserve)
138 .ok_or(LendingError::InvalidObligationCollateral)?;
139 Ok((&self.deposits[collateral_index], collateral_index))
140 }
141
142 pub fn find_or_add_collateral_to_deposits(
144 &mut self,
145 deposit_reserve: Pubkey,
146 ) -> Result<&mut ObligationCollateral, ProgramError> {
147 if let Some(collateral_index) = self._find_collateral_index_in_deposits(deposit_reserve) {
148 return Ok(&mut self.deposits[collateral_index]);
149 }
150 if self.deposits.len() + self.borrows.len() >= MAX_OBLIGATION_RESERVES {
151 msg!(
152 "Obligation cannot have more than {} deposits and borrows combined",
153 MAX_OBLIGATION_RESERVES
154 );
155 return Err(LendingError::ObligationReserveLimit.into());
156 }
157 let collateral = ObligationCollateral::new(deposit_reserve);
158 self.deposits.push(collateral);
159 Ok(self.deposits.last_mut().unwrap())
160 }
161
162 fn _find_collateral_index_in_deposits(&self, deposit_reserve: Pubkey) -> Option<usize> {
163 self.deposits
164 .iter()
165 .position(|collateral| collateral.deposit_reserve == deposit_reserve)
166 }
167
168 pub fn find_liquidity_in_borrows(
170 &self,
171 borrow_reserve: Pubkey,
172 ) -> Result<(&ObligationLiquidity, usize), ProgramError> {
173 if self.borrows.is_empty() {
174 msg!("Obligation has no borrows");
175 return Err(LendingError::ObligationBorrowsEmpty.into());
176 }
177 let liquidity_index = self
178 ._find_liquidity_index_in_borrows(borrow_reserve)
179 .ok_or(LendingError::InvalidObligationLiquidity)?;
180 Ok((&self.borrows[liquidity_index], liquidity_index))
181 }
182
183 pub fn find_or_add_liquidity_to_borrows(
185 &mut self,
186 borrow_reserve: Pubkey,
187 ) -> Result<&mut ObligationLiquidity, ProgramError> {
188 if let Some(liquidity_index) = self._find_liquidity_index_in_borrows(borrow_reserve) {
189 return Ok(&mut self.borrows[liquidity_index]);
190 }
191 if self.deposits.len() + self.borrows.len() >= MAX_OBLIGATION_RESERVES {
192 msg!(
193 "Obligation cannot have more than {} deposits and borrows combined",
194 MAX_OBLIGATION_RESERVES
195 );
196 return Err(LendingError::ObligationReserveLimit.into());
197 }
198 let liquidity = ObligationLiquidity::new(borrow_reserve);
199 self.borrows.push(liquidity);
200 Ok(self.borrows.last_mut().unwrap())
201 }
202
203 fn _find_liquidity_index_in_borrows(&self, borrow_reserve: Pubkey) -> Option<usize> {
204 self.borrows
205 .iter()
206 .position(|liquidity| liquidity.borrow_reserve == borrow_reserve)
207 }
208}
209
210pub struct InitObligationParams {
212 pub current_slot: Slot,
214 pub lending_market: Pubkey,
216 pub owner: Pubkey,
218 pub deposits: Vec<ObligationCollateral>,
220 pub borrows: Vec<ObligationLiquidity>,
222}
223
224impl Sealed for Obligation {}
225impl IsInitialized for Obligation {
226 fn is_initialized(&self) -> bool {
227 self.version != UNINITIALIZED_VERSION
228 }
229}
230
231#[derive(Clone, Debug, Default, PartialEq)]
233pub struct ObligationCollateral {
234 pub deposit_reserve: Pubkey,
236 pub deposited_amount: u64,
238 pub market_value: Decimal,
240}
241
242impl ObligationCollateral {
243 pub fn new(deposit_reserve: Pubkey) -> Self {
245 Self {
246 deposit_reserve,
247 deposited_amount: 0,
248 market_value: Decimal::zero(),
249 }
250 }
251
252 pub fn deposit(&mut self, collateral_amount: u64) -> ProgramResult {
254 self.deposited_amount = self
255 .deposited_amount
256 .checked_add(collateral_amount)
257 .ok_or(LendingError::MathOverflow)?;
258 Ok(())
259 }
260
261 pub fn withdraw(&mut self, collateral_amount: u64) -> ProgramResult {
263 self.deposited_amount = self
264 .deposited_amount
265 .checked_sub(collateral_amount)
266 .ok_or(LendingError::MathOverflow)?;
267 Ok(())
268 }
269}
270
271#[derive(Clone, Debug, Default, PartialEq)]
273pub struct ObligationLiquidity {
274 pub borrow_reserve: Pubkey,
276 pub cumulative_borrow_rate_wads: Decimal,
278 pub borrowed_amount_wads: Decimal,
280 pub market_value: Decimal,
282}
283
284impl ObligationLiquidity {
285 pub fn new(borrow_reserve: Pubkey) -> Self {
287 Self {
288 borrow_reserve,
289 cumulative_borrow_rate_wads: Decimal::one(),
290 borrowed_amount_wads: Decimal::zero(),
291 market_value: Decimal::zero(),
292 }
293 }
294
295 pub fn repay(&mut self, settle_amount: Decimal) -> ProgramResult {
297 self.borrowed_amount_wads = self.borrowed_amount_wads.try_sub(settle_amount)?;
298 Ok(())
299 }
300
301 pub fn borrow(&mut self, borrow_amount: Decimal) -> ProgramResult {
303 self.borrowed_amount_wads = self.borrowed_amount_wads.try_add(borrow_amount)?;
304 Ok(())
305 }
306
307 pub fn accrue_interest(&mut self, cumulative_borrow_rate_wads: Decimal) -> ProgramResult {
309 match cumulative_borrow_rate_wads.cmp(&self.cumulative_borrow_rate_wads) {
310 Ordering::Less => {
311 msg!("Interest rate cannot be negative");
312 return Err(LendingError::NegativeInterestRate.into());
313 }
314 Ordering::Equal => {}
315 Ordering::Greater => {
316 let compounded_interest_rate: Rate = cumulative_borrow_rate_wads
317 .try_div(self.cumulative_borrow_rate_wads)?
318 .try_into()?;
319
320 self.borrowed_amount_wads = self
321 .borrowed_amount_wads
322 .try_mul(compounded_interest_rate)?;
323 self.cumulative_borrow_rate_wads = cumulative_borrow_rate_wads;
324 }
325 }
326
327 Ok(())
328 }
329}
330
331const OBLIGATION_COLLATERAL_LEN: usize = 56; const OBLIGATION_LIQUIDITY_LEN: usize = 80; const OBLIGATION_LEN: usize = 916; impl Pack for Obligation {
336 const LEN: usize = OBLIGATION_LEN;
337
338 fn pack_into_slice(&self, dst: &mut [u8]) {
339 let output = array_mut_ref![dst, 0, OBLIGATION_LEN];
340 #[allow(clippy::ptr_offset_with_cast)]
341 let (
342 version,
343 last_update_slot,
344 last_update_stale,
345 lending_market,
346 owner,
347 deposited_value,
348 borrowed_value,
349 allowed_borrow_value,
350 unhealthy_borrow_value,
351 deposits_len,
352 borrows_len,
353 data_flat,
354 ) = mut_array_refs![
355 output,
356 1,
357 8,
358 1,
359 PUBKEY_BYTES,
360 PUBKEY_BYTES,
361 16,
362 16,
363 16,
364 16,
365 1,
366 1,
367 OBLIGATION_COLLATERAL_LEN + (OBLIGATION_LIQUIDITY_LEN * (MAX_OBLIGATION_RESERVES - 1))
368 ];
369
370 *version = self.version.to_le_bytes();
372 *last_update_slot = self.last_update.slot.to_le_bytes();
373 pack_bool(self.last_update.stale, last_update_stale);
374 lending_market.copy_from_slice(self.lending_market.as_ref());
375 owner.copy_from_slice(self.owner.as_ref());
376 pack_decimal(self.deposited_value, deposited_value);
377 pack_decimal(self.borrowed_value, borrowed_value);
378 pack_decimal(self.allowed_borrow_value, allowed_borrow_value);
379 pack_decimal(self.unhealthy_borrow_value, unhealthy_borrow_value);
380 *deposits_len = u8::try_from(self.deposits.len()).unwrap().to_le_bytes();
381 *borrows_len = u8::try_from(self.borrows.len()).unwrap().to_le_bytes();
382
383 let mut offset = 0;
384
385 for collateral in &self.deposits {
387 let deposits_flat = array_mut_ref![data_flat, offset, OBLIGATION_COLLATERAL_LEN];
388 #[allow(clippy::ptr_offset_with_cast)]
389 let (deposit_reserve, deposited_amount, market_value) =
390 mut_array_refs![deposits_flat, PUBKEY_BYTES, 8, 16];
391 deposit_reserve.copy_from_slice(collateral.deposit_reserve.as_ref());
392 *deposited_amount = collateral.deposited_amount.to_le_bytes();
393 pack_decimal(collateral.market_value, market_value);
394 offset += OBLIGATION_COLLATERAL_LEN;
395 }
396
397 for liquidity in &self.borrows {
399 let borrows_flat = array_mut_ref![data_flat, offset, OBLIGATION_LIQUIDITY_LEN];
400 #[allow(clippy::ptr_offset_with_cast)]
401 let (borrow_reserve, cumulative_borrow_rate_wads, borrowed_amount_wads, market_value) =
402 mut_array_refs![borrows_flat, PUBKEY_BYTES, 16, 16, 16];
403 borrow_reserve.copy_from_slice(liquidity.borrow_reserve.as_ref());
404 pack_decimal(
405 liquidity.cumulative_borrow_rate_wads,
406 cumulative_borrow_rate_wads,
407 );
408 pack_decimal(liquidity.borrowed_amount_wads, borrowed_amount_wads);
409 pack_decimal(liquidity.market_value, market_value);
410 offset += OBLIGATION_LIQUIDITY_LEN;
411 }
412 }
413
414 fn unpack_from_slice(src: &[u8]) -> Result<Self, ProgramError> {
416 let input = array_ref![src, 0, OBLIGATION_LEN];
417 #[allow(clippy::ptr_offset_with_cast)]
418 let (
419 version,
420 last_update_slot,
421 last_update_stale,
422 lending_market,
423 owner,
424 deposited_value,
425 borrowed_value,
426 allowed_borrow_value,
427 unhealthy_borrow_value,
428 deposits_len,
429 borrows_len,
430 data_flat,
431 ) = array_refs![
432 input,
433 1,
434 8,
435 1,
436 PUBKEY_BYTES,
437 PUBKEY_BYTES,
438 16,
439 16,
440 16,
441 16,
442 1,
443 1,
444 OBLIGATION_COLLATERAL_LEN + (OBLIGATION_LIQUIDITY_LEN * (MAX_OBLIGATION_RESERVES - 1))
445 ];
446
447 let version = u8::from_le_bytes(*version);
448 if version > PROGRAM_VERSION {
449 msg!("Obligation version does not match lending program version");
450 return Err(ProgramError::InvalidAccountData);
451 }
452
453 let deposits_len = u8::from_le_bytes(*deposits_len);
454 let borrows_len = u8::from_le_bytes(*borrows_len);
455 let mut deposits = Vec::with_capacity(deposits_len as usize + 1);
456 let mut borrows = Vec::with_capacity(borrows_len as usize + 1);
457
458 let mut offset = 0;
459 for _ in 0..deposits_len {
460 let deposits_flat = array_ref![data_flat, offset, OBLIGATION_COLLATERAL_LEN];
461 #[allow(clippy::ptr_offset_with_cast)]
462 let (deposit_reserve, deposited_amount, market_value) =
463 array_refs![deposits_flat, PUBKEY_BYTES, 8, 16];
464 deposits.push(ObligationCollateral {
465 deposit_reserve: Pubkey::new(deposit_reserve),
466 deposited_amount: u64::from_le_bytes(*deposited_amount),
467 market_value: unpack_decimal(market_value),
468 });
469 offset += OBLIGATION_COLLATERAL_LEN;
470 }
471 for _ in 0..borrows_len {
472 let borrows_flat = array_ref![data_flat, offset, OBLIGATION_LIQUIDITY_LEN];
473 #[allow(clippy::ptr_offset_with_cast)]
474 let (borrow_reserve, cumulative_borrow_rate_wads, borrowed_amount_wads, market_value) =
475 array_refs![borrows_flat, PUBKEY_BYTES, 16, 16, 16];
476 borrows.push(ObligationLiquidity {
477 borrow_reserve: Pubkey::new(borrow_reserve),
478 cumulative_borrow_rate_wads: unpack_decimal(cumulative_borrow_rate_wads),
479 borrowed_amount_wads: unpack_decimal(borrowed_amount_wads),
480 market_value: unpack_decimal(market_value),
481 });
482 offset += OBLIGATION_LIQUIDITY_LEN;
483 }
484
485 Ok(Self {
486 version,
487 last_update: LastUpdate {
488 slot: u64::from_le_bytes(*last_update_slot),
489 stale: unpack_bool(last_update_stale)?,
490 },
491 lending_market: Pubkey::new_from_array(*lending_market),
492 owner: Pubkey::new_from_array(*owner),
493 deposits,
494 borrows,
495 deposited_value: unpack_decimal(deposited_value),
496 borrowed_value: unpack_decimal(borrowed_value),
497 allowed_borrow_value: unpack_decimal(allowed_borrow_value),
498 unhealthy_borrow_value: unpack_decimal(unhealthy_borrow_value),
499 })
500 }
501}
502
503#[cfg(test)]
504mod test {
505 use super::*;
506 use crate::math::TryAdd;
507 use proptest::prelude::*;
508
509 const MAX_COMPOUNDED_INTEREST: u64 = 100; #[test]
512 fn obligation_accrue_interest_failure() {
513 assert_eq!(
514 ObligationLiquidity {
515 cumulative_borrow_rate_wads: Decimal::zero(),
516 ..ObligationLiquidity::default()
517 }
518 .accrue_interest(Decimal::one()),
519 Err(LendingError::MathOverflow.into())
520 );
521
522 assert_eq!(
523 ObligationLiquidity {
524 cumulative_borrow_rate_wads: Decimal::from(2u64),
525 ..ObligationLiquidity::default()
526 }
527 .accrue_interest(Decimal::one()),
528 Err(LendingError::NegativeInterestRate.into())
529 );
530
531 assert_eq!(
532 ObligationLiquidity {
533 cumulative_borrow_rate_wads: Decimal::one(),
534 borrowed_amount_wads: Decimal::from(u64::MAX),
535 ..ObligationLiquidity::default()
536 }
537 .accrue_interest(Decimal::from(10 * MAX_COMPOUNDED_INTEREST)),
538 Err(LendingError::MathOverflow.into())
539 );
540 }
541
542 prop_compose! {
544 fn cumulative_rates()(rate in 1..=u128::MAX)(
545 current_rate in Just(rate),
546 max_new_rate in rate..=rate.saturating_mul(MAX_COMPOUNDED_INTEREST as u128),
547 ) -> (u128, u128) {
548 (current_rate, max_new_rate)
549 }
550 }
551
552 const MAX_BORROWED: u128 = u64::MAX as u128 * WAD as u128;
553
554 prop_compose! {
556 fn repay_partial_amounts()(amount in 1..=u64::MAX)(
557 repay_amount in Just(WAD as u128 * amount as u128),
558 borrowed_amount in (WAD as u128 * amount as u128 + 1)..=MAX_BORROWED,
559 ) -> (u128, u128) {
560 (repay_amount, borrowed_amount)
561 }
562 }
563
564 prop_compose! {
566 fn repay_full_amounts()(amount in 1..=u64::MAX)(
567 repay_amount in Just(WAD as u128 * amount as u128),
568 ) -> (u128, u128) {
569 (repay_amount, repay_amount)
570 }
571 }
572
573 proptest! {
574 #[test]
575 fn repay_partial(
576 (repay_amount, borrowed_amount) in repay_partial_amounts(),
577 ) {
578 let borrowed_amount_wads = Decimal::from_scaled_val(borrowed_amount);
579 let repay_amount_wads = Decimal::from_scaled_val(repay_amount);
580 let mut obligation = Obligation {
581 borrows: vec![ObligationLiquidity {
582 borrowed_amount_wads,
583 ..ObligationLiquidity::default()
584 }],
585 ..Obligation::default()
586 };
587
588 obligation.repay(repay_amount_wads, 0)?;
589 assert!(obligation.borrows[0].borrowed_amount_wads < borrowed_amount_wads);
590 assert!(obligation.borrows[0].borrowed_amount_wads > Decimal::zero());
591 }
592
593 #[test]
594 fn repay_full(
595 (repay_amount, borrowed_amount) in repay_full_amounts(),
596 ) {
597 let borrowed_amount_wads = Decimal::from_scaled_val(borrowed_amount);
598 let repay_amount_wads = Decimal::from_scaled_val(repay_amount);
599 let mut obligation = Obligation {
600 borrows: vec![ObligationLiquidity {
601 borrowed_amount_wads,
602 ..ObligationLiquidity::default()
603 }],
604 ..Obligation::default()
605 };
606
607 obligation.repay(repay_amount_wads, 0)?;
608 assert_eq!(obligation.borrows.len(), 0);
609 }
610
611 #[test]
612 fn accrue_interest(
613 (current_borrow_rate, new_borrow_rate) in cumulative_rates(),
614 borrowed_amount in 0..=u64::MAX,
615 ) {
616 let cumulative_borrow_rate_wads = Decimal::one().try_add(Decimal::from_scaled_val(current_borrow_rate))?;
617 let borrowed_amount_wads = Decimal::from(borrowed_amount);
618 let mut liquidity = ObligationLiquidity {
619 cumulative_borrow_rate_wads,
620 borrowed_amount_wads,
621 ..ObligationLiquidity::default()
622 };
623
624 let next_cumulative_borrow_rate = Decimal::one().try_add(Decimal::from_scaled_val(new_borrow_rate))?;
625 liquidity.accrue_interest(next_cumulative_borrow_rate)?;
626
627 if next_cumulative_borrow_rate > cumulative_borrow_rate_wads {
628 assert!(liquidity.borrowed_amount_wads > borrowed_amount_wads);
629 } else {
630 assert!(liquidity.borrowed_amount_wads == borrowed_amount_wads);
631 }
632 }
633 }
634}