templar_common/
supply.rs

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
15/// This struct can only be constructed after accumulating yield on a
16/// supply position. This serves as proof that the yield has accrued, so it
17/// is safe to perform certain other operations.
18pub 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            // Accumulated amount is derived from real balances, so it should
183            // never overflow underlying data type.
184            #[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        // Calling market.snapshot() performs the market accounting
238    }
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    /// Returns the amount successfully removed from incoming.
282    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    /// Removes the requested amount from the supply record. The amount is
336    /// removed from the yield record, the incoming deposit record, and the
337    /// active supply record, in that order.
338    pub fn record_withdrawal_initial(
339        &mut self,
340        _proof: YieldAccumulationProof,
341        requested_amount: BorrowAssetAmount,
342        block_timestamp_ms: u64,
343    ) -> WithdrawalAttempt {
344        //
345        // Check liquidity & eligibility
346        //
347
348        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        //
375        // Execute removal
376        //
377
378        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        // The only way to withdraw from a position is if it already has a deposit.
396        // Adding a deposit guarantees started_at_block_timestamp_ms != None
397        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}