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