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;
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            // Accumulated amount is derived from real balances, so it should
189            // never overflow underlying data type.
190            #[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        // Calling market.snapshot() performs the market accounting
244    }
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    /// Returns the amount successfully removed from incoming.
288    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        //
348        // Check liquidity & eligibility
349        //
350
351        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        //
374        // Execute removal
375        //
376
377        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        // The only way to withdraw from a position is if it already has a deposit.
391        // Adding a deposit guarantees started_at_block_timestamp_ms != None
392        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}