1use std::ops::{Deref, DerefMut};
2
3use near_sdk::{json_types::U64, near, require, AccountId};
4
5use crate::{
6 accumulator::{AccumulationRecord, Accumulator},
7 asset::{BorrowAsset, BorrowAssetAmount},
8 event::MarketEvent,
9 incoming_deposit::IncomingDeposit,
10 market::{Market, Withdrawal},
11 number::Decimal,
12 YEAR_PER_MS,
13};
14
15pub struct YieldAccumulationProof(());
19
20#[derive(Default, Debug, Clone, PartialEq, Eq)]
21#[near(serializers = [json, borsh])]
22pub struct Deposit {
23 pub active: BorrowAssetAmount,
24 pub incoming: Vec<IncomingDeposit>,
25 pub outgoing: BorrowAssetAmount,
26}
27
28impl Deposit {
29 pub fn total(&self) -> BorrowAssetAmount {
30 let mut total = self.active + self.outgoing;
31 for incoming in &self.incoming {
32 total += incoming.amount;
33 }
34 total
35 }
36}
37
38#[derive(Debug, Clone, PartialEq, Eq)]
39#[near(serializers = [json, borsh])]
40pub struct SupplyPosition {
41 started_at_block_timestamp_ms: Option<U64>,
42 borrow_asset_deposit: Deposit,
43 pub borrow_asset_yield: Accumulator<BorrowAsset>,
44}
45
46impl SupplyPosition {
47 pub fn new(current_snapshot_index: u32) -> Self {
48 Self {
49 started_at_block_timestamp_ms: None,
50 borrow_asset_deposit: Deposit::default(),
51 borrow_asset_yield: Accumulator::new(current_snapshot_index),
52 }
53 }
54
55 pub fn get_deposit(&self) -> &Deposit {
56 &self.borrow_asset_deposit
57 }
58
59 pub fn total_incoming(&self) -> BorrowAssetAmount {
60 self.borrow_asset_deposit
61 .incoming
62 .iter()
63 .fold(BorrowAssetAmount::zero(), |total_incoming, incoming| {
64 total_incoming + incoming.amount
65 })
66 }
67
68 pub fn get_started_at_block_timestamp_ms(&self) -> Option<u64> {
69 self.started_at_block_timestamp_ms.map(u64::from)
70 }
71
72 pub fn exists(&self) -> bool {
73 !self.borrow_asset_deposit.total().is_zero()
74 || !self.borrow_asset_yield.get_total().is_zero()
75 }
76
77 pub fn can_be_removed(&self) -> bool {
78 !self.exists()
79 }
80}
81
82pub struct SupplyPositionRef<M> {
83 market: M,
84 account_id: AccountId,
85 position: SupplyPosition,
86}
87
88impl<M> SupplyPositionRef<M> {
89 pub fn new(market: M, account_id: AccountId, position: SupplyPosition) -> Self {
90 Self {
91 market,
92 account_id,
93 position,
94 }
95 }
96
97 pub fn account_id(&self) -> &AccountId {
98 &self.account_id
99 }
100
101 pub fn total_deposit(&self) -> BorrowAssetAmount {
102 self.position.borrow_asset_deposit.total()
103 }
104
105 pub fn total_yield(&self) -> BorrowAssetAmount {
106 self.position.borrow_asset_yield.get_total()
107 }
108
109 pub fn inner(&self) -> &SupplyPosition {
110 &self.position
111 }
112}
113
114impl<M: Deref<Target = Market>> SupplyPositionRef<M> {
115 pub fn is_within_allowable_range(&self) -> bool {
116 self.market
117 .configuration
118 .supply_range
119 .contains(self.position.borrow_asset_deposit.total())
120 }
121
122 pub fn calculate_yield(&self, snapshot_limit: u32) -> AccumulationRecord<BorrowAsset> {
123 let mut next_snapshot_index = self.position.borrow_asset_yield.get_next_snapshot_index();
124
125 let mut amount = u128::from(self.position.borrow_asset_deposit.active);
126 let mut accumulated = Decimal::ZERO;
127 let mut next_incoming = 0;
128
129 #[allow(clippy::unwrap_used, reason = "Guaranteed previous snapshot exists")]
130 let mut prev_end_timestamp_ms = self
131 .market
132 .finalized_snapshots
133 .get(next_snapshot_index.checked_sub(1).unwrap())
134 .unwrap()
135 .end_timestamp_ms
136 .0;
137
138 let weight_numerator = self.market.configuration.yield_weights.supply.get();
139 let weight_denominator = self.market.configuration.yield_weights.total_weight().get();
140
141 #[allow(
142 clippy::cast_possible_truncation,
143 reason = "Assume # of snapshots is never >u32::MAX"
144 )]
145 for (i, snapshot) in self
146 .market
147 .finalized_snapshots
148 .iter()
149 .enumerate()
150 .skip(next_snapshot_index as usize)
151 .take(snapshot_limit as usize)
152 {
153 while let Some(incoming) = self
154 .position
155 .borrow_asset_deposit
156 .incoming
157 .get(next_incoming)
158 .filter(|incoming| incoming.activate_at_snapshot_index as usize == i)
159 {
160 next_incoming += 1;
161 amount += u128::from(incoming.amount);
162 }
163
164 if !snapshot.borrow_asset_deposited_active.is_zero() {
165 let snapshot_duration_ms = snapshot.end_timestamp_ms.0 - prev_end_timestamp_ms;
166 let interest_paid_by_borrowers = Decimal::from(snapshot.borrow_asset_borrowed)
167 * snapshot.interest_rate
168 * snapshot_duration_ms
169 * YEAR_PER_MS;
170 let other_yield = Decimal::from(snapshot.yield_distribution);
171 accumulated +=
172 (interest_paid_by_borrowers + other_yield) * amount * weight_numerator
173 / u128::from(snapshot.borrow_asset_deposited_active)
174 / weight_denominator;
175 }
176
177 next_snapshot_index = i as u32 + 1;
178 prev_end_timestamp_ms = snapshot.end_timestamp_ms.0;
179 }
180
181 AccumulationRecord {
182 #[allow(clippy::unwrap_used, reason = "Derived from real balances")]
185 amount: accumulated.to_u128_floor().unwrap().into(),
186 fraction_as_u128_dividend: accumulated.fractional_part_as_u128_dividend(),
187 next_snapshot_index,
188 }
189 }
190}
191
192pub struct SupplyPositionGuard<'a>(SupplyPositionRef<&'a mut Market>);
193
194impl Drop for SupplyPositionGuard<'_> {
195 fn drop(&mut self) {
196 self.0
197 .market
198 .supply_positions
199 .insert(&self.0.account_id, &self.0.position);
200 }
201}
202
203impl<'a> Deref for SupplyPositionGuard<'a> {
204 type Target = SupplyPositionRef<&'a mut Market>;
205
206 fn deref(&self) -> &Self::Target {
207 &self.0
208 }
209}
210
211impl DerefMut for SupplyPositionGuard<'_> {
212 fn deref_mut(&mut self) -> &mut Self::Target {
213 &mut self.0
214 }
215}
216
217impl<'a> SupplyPositionGuard<'a> {
218 pub fn new(market: &'a mut Market, account_id: AccountId, position: SupplyPosition) -> Self {
219 Self(SupplyPositionRef::new(market, account_id, position))
220 }
221
222 fn activate_incoming(&mut self, through_snapshot_index: u32) {
223 let mut incoming = self
224 .position
225 .borrow_asset_deposit
226 .incoming
227 .clone()
228 .into_iter()
229 .peekable();
230 while let Some(deposit) =
231 incoming.next_if(|d| d.activate_at_snapshot_index <= through_snapshot_index)
232 {
233 self.position.borrow_asset_deposit.active += deposit.amount;
234 }
235 self.position.borrow_asset_deposit.incoming = incoming.collect();
236
237 }
239
240 fn remove_active(&mut self, amount: BorrowAssetAmount) {
241 self.position.borrow_asset_deposit.active -= amount;
242 self.market.borrow_asset_deposited_active -= amount;
243 }
244
245 fn add_incoming(&mut self, amount: BorrowAssetAmount, activate_at_snapshot_index: u32) {
246 let incoming = &mut self.position.borrow_asset_deposit.incoming;
247 if let Some(deposit) = incoming
248 .last_mut()
249 .filter(|i| i.activate_at_snapshot_index == activate_at_snapshot_index)
250 {
251 deposit.amount += amount;
252 } else {
253 const MAX_INCOMING: usize = 4;
254 require!(
255 incoming.len() < MAX_INCOMING,
256 "Too many deposits without running accumulation",
257 );
258 incoming.push(IncomingDeposit {
259 activate_at_snapshot_index,
260 amount,
261 });
262 }
263
264 if let Some(incoming) = self
265 .market
266 .borrow_asset_deposited_incoming
267 .iter_mut()
268 .find(|incoming| incoming.activate_at_snapshot_index == activate_at_snapshot_index)
269 {
270 incoming.amount += amount;
271 } else {
272 self.market
273 .borrow_asset_deposited_incoming
274 .push(IncomingDeposit {
275 activate_at_snapshot_index,
276 amount,
277 });
278 }
279 }
280
281 fn remove_incoming(&mut self, amount: BorrowAssetAmount) -> BorrowAssetAmount {
283 let mut total = BorrowAssetAmount::zero();
284 while let Some(newest) = self.position.borrow_asset_deposit.incoming.pop() {
285 total += newest.amount;
286
287 let Some(market_incoming) = self
288 .market
289 .borrow_asset_deposited_incoming
290 .iter_mut()
291 .find(|incoming| {
292 incoming.activate_at_snapshot_index == newest.activate_at_snapshot_index
293 })
294 else {
295 crate::panic_with_message("Invariant violation: Market incoming entry should exist if position incoming entry exists");
296 };
297 market_incoming.amount = market_incoming.amount.unwrap_sub(newest.amount, "Invariant violation: Market incoming >= position incoming should hold for all snapshot indices");
298
299 #[allow(clippy::comparison_chain)]
300 if total == amount {
301 return amount;
302 } else if total > amount {
303 self.add_incoming(total - amount, newest.activate_at_snapshot_index);
304 return amount;
305 }
306 }
307
308 total
309 }
310
311 pub fn accumulate_yield_partial(&mut self, snapshot_limit: u32) {
312 require!(snapshot_limit > 0, "snapshot_limit must be nonzero");
313
314 let accumulation_record = self.calculate_yield(snapshot_limit);
315 self.activate_incoming(accumulation_record.next_snapshot_index);
316
317 if !accumulation_record.amount.is_zero() {
318 MarketEvent::YieldAccumulated {
319 account_id: self.account_id.clone(),
320 borrow_asset_amount: accumulation_record.amount,
321 }
322 .emit();
323 }
324
325 self.position
326 .borrow_asset_yield
327 .accumulate(accumulation_record);
328 }
329
330 pub fn accumulate_yield(&mut self) -> YieldAccumulationProof {
331 self.accumulate_yield_partial(u32::MAX);
332 YieldAccumulationProof(())
333 }
334
335 pub fn record_withdrawal_initial(
339 &mut self,
340 _proof: YieldAccumulationProof,
341 requested_amount: BorrowAssetAmount,
342 block_timestamp_ms: u64,
343 ) -> WithdrawalAttempt {
344 let my_incoming = self.position.total_incoming();
349 let my_active = self.position.get_deposit().active;
350 let my_yield = self
351 .position
352 .borrow_asset_yield
353 .get_total()
354 .min(self.market.borrow_asset_paid_to_fees);
355 let entitled_to_withdraw = my_incoming + my_active + my_yield;
356
357 if entitled_to_withdraw.is_zero() {
358 return WithdrawalAttempt::EmptyPosition;
359 }
360
361 let requested_amount = requested_amount.min(entitled_to_withdraw);
362 let available_to_me = self.market.borrow_asset_deposited_active
363 + self.market.borrow_asset_paid_to_fees
364 - self.market.borrow_asset_borrowed
365 + my_incoming;
366 let can_withdraw_now = entitled_to_withdraw.min(available_to_me);
367
368 if can_withdraw_now.is_zero() {
369 return WithdrawalAttempt::NoLiquidity;
370 }
371
372 let withdrawal_amount = requested_amount.min(can_withdraw_now);
373
374 self.position.borrow_asset_deposit.outgoing += withdrawal_amount;
379
380 let mut amount_to_remove = withdrawal_amount;
381
382 let amount_from_yield = my_yield.min(amount_to_remove);
383 self.position.borrow_asset_yield.remove(amount_from_yield);
384 self.market.borrow_asset_paid_to_fees -= amount_from_yield;
385 amount_to_remove -= amount_from_yield;
386
387 let amount_from_incoming = my_incoming.min(amount_to_remove);
388 self.remove_incoming(amount_from_incoming);
389 amount_to_remove -= amount_from_incoming;
390
391 if !amount_to_remove.is_zero() {
392 self.remove_active(amount_to_remove);
393 }
394
395 let Some(U64(started_at_block_timestamp_ms)) =
398 self.0.position.started_at_block_timestamp_ms
399 else {
400 crate::panic_with_message(
401 "Invariant violation: Position with deposit has no timestamp",
402 );
403 };
404 let supply_duration = block_timestamp_ms.saturating_sub(started_at_block_timestamp_ms);
405
406 let amount_to_fees = self
407 .market
408 .configuration
409 .supply_withdrawal_fee
410 .of(withdrawal_amount, supply_duration)
411 .unwrap_or_else(|| crate::panic_with_message("Fee calculation overflow"))
412 .min(withdrawal_amount);
413
414 let amount_to_account = withdrawal_amount.saturating_sub(amount_to_fees);
415
416 self.market.borrow_asset_balance -= amount_to_account;
417 self.market.borrow_asset_withdrawal_in_flight += amount_to_account;
418
419 let withdrawal = Withdrawal {
420 account_id: self.account_id.clone(),
421 amount_to_account,
422 amount_to_fees,
423 };
424
425 if requested_amount > can_withdraw_now {
426 WithdrawalAttempt::Partial {
427 withdrawal,
428 remaining: requested_amount.saturating_sub(can_withdraw_now),
429 }
430 } else {
431 WithdrawalAttempt::Full(withdrawal)
432 }
433 }
434
435 pub fn record_withdrawal_final(&mut self, withdrawal: &Withdrawal, success: bool) {
436 let amount = withdrawal.amount_to_account + withdrawal.amount_to_fees;
437
438 self.position.borrow_asset_deposit.outgoing -= amount;
439 self.market.borrow_asset_withdrawal_in_flight -= withdrawal.amount_to_account;
440
441 if success {
442 self.market
443 .record_borrow_asset_protocol_yield(withdrawal.amount_to_fees);
444
445 MarketEvent::SupplyWithdrawn {
446 account_id: self.account_id.clone(),
447 borrow_asset_amount_to_account: withdrawal.amount_to_account,
448 borrow_asset_amount_to_fees: withdrawal.amount_to_fees,
449 }
450 .emit();
451 } else {
452 self.market.borrow_asset_balance += withdrawal.amount_to_account;
453 self.add_incoming(amount, self.market.finalized_snapshots.len() + 1);
454 }
455 }
456
457 pub fn record_deposit(
458 &mut self,
459 _proof: YieldAccumulationProof,
460 amount: BorrowAssetAmount,
461 block_timestamp_ms: u64,
462 ) {
463 if self.position.started_at_block_timestamp_ms.is_none()
464 || self.position.borrow_asset_deposit.active.is_zero()
465 {
466 self.position.started_at_block_timestamp_ms = Some(block_timestamp_ms.into());
467 }
468
469 self.market.borrow_asset_balance += amount;
470 self.add_incoming(amount, self.market.finalized_snapshots.len() + 1);
471
472 if !amount.is_zero() {
473 MarketEvent::SupplyDeposited {
474 account_id: self.account_id.clone(),
475 borrow_asset_amount: amount,
476 }
477 .emit();
478 }
479 }
480
481 pub fn record_yield_withdrawal(&mut self, amount: BorrowAssetAmount) {
482 self.0.position.borrow_asset_yield.remove(amount);
483 }
484}
485
486#[derive(Debug)]
487pub enum WithdrawalAttempt {
488 Full(Withdrawal),
489 Partial {
490 withdrawal: Withdrawal,
491 remaining: BorrowAssetAmount,
492 },
493 EmptyPosition,
494 NoLiquidity,
495}