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 pub borrow_asset_deposited_active: BorrowAssetAmount,
41 pub borrow_asset_deposited_incoming: Vec<IncomingDeposit>,
43 pub borrow_asset_withdrawal_in_flight: BorrowAssetAmount,
44 pub borrow_asset_borrowed_in_flight: BorrowAssetAmount,
48 pub borrow_asset_borrowed: BorrowAssetAmount,
52 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 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 MarketEvent::SnapshotFinalized {
176 index: current_snapshot_index,
177 snapshot: snapshot.clone(),
178 }
179 .emit();
180 self.finalized_snapshots.push(snapshot);
181
182 let current_snapshot_index = current_snapshot_index + 1;
184
185 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 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 if amount.is_zero() {
330 return;
331 }
332
333 self.current_yield_distribution += amount;
334 }
335
336 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 #[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}