templar_common/market/
impl.rs

1use near_sdk::{
2    collections::{LookupMap, UnorderedMap},
3    env, near, AccountId, BorshStorageKey, IntoStorageKey,
4};
5
6use crate::{
7    accumulator::{AccumulationRecord, Accumulator},
8    asset::{BorrowAsset, BorrowAssetAmount, CollateralAssetAmount},
9    borrow::{BorrowPosition, BorrowPositionGuard, BorrowPositionRef},
10    chunked_append_only_list::ChunkedAppendOnlyList,
11    event::MarketEvent,
12    incoming_deposit::IncomingDeposit,
13    market::MarketConfiguration,
14    number::Decimal,
15    snapshot::Snapshot,
16    supply::{SupplyPosition, SupplyPositionGuard, SupplyPositionRef},
17    time_chunk::TimeChunk,
18    withdrawal_queue::WithdrawalQueue,
19    YEAR_PER_MS,
20};
21
22#[derive(Debug, Copy, Clone)]
23pub struct SnapshotProof(());
24
25#[derive(BorshStorageKey)]
26#[near]
27enum StorageKey {
28    SupplyPositions,
29    BorrowPositions,
30    FinalizedSnapshots,
31    WithdrawalQueue,
32    StaticYield,
33}
34
35#[near]
36pub struct Market {
37    prefix: Vec<u8>,
38    pub configuration: MarketConfiguration,
39    /// Total amount of borrow asset earning interest in the market.
40    pub borrow_asset_deposited_active: BorrowAssetAmount,
41    /// Upcoming snapshot indices with amounts of borrow asset that will be activated.
42    pub borrow_asset_deposited_incoming: Vec<IncomingDeposit>,
43    pub borrow_asset_withdrawal_in_flight: BorrowAssetAmount,
44    /// Sending borrow asset out, because if somebody sends the contract borrow asset, it's ok for the
45    /// contract to attempt to fulfill withdrawal request, even if the market thinks it doesn't have
46    /// enough to fulfill.
47    pub borrow_asset_borrowed_in_flight: BorrowAssetAmount,
48    /// Amount of borrow asset that has been withdrawn (is in use by) by borrowers.
49    ///
50    /// `borrow_asset_deposited_active - borrow_asset_borrowed - borrow_asset_borrowed_in_flight >= 0` should always be true.
51    pub borrow_asset_borrowed: BorrowAssetAmount,
52    /// Market-wide collateral asset deposit tracking.
53    pub collateral_asset_deposited: CollateralAssetAmount,
54    pub(crate) supply_positions: UnorderedMap<AccountId, SupplyPosition>,
55    pub(crate) borrow_positions: UnorderedMap<AccountId, BorrowPosition>,
56    pub current_time_chunk: TimeChunk,
57    pub current_yield_distribution: BorrowAssetAmount,
58    pub finalized_snapshots: ChunkedAppendOnlyList<Snapshot, 32>,
59    pub withdrawal_queue: WithdrawalQueue,
60    pub static_yield: LookupMap<AccountId, Accumulator<BorrowAsset>>,
61    single_snapshot_maximum_interest_precomputed: Decimal,
62}
63
64impl Market {
65    pub fn new(prefix: impl IntoStorageKey, configuration: MarketConfiguration) -> Self {
66        if let Err(e) = configuration.validate() {
67            crate::panic_with_message(&e.to_string());
68        }
69
70        let prefix = prefix.into_storage_key();
71        macro_rules! key {
72            ($key: ident) => {
73                [
74                    prefix.as_slice(),
75                    StorageKey::$key.into_storage_key().as_slice(),
76                ]
77                .concat()
78            };
79        }
80
81        let first_snapshot = Snapshot::new(configuration.time_chunk_configuration.previous());
82        let last_time_chunk = configuration.time_chunk_configuration.now();
83
84        let single_snapshot_maximum_interest_precomputed =
85            configuration.single_snapshot_maximum_interest();
86
87        let mut self_ = Self {
88            prefix: prefix.clone(),
89            configuration,
90            borrow_asset_deposited_active: 0.into(),
91            borrow_asset_deposited_incoming: Vec::new(),
92            borrow_asset_withdrawal_in_flight: 0.into(),
93            borrow_asset_borrowed_in_flight: 0.into(),
94            borrow_asset_borrowed: 0.into(),
95            collateral_asset_deposited: 0.into(),
96            supply_positions: UnorderedMap::new(key!(SupplyPositions)),
97            borrow_positions: UnorderedMap::new(key!(BorrowPositions)),
98            current_time_chunk: last_time_chunk,
99            current_yield_distribution: 0.into(),
100            finalized_snapshots: ChunkedAppendOnlyList::new(key!(FinalizedSnapshots)),
101            withdrawal_queue: WithdrawalQueue::new(key!(WithdrawalQueue)),
102            static_yield: LookupMap::new(key!(StaticYield)),
103            single_snapshot_maximum_interest_precomputed,
104        };
105
106        self_.finalized_snapshots.push(first_snapshot);
107
108        self_
109    }
110
111    pub fn borrowed(&self) -> BorrowAssetAmount {
112        self.borrow_asset_borrowed + self.borrow_asset_borrowed_in_flight
113    }
114
115    pub fn total_incoming(&self) -> BorrowAssetAmount {
116        self.borrow_asset_deposited_incoming
117            .iter()
118            .fold(BorrowAssetAmount::zero(), |total_incoming, incoming| {
119                total_incoming + incoming.amount
120            })
121    }
122
123    pub fn incoming_at(&self, snapshot_index: u32) -> BorrowAssetAmount {
124        self.borrow_asset_deposited_incoming
125            .iter()
126            .find_map(|incoming| {
127                (incoming.activate_at_snapshot_index == snapshot_index).then_some(incoming.amount)
128            })
129            .unwrap_or(0.into())
130    }
131
132    pub fn get_last_finalized_snapshot(&self) -> &Snapshot {
133        #[allow(clippy::unwrap_used, reason = "Snapshots are never empty")]
134        self.finalized_snapshots
135            .get(self.finalized_snapshots.len() - 1)
136            .unwrap()
137    }
138
139    pub fn current_snapshot(&self) -> Snapshot {
140        let current_snapshot_index = self.finalized_snapshots.len();
141        let incoming = self.incoming_at(current_snapshot_index);
142
143        let active = self.borrow_asset_deposited_active + incoming;
144
145        let borrowed = self.borrowed();
146
147        let interest_rate = self
148            .configuration
149            .borrow_interest_rate_strategy
150            .at(usage_ratio(active, borrowed));
151
152        Snapshot {
153            time_chunk: self.current_time_chunk,
154            end_timestamp_ms: env::block_timestamp_ms().into(),
155            borrow_asset_deposited_active: active,
156            borrow_asset_borrowed: borrowed,
157            collateral_asset_deposited: self.collateral_asset_deposited,
158            yield_distribution: self.current_yield_distribution,
159            interest_rate,
160        }
161    }
162
163    pub fn snapshot(&mut self) -> SnapshotProof {
164        let now = self.configuration.time_chunk_configuration.now();
165
166        // Do we need to finalize the current snapshot?
167        if self.current_time_chunk == now {
168            return SnapshotProof(());
169        }
170
171        let snapshot = self.current_snapshot();
172        let current_snapshot_index = self.finalized_snapshots.len();
173
174        // Emit event and push finalized snapshot
175        MarketEvent::SnapshotFinalized {
176            index: current_snapshot_index,
177            snapshot: snapshot.clone(),
178        }
179        .emit();
180        self.finalized_snapshots.push(snapshot);
181
182        // We just pushed a snapshot
183        let current_snapshot_index = current_snapshot_index + 1;
184
185        // Activate incoming funds
186        for i in 0..self.borrow_asset_deposited_incoming.len() {
187            let incoming = &self.borrow_asset_deposited_incoming[i];
188            if incoming.activate_at_snapshot_index == current_snapshot_index {
189                self.borrow_asset_deposited_active += incoming.amount;
190                self.borrow_asset_deposited_incoming.remove(i);
191                break;
192            }
193        }
194
195        // Reset for the new time chunk
196        self.current_time_chunk = now;
197        self.current_yield_distribution = 0.into();
198
199        SnapshotProof(())
200    }
201
202    pub fn single_snapshot_fee(&self, amount: BorrowAssetAmount) -> Option<BorrowAssetAmount> {
203        (u128::from(amount) * self.single_snapshot_maximum_interest_precomputed)
204            .to_u128_ceil()
205            .map(Into::into)
206    }
207
208    pub fn interest_rate(&self) -> Decimal {
209        self.configuration
210            .borrow_interest_rate_strategy
211            .at(usage_ratio(
212                self.borrow_asset_deposited_active,
213                self.borrowed(),
214            ))
215    }
216
217    pub fn get_borrow_asset_available_to_borrow(&self) -> BorrowAssetAmount {
218        #[allow(
219            clippy::unwrap_used,
220            reason = "Factor is guaranteed to be <=1, so value must still fit in u128"
221        )]
222        let must_retain = ((1u32 - self.configuration.borrow_asset_maximum_usage_ratio)
223            * Decimal::from(self.borrow_asset_deposited_active))
224        .to_u128_ceil()
225        .unwrap();
226
227        u128::from(self.borrow_asset_deposited_active)
228            .saturating_sub(u128::from(self.borrowed()))
229            .saturating_sub(must_retain)
230            .into()
231    }
232
233    pub fn iter_supply_positions(&self) -> impl Iterator<Item = (AccountId, SupplyPosition)> + '_ {
234        self.supply_positions.iter()
235    }
236
237    pub fn supply_position_ref(&self, account_id: AccountId) -> Option<SupplyPositionRef<&Self>> {
238        self.supply_positions
239            .get(&account_id)
240            .map(|position| SupplyPositionRef::new(self, account_id, position))
241    }
242
243    pub fn supply_position_guard(
244        &mut self,
245        _proof: SnapshotProof,
246        account_id: AccountId,
247    ) -> Option<SupplyPositionGuard> {
248        self.supply_positions
249            .get(&account_id)
250            .map(|position| SupplyPositionGuard::new(self, account_id, position))
251    }
252
253    pub fn get_or_create_supply_position_guard(
254        &mut self,
255        _proof: SnapshotProof,
256        account_id: AccountId,
257    ) -> SupplyPositionGuard {
258        let position = self
259            .supply_positions
260            .get(&account_id)
261            .unwrap_or_else(|| SupplyPosition::new(self.finalized_snapshots.len()));
262
263        SupplyPositionGuard::new(self, account_id, position)
264    }
265
266    pub fn cleanup_supply_position(&mut self, account_id: &AccountId) -> bool {
267        self.supply_positions
268            .get(account_id)
269            .filter(SupplyPosition::can_be_removed)
270            .and_then(|_| self.supply_positions.remove(account_id))
271            .is_some()
272    }
273
274    pub fn iter_borrow_positions(&self) -> impl Iterator<Item = (AccountId, BorrowPosition)> + '_ {
275        self.borrow_positions.iter()
276    }
277
278    pub fn borrow_position_ref(&self, account_id: AccountId) -> Option<BorrowPositionRef<&Self>> {
279        self.borrow_positions
280            .get(&account_id)
281            .map(|position| BorrowPositionRef::new(self, account_id, position))
282    }
283
284    pub fn borrow_position_guard(
285        &mut self,
286        _proof: SnapshotProof,
287        account_id: AccountId,
288    ) -> Option<BorrowPositionGuard> {
289        self.borrow_positions
290            .get(&account_id)
291            .map(|position| BorrowPositionGuard::new(self, account_id, position))
292    }
293
294    pub fn get_or_create_borrow_position_guard(
295        &mut self,
296        _proof: SnapshotProof,
297        account_id: AccountId,
298    ) -> BorrowPositionGuard {
299        let position = self
300            .borrow_positions
301            .get(&account_id)
302            .unwrap_or_else(|| BorrowPosition::new(self.finalized_snapshots.len()));
303
304        BorrowPositionGuard::new(self, account_id, position)
305    }
306
307    pub fn cleanup_borrow_position(&mut self, account_id: &AccountId) -> bool {
308        self.borrow_positions
309            .get(account_id)
310            .filter(|p| !p.exists())
311            .and_then(|_| self.borrow_positions.remove(account_id))
312            .is_some()
313    }
314
315    pub fn record_borrow_asset_protocol_yield(&mut self, amount: BorrowAssetAmount) {
316        let mut yield_record = self
317            .static_yield
318            .get(&self.configuration.protocol_account_id)
319            .unwrap_or_else(|| Accumulator::new(1));
320
321        yield_record.add_once(amount);
322
323        self.static_yield
324            .insert(&self.configuration.protocol_account_id, &yield_record);
325    }
326
327    pub fn record_borrow_asset_yield_distribution(&mut self, amount: BorrowAssetAmount) {
328        // Sanity.
329        if amount.is_zero() {
330            return;
331        }
332
333        self.current_yield_distribution += amount;
334    }
335
336    /// Accumulate static yield for an account.
337    ///
338    /// # Errors
339    ///
340    /// - When the account is not configured to earn static yield.
341    pub fn accumulate_static_yield(
342        &mut self,
343        account_id: &AccountId,
344        snapshot_limit: u32,
345    ) -> Result<(), UnknownAccount> {
346        let weight_numerator = *self
347            .configuration
348            .yield_weights
349            .r#static
350            .get(account_id)
351            .ok_or(UnknownAccount)?;
352        let weight_denominator = self.configuration.yield_weights.total_weight().get();
353        let mut accumulator = self
354            .static_yield
355            .get(account_id)
356            .unwrap_or_else(|| Accumulator::new(1));
357
358        let mut next_snapshot_index = accumulator.get_next_snapshot_index();
359        let mut accumulated = Decimal::ZERO;
360
361        #[allow(clippy::unwrap_used, reason = "Guaranteed previous snapshot exists")]
362        let mut prev_end_timestamp_ms = self
363            .finalized_snapshots
364            .get(next_snapshot_index.checked_sub(1).unwrap())
365            .unwrap()
366            .end_timestamp_ms
367            .0;
368
369        #[allow(
370            clippy::cast_possible_truncation,
371            reason = "Assume # of snapshots is never >u32::MAX"
372        )]
373        for (i, snapshot) in self
374            .finalized_snapshots
375            .iter()
376            .enumerate()
377            .skip(next_snapshot_index as usize)
378            .take(snapshot_limit as usize)
379        {
380            let snapshot_duration_ms = snapshot.end_timestamp_ms.0 - prev_end_timestamp_ms;
381            let interest_paid_by_borrowers = Decimal::from(snapshot.borrow_asset_borrowed)
382                * snapshot.interest_rate
383                * snapshot_duration_ms
384                * YEAR_PER_MS;
385            let other_yield = Decimal::from(snapshot.yield_distribution);
386            accumulated +=
387                (interest_paid_by_borrowers + other_yield) * weight_numerator / weight_denominator;
388
389            next_snapshot_index = i as u32 + 1;
390            prev_end_timestamp_ms = snapshot.end_timestamp_ms.0;
391        }
392
393        let accumulation_record = AccumulationRecord {
394            // Accumulated amount is derived from real balances, so it should
395            // never overflow underlying data type.
396            #[allow(clippy::unwrap_used, reason = "Derived from real balances")]
397            amount: accumulated.to_u128_floor().unwrap().into(),
398            fraction_as_u128_dividend: accumulated.fractional_part_as_u128_dividend(),
399            next_snapshot_index,
400        };
401
402        accumulator.accumulate(accumulation_record);
403
404        self.static_yield.insert(account_id, &accumulator);
405
406        Ok(())
407    }
408}
409
410#[derive(Debug, thiserror::Error)]
411#[error("This account does not earn static yield")]
412pub struct UnknownAccount;
413
414fn usage_ratio(active: BorrowAssetAmount, borrowed: BorrowAssetAmount) -> Decimal {
415    if active.is_zero() || borrowed.is_zero() {
416        Decimal::ZERO
417    } else if borrowed >= active {
418        Decimal::ONE
419    } else {
420        Decimal::from(borrowed) / Decimal::from(active)
421    }
422}