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 that the market knows it currently holds.
40    pub borrow_asset_balance: BorrowAssetAmount,
41    /// Total amount of borrow asset earning interest in the market.
42    pub borrow_asset_deposited_active: BorrowAssetAmount,
43    /// Upcoming snapshot indices with amounts of borrow asset that will be activated.
44    pub borrow_asset_deposited_incoming: Vec<IncomingDeposit>,
45    pub borrow_asset_withdrawal_in_flight: BorrowAssetAmount,
46    /// Sending borrow asset out, because if somebody sends the contract borrow asset, it's ok for the
47    /// contract to attempt to fulfill withdrawal request, even if the market thinks it doesn't have
48    /// enough to fulfill.
49    pub borrow_asset_borrowed_in_flight: BorrowAssetAmount,
50    /// Amount of borrow asset that has been withdrawn (is in use by) by borrowers.
51    ///
52    /// `borrow_asset_deposited_active - borrow_asset_borrowed - borrow_asset_borrowed_in_flight >= 0` should always be true.
53    pub borrow_asset_borrowed: BorrowAssetAmount,
54    /// Amount paid to fees that can be withdrawn by suppliers as yield.
55    pub borrow_asset_paid_to_fees: BorrowAssetAmount,
56    /// Market-wide collateral asset deposit tracking.
57    pub collateral_asset_deposited: CollateralAssetAmount,
58    pub(crate) supply_positions: UnorderedMap<AccountId, SupplyPosition>,
59    pub(crate) borrow_positions: UnorderedMap<AccountId, BorrowPosition>,
60    pub current_time_chunk: TimeChunk,
61    pub current_yield_distribution: BorrowAssetAmount,
62    pub finalized_snapshots: ChunkedAppendOnlyList<Snapshot, 32>,
63    pub withdrawal_queue: WithdrawalQueue,
64    pub static_yield: LookupMap<AccountId, Accumulator<BorrowAsset>>,
65    single_snapshot_maximum_interest_precomputed: Decimal,
66}
67
68impl Market {
69    pub fn new(prefix: impl IntoStorageKey, configuration: MarketConfiguration) -> Self {
70        if let Err(e) = configuration.validate() {
71            crate::panic_with_message(&e.to_string());
72        }
73
74        let prefix = prefix.into_storage_key();
75        macro_rules! key {
76            ($key: ident) => {
77                [
78                    prefix.as_slice(),
79                    StorageKey::$key.into_storage_key().as_slice(),
80                ]
81                .concat()
82            };
83        }
84
85        let first_snapshot = Snapshot::new(configuration.time_chunk_configuration.previous());
86        let last_time_chunk = configuration.time_chunk_configuration.now();
87
88        let single_snapshot_maximum_interest_precomputed =
89            configuration.single_snapshot_maximum_interest();
90
91        let mut self_ = Self {
92            prefix: prefix.clone(),
93            configuration,
94            borrow_asset_balance: 0.into(),
95            borrow_asset_deposited_active: 0.into(),
96            borrow_asset_deposited_incoming: Vec::new(),
97            borrow_asset_withdrawal_in_flight: 0.into(),
98            borrow_asset_borrowed_in_flight: 0.into(),
99            borrow_asset_borrowed: 0.into(),
100            borrow_asset_paid_to_fees: 0.into(),
101            collateral_asset_deposited: 0.into(),
102            supply_positions: UnorderedMap::new(key!(SupplyPositions)),
103            borrow_positions: UnorderedMap::new(key!(BorrowPositions)),
104            current_time_chunk: last_time_chunk,
105            current_yield_distribution: 0.into(),
106            finalized_snapshots: ChunkedAppendOnlyList::new(key!(FinalizedSnapshots)),
107            withdrawal_queue: WithdrawalQueue::new(key!(WithdrawalQueue)),
108            static_yield: LookupMap::new(key!(StaticYield)),
109            single_snapshot_maximum_interest_precomputed,
110        };
111
112        self_.finalized_snapshots.push(first_snapshot);
113
114        self_
115    }
116
117    pub fn borrowed(&self) -> BorrowAssetAmount {
118        self.borrow_asset_borrowed + self.borrow_asset_borrowed_in_flight
119    }
120
121    pub fn total_incoming(&self) -> BorrowAssetAmount {
122        self.borrow_asset_deposited_incoming
123            .iter()
124            .fold(BorrowAssetAmount::zero(), |total_incoming, incoming| {
125                total_incoming + incoming.amount
126            })
127    }
128
129    pub fn incoming_at(&self, snapshot_index: u32) -> BorrowAssetAmount {
130        self.borrow_asset_deposited_incoming
131            .iter()
132            .find_map(|incoming| {
133                (incoming.activate_at_snapshot_index == snapshot_index).then_some(incoming.amount)
134            })
135            .unwrap_or(0.into())
136    }
137
138    pub fn get_last_finalized_snapshot(&self) -> &Snapshot {
139        #[allow(clippy::unwrap_used, reason = "Snapshots are never empty")]
140        self.finalized_snapshots
141            .get(self.finalized_snapshots.len() - 1)
142            .unwrap()
143    }
144
145    pub fn current_snapshot(&self) -> Snapshot {
146        let current_snapshot_index = self.finalized_snapshots.len();
147        let incoming = self.incoming_at(current_snapshot_index);
148
149        let active = self.borrow_asset_deposited_active + incoming;
150
151        let borrowed = self.borrowed();
152
153        let interest_rate = self
154            .configuration
155            .borrow_interest_rate_strategy
156            .at(usage_ratio(active, borrowed));
157
158        Snapshot {
159            time_chunk: self.current_time_chunk,
160            end_timestamp_ms: env::block_timestamp_ms().into(),
161            borrow_asset_deposited_active: active,
162            borrow_asset_borrowed: borrowed,
163            collateral_asset_deposited: self.collateral_asset_deposited,
164            yield_distribution: self.current_yield_distribution,
165            interest_rate,
166        }
167    }
168
169    pub fn snapshot(&mut self) -> SnapshotProof {
170        let now = self.configuration.time_chunk_configuration.now();
171
172        // Do we need to finalize the current snapshot?
173        if self.current_time_chunk == now {
174            return SnapshotProof(());
175        }
176
177        let snapshot = self.current_snapshot();
178        let current_snapshot_index = self.finalized_snapshots.len();
179
180        // Emit event and push finalized snapshot
181        MarketEvent::SnapshotFinalized {
182            index: current_snapshot_index,
183            snapshot: snapshot.clone(),
184        }
185        .emit();
186        self.finalized_snapshots.push(snapshot);
187
188        // We just pushed a snapshot
189        let current_snapshot_index = current_snapshot_index + 1;
190
191        // Activate incoming funds
192        for i in 0..self.borrow_asset_deposited_incoming.len() {
193            let incoming = &self.borrow_asset_deposited_incoming[i];
194            if incoming.activate_at_snapshot_index == current_snapshot_index {
195                self.borrow_asset_deposited_active += incoming.amount;
196                self.borrow_asset_deposited_incoming.remove(i);
197                break;
198            }
199        }
200
201        // Reset for the new time chunk
202        self.current_time_chunk = now;
203        self.current_yield_distribution = 0.into();
204
205        SnapshotProof(())
206    }
207
208    pub fn single_snapshot_fee(&self, amount: BorrowAssetAmount) -> Option<BorrowAssetAmount> {
209        (u128::from(amount) * self.single_snapshot_maximum_interest_precomputed)
210            .to_u128_ceil()
211            .map(Into::into)
212    }
213
214    pub fn interest_rate(&self) -> Decimal {
215        self.configuration
216            .borrow_interest_rate_strategy
217            .at(usage_ratio(
218                self.borrow_asset_deposited_active,
219                self.borrowed(),
220            ))
221    }
222
223    pub fn get_borrow_asset_available_to_borrow(&self) -> BorrowAssetAmount {
224        #[allow(
225            clippy::unwrap_used,
226            reason = "Factor is guaranteed to be <=1, so value must still fit in u128"
227        )]
228        let must_retain: BorrowAssetAmount = ((1u32
229            - self.configuration.borrow_asset_maximum_usage_ratio)
230            * Decimal::from(self.borrow_asset_deposited_active))
231        .to_u128_ceil()
232        .unwrap()
233        .into();
234
235        self.borrow_asset_deposited_active
236            .saturating_sub(self.borrowed())
237            .saturating_sub(must_retain)
238    }
239
240    pub fn iter_supply_positions(&self) -> impl Iterator<Item = (AccountId, SupplyPosition)> + '_ {
241        self.supply_positions.iter()
242    }
243
244    pub fn supply_position_ref(&self, account_id: AccountId) -> Option<SupplyPositionRef<&Self>> {
245        self.supply_positions
246            .get(&account_id)
247            .map(|position| SupplyPositionRef::new(self, account_id, position))
248    }
249
250    pub fn supply_position_guard(
251        &mut self,
252        _proof: SnapshotProof,
253        account_id: AccountId,
254    ) -> Option<SupplyPositionGuard> {
255        self.supply_positions
256            .get(&account_id)
257            .map(|position| SupplyPositionGuard::new(self, account_id, position))
258    }
259
260    pub fn get_or_create_supply_position_guard(
261        &mut self,
262        _proof: SnapshotProof,
263        account_id: AccountId,
264    ) -> SupplyPositionGuard {
265        let position = self
266            .supply_positions
267            .get(&account_id)
268            .unwrap_or_else(|| SupplyPosition::new(self.finalized_snapshots.len()));
269
270        SupplyPositionGuard::new(self, account_id, position)
271    }
272
273    pub fn cleanup_supply_position(&mut self, account_id: &AccountId) -> bool {
274        self.supply_positions
275            .get(account_id)
276            .filter(SupplyPosition::can_be_removed)
277            .and_then(|_| self.supply_positions.remove(account_id))
278            .is_some()
279    }
280
281    pub fn iter_borrow_positions(&self) -> impl Iterator<Item = (AccountId, BorrowPosition)> + '_ {
282        self.borrow_positions.iter()
283    }
284
285    pub fn borrow_position_ref(&self, account_id: AccountId) -> Option<BorrowPositionRef<&Self>> {
286        self.borrow_positions
287            .get(&account_id)
288            .map(|position| BorrowPositionRef::new(self, account_id, position))
289    }
290
291    pub fn borrow_position_guard(
292        &mut self,
293        _proof: SnapshotProof,
294        account_id: AccountId,
295    ) -> Option<BorrowPositionGuard> {
296        self.borrow_positions
297            .get(&account_id)
298            .map(|position| BorrowPositionGuard::new(self, account_id, position))
299    }
300
301    pub fn get_or_create_borrow_position_guard(
302        &mut self,
303        _proof: SnapshotProof,
304        account_id: AccountId,
305    ) -> BorrowPositionGuard {
306        let position = self
307            .borrow_positions
308            .get(&account_id)
309            .unwrap_or_else(|| BorrowPosition::new(self.finalized_snapshots.len()));
310
311        BorrowPositionGuard::new(self, account_id, position)
312    }
313
314    pub fn cleanup_borrow_position(&mut self, account_id: &AccountId) -> bool {
315        self.borrow_positions
316            .get(account_id)
317            .filter(|p| !p.exists())
318            .and_then(|_| self.borrow_positions.remove(account_id))
319            .is_some()
320    }
321
322    pub fn record_borrow_asset_protocol_yield(&mut self, amount: BorrowAssetAmount) {
323        let mut yield_record = self
324            .static_yield
325            .get(&self.configuration.protocol_account_id)
326            .unwrap_or_else(|| Accumulator::new(1));
327
328        yield_record.add_once(amount);
329
330        self.static_yield
331            .insert(&self.configuration.protocol_account_id, &yield_record);
332    }
333
334    pub fn record_borrow_asset_yield_distribution(&mut self, amount: BorrowAssetAmount) {
335        // Sanity.
336        if amount.is_zero() {
337            return;
338        }
339
340        self.current_yield_distribution += amount;
341    }
342
343    /// Accumulate static yield for an account.
344    ///
345    /// # Errors
346    ///
347    /// - When the account is not configured to earn static yield.
348    pub fn accumulate_static_yield(
349        &mut self,
350        account_id: &AccountId,
351        snapshot_limit: u32,
352    ) -> Result<(), UnknownAccount> {
353        let weight_numerator = *self
354            .configuration
355            .yield_weights
356            .r#static
357            .get(account_id)
358            .ok_or(UnknownAccount)?;
359        let weight_denominator = self.configuration.yield_weights.total_weight().get();
360        let mut accumulator = self
361            .static_yield
362            .get(account_id)
363            .unwrap_or_else(|| Accumulator::new(1));
364
365        let mut next_snapshot_index = accumulator.get_next_snapshot_index();
366        let mut accumulated = Decimal::ZERO;
367
368        #[allow(clippy::unwrap_used, reason = "Guaranteed previous snapshot exists")]
369        let mut prev_end_timestamp_ms = self
370            .finalized_snapshots
371            .get(next_snapshot_index.checked_sub(1).unwrap())
372            .unwrap()
373            .end_timestamp_ms
374            .0;
375
376        #[allow(
377            clippy::cast_possible_truncation,
378            reason = "Assume # of snapshots is never >u32::MAX"
379        )]
380        for (i, snapshot) in self
381            .finalized_snapshots
382            .iter()
383            .enumerate()
384            .skip(next_snapshot_index as usize)
385            .take(snapshot_limit as usize)
386        {
387            let snapshot_duration_ms = snapshot.end_timestamp_ms.0 - prev_end_timestamp_ms;
388            let interest_paid_by_borrowers = Decimal::from(snapshot.borrow_asset_borrowed)
389                * snapshot.interest_rate
390                * snapshot_duration_ms
391                * YEAR_PER_MS;
392            let other_yield = Decimal::from(snapshot.yield_distribution);
393            accumulated +=
394                (interest_paid_by_borrowers + other_yield) * weight_numerator / weight_denominator;
395
396            next_snapshot_index = i as u32 + 1;
397            prev_end_timestamp_ms = snapshot.end_timestamp_ms.0;
398        }
399
400        let accumulation_record = AccumulationRecord {
401            // Accumulated amount is derived from real balances, so it should
402            // never overflow underlying data type.
403            #[allow(clippy::unwrap_used, reason = "Derived from real balances")]
404            amount: accumulated.to_u128_floor().unwrap().into(),
405            fraction_as_u128_dividend: accumulated.fractional_part_as_u128_dividend(),
406            next_snapshot_index,
407        };
408
409        accumulator.accumulate(accumulation_record);
410
411        self.static_yield.insert(account_id, &accumulator);
412
413        Ok(())
414    }
415}
416
417#[derive(Debug, thiserror::Error)]
418#[error("This account does not earn static yield")]
419pub struct UnknownAccount;
420
421fn usage_ratio(active: BorrowAssetAmount, borrowed: BorrowAssetAmount) -> Decimal {
422    if active.is_zero() || borrowed.is_zero() {
423        Decimal::ZERO
424    } else if borrowed >= active {
425        Decimal::ONE
426    } else {
427        Decimal::from(borrowed) / Decimal::from(active)
428    }
429}
430
431#[allow(clippy::too_many_lines)]
432#[cfg(test)]
433mod tests {
434    use near_sdk::{test_utils::*, testing_env, VMContext};
435
436    use crate::{
437        asset::FungibleAsset,
438        borrow::InitialBorrow,
439        dec,
440        fee::{Fee, TimeBasedFee},
441        interest_rate_strategy::InterestRateStrategy,
442        market::{PriceOracleConfiguration, Withdrawal, YieldWeights},
443        oracle::pyth::PriceIdentifier,
444        price::PricePair,
445        supply::WithdrawalAttempt,
446        time_chunk::TimeChunkConfiguration,
447    };
448
449    use super::*;
450
451    fn configuration() -> MarketConfiguration {
452        MarketConfiguration {
453            time_chunk_configuration: TimeChunkConfiguration::new(1),
454            borrow_asset: FungibleAsset::nep141("borrow.near".parse().unwrap()),
455            collateral_asset: FungibleAsset::nep141("collateral.near".parse().unwrap()),
456            price_oracle_configuration: PriceOracleConfiguration {
457                account_id: "pyth-oracle.near".parse().unwrap(),
458                collateral_asset_price_id: PriceIdentifier([0xcc; 32]),
459                collateral_asset_decimals: 24,
460                borrow_asset_price_id: PriceIdentifier([0xbb; 32]),
461                borrow_asset_decimals: 24,
462                price_maximum_age_s: 60,
463            },
464            borrow_mcr_maintenance: dec!("1.25"),
465            borrow_mcr_liquidation: dec!("1.2"),
466            borrow_asset_maximum_usage_ratio: dec!("0.9"),
467            borrow_origination_fee: Fee::Proportional(dec!("0.25")),
468            borrow_interest_rate_strategy: InterestRateStrategy::zero(),
469            borrow_maximum_duration_ms: None,
470            borrow_range: (1, None).try_into().unwrap(),
471            supply_range: (1, None).try_into().unwrap(),
472            supply_withdrawal_range: (1, None).try_into().unwrap(),
473            supply_withdrawal_fee: TimeBasedFee::zero(),
474            yield_weights: YieldWeights::new_with_supply_weight(9)
475                .with_static("revenue.tmplr.near".parse().unwrap(), 1),
476            protocol_account_id: "revenue.tmplr.near".parse().unwrap(),
477            liquidation_maximum_spread: dec!("0.05"),
478        }
479    }
480
481    fn price_pair(collateral: i64, borrow: i64) -> PricePair {
482        PricePair::new(
483            &crate::oracle::pyth::Price {
484                price: collateral.into(),
485                conf: 0.into(),
486                expo: 24,
487                publish_time: 10,
488            },
489            24,
490            &crate::oracle::pyth::Price {
491                price: borrow.into(),
492                conf: 0.into(),
493                expo: 24,
494                publish_time: 10,
495            },
496            24,
497        )
498        .unwrap()
499    }
500
501    struct TestMarketController {
502        pub context: VMContext,
503        pub market: Market,
504    }
505
506    impl TestMarketController {
507        pub fn print(&self) {
508            eprintln!(
509                "Available: {}",
510                self.market.get_borrow_asset_available_to_borrow(),
511            );
512            eprintln!("Balance: {}", self.market.borrow_asset_balance);
513            eprintln!("Borrowed: {}", self.market.borrow_asset_borrowed);
514            eprintln!("Supply: {}", self.market.borrow_asset_deposited_active);
515            eprintln!("Paid to fees: {}", self.market.borrow_asset_paid_to_fees);
516            eprintln!("{:#?}", self.market.borrow_asset_deposited_incoming);
517        }
518
519        pub fn new(configuration: MarketConfiguration) -> Self {
520            let context = VMContextBuilder::new()
521                .block_timestamp(1_000_000_000_000)
522                .build();
523            testing_env!(context.clone());
524
525            let market = Market::new(b"m", configuration);
526
527            Self { context, market }
528        }
529
530        pub fn tick(&mut self) -> SnapshotProof {
531            self.context.block_timestamp += 1_000_000;
532            testing_env!(self.context.clone());
533            self.market.snapshot()
534        }
535
536        pub fn supply(&mut self, account: AccountId, amount: u128) {
537            let snapshot = self.tick();
538            let mut supply_position = self
539                .market
540                .get_or_create_supply_position_guard(snapshot, account);
541            let yield_proof = supply_position.accumulate_yield();
542            supply_position.record_deposit(yield_proof, amount.into(), env::block_timestamp_ms());
543        }
544
545        pub fn collateralize(&mut self, account: AccountId, amount: u128) {
546            let snapshot_proof = self.tick();
547            let mut borrow_position = self
548                .market
549                .get_or_create_borrow_position_guard(snapshot_proof, account);
550            let interest_proof = borrow_position.accumulate_interest();
551            borrow_position.record_collateral_asset_deposit(interest_proof, amount.into());
552        }
553
554        pub fn borrow_initial(&mut self, account_id: AccountId, amount: u128) -> InitialBorrow {
555            let snapshot = self.tick();
556            let mut borrow_position = self
557                .market
558                .borrow_position_guard(snapshot, account_id)
559                .unwrap();
560            let interest_proof = borrow_position.accumulate_interest();
561            borrow_position
562                .record_borrow_initial(
563                    snapshot,
564                    interest_proof,
565                    amount.into(),
566                    &price_pair(1, 1),
567                    env::block_timestamp_ms(),
568                )
569                .unwrap()
570        }
571
572        pub fn borrow_final(&mut self, account_id: AccountId, initial: &InitialBorrow) {
573            let snapshot = self.tick();
574            let mut borrow_position = self
575                .market
576                .borrow_position_guard(snapshot, account_id)
577                .unwrap();
578            let interest_proof = borrow_position.accumulate_interest();
579            borrow_position.record_borrow_final(
580                snapshot,
581                interest_proof,
582                initial,
583                true,
584                env::block_timestamp_ms(),
585            );
586        }
587
588        pub fn accumulate_interest(&mut self, account_id: AccountId) -> BorrowPosition {
589            let snapshot = self.tick();
590            let mut borrow_position = self
591                .market
592                .borrow_position_guard(snapshot, account_id)
593                .unwrap();
594            let _ = borrow_position.accumulate_interest();
595            borrow_position.inner().clone()
596        }
597
598        pub fn repay(
599            &mut self,
600            account_id: AccountId,
601            amount: impl Into<BorrowAssetAmount>,
602        ) -> BorrowAssetAmount {
603            let snapshot = self.tick();
604            let mut borrow_position = self
605                .market
606                .borrow_position_guard(snapshot, account_id)
607                .unwrap();
608            let interest_proof = borrow_position.accumulate_interest();
609            borrow_position.record_repay(interest_proof, amount.into())
610        }
611
612        pub fn withdraw_collateral_initial(&mut self, account_id: AccountId, amount: u128) {
613            let snapshot = self.tick();
614            let mut borrow_position = self
615                .market
616                .borrow_position_guard(snapshot, account_id)
617                .unwrap();
618            let interest_proof = borrow_position.accumulate_interest();
619            borrow_position
620                .record_collateral_asset_withdrawal_initial(interest_proof, amount.into());
621        }
622
623        pub fn withdraw_collateral_final(&mut self, account_id: AccountId, amount: u128) {
624            let snapshot = self.tick();
625            let mut borrow_position = self
626                .market
627                .borrow_position_guard(snapshot, account_id)
628                .unwrap();
629            let interest_proof = borrow_position.accumulate_interest();
630            borrow_position.record_collateral_asset_withdrawal_final(
631                interest_proof,
632                amount.into(),
633                true,
634            );
635        }
636
637        pub fn liquidate(
638            &mut self,
639            liquidator_id: AccountId,
640            position_id: AccountId,
641            send: u128,
642            request: u128,
643            price_pair: &PricePair,
644        ) {
645            let snapshot = self.tick();
646            let mut borrow_position = self
647                .market
648                .borrow_position_guard(snapshot, position_id)
649                .unwrap();
650            let interest_proof = borrow_position.accumulate_interest();
651            let liquidation = borrow_position
652                .record_liquidation(
653                    interest_proof,
654                    liquidator_id,
655                    send.into(),
656                    Some(request.into()),
657                    price_pair,
658                    env::block_timestamp_ms(),
659                )
660                .unwrap();
661            assert_eq!(u128::from(liquidation.liquidated), request);
662        }
663
664        pub fn accumulate_yield(&mut self, account_id: AccountId) -> SupplyPosition {
665            let snapshot = self.tick();
666            let mut supply_position = self
667                .market
668                .supply_position_guard(snapshot, account_id)
669                .unwrap();
670            let _ = supply_position.accumulate_yield();
671            supply_position.inner().clone()
672        }
673
674        pub fn withdraw_supply_initial(
675            &mut self,
676            account_id: AccountId,
677            amount: u128,
678        ) -> WithdrawalAttempt {
679            let snapshot = self.tick();
680            let mut supply_position = self
681                .market
682                .supply_position_guard(snapshot, account_id)
683                .unwrap();
684            let proof = supply_position.accumulate_yield();
685            supply_position.record_withdrawal_initial(
686                proof,
687                amount.into(),
688                env::block_timestamp_ms(),
689            )
690        }
691
692        pub fn withdraw_supply_final(&mut self, account_id: AccountId, initial: &Withdrawal) {
693            let snapshot = self.tick();
694            let mut supply_position = self
695                .market
696                .supply_position_guard(snapshot, account_id)
697                .unwrap();
698            supply_position.record_withdrawal_final(initial, true);
699        }
700    }
701
702    #[test]
703    fn balance_1() {
704        let supplier: AccountId = "supply.near".parse().unwrap();
705        let borrower: AccountId = "borrow.near".parse().unwrap();
706
707        let mut c = TestMarketController::new(configuration());
708
709        // Supply
710        c.supply(supplier.clone(), 10_000_000);
711        assert_eq!(c.market.borrow_asset_balance, 10_000_000.into());
712        assert_eq!(
713            c.market.get_borrow_asset_available_to_borrow(),
714            0.into(),
715            "still incoming, not yet active",
716        );
717
718        c.collateralize(borrower.clone(), 4_000_000);
719        assert_eq!(c.market.borrow_asset_balance, 10_000_000.into());
720        assert_eq!(
721            c.market.get_borrow_asset_available_to_borrow(),
722            9_000_000.into()
723        );
724
725        let initial = c.borrow_initial(borrower.clone(), 2_000_000);
726        c.print();
727        assert_eq!(c.market.borrow_asset_balance, 8_000_000.into());
728        assert_eq!(
729            c.market.get_borrow_asset_available_to_borrow(),
730            7_000_000.into()
731        );
732
733        c.borrow_final(borrower.clone(), &initial);
734        assert_eq!(c.market.borrow_asset_borrowed, 2_000_000.into());
735        assert_eq!(c.market.borrow_asset_balance, 8_000_000.into());
736        assert_eq!(
737            c.market.get_borrow_asset_available_to_borrow(),
738            7_000_000.into()
739        );
740
741        // Repay half
742        c.repay(borrower.clone(), 1_500_000);
743        assert_eq!(c.market.borrow_asset_borrowed, 1_000_000.into());
744        assert_eq!(c.market.borrow_asset_balance, 9_500_000.into());
745        assert_eq!(
746            c.market.get_borrow_asset_available_to_borrow(),
747            8_000_000.into()
748        );
749
750        // Withdraw half of collateral: initial
751        c.withdraw_collateral_initial(borrower.clone(), 2_000_000);
752        assert_eq!(c.market.borrow_asset_balance, 9_500_000.into());
753        assert_eq!(
754            c.market.get_borrow_asset_available_to_borrow(),
755            8_000_000.into()
756        );
757
758        // Withdraw half of collateral: final
759        c.withdraw_collateral_final(borrower.clone(), 2_000_000);
760        assert_eq!(c.market.borrow_asset_balance, 9_500_000.into());
761        assert_eq!(
762            c.market.get_borrow_asset_available_to_borrow(),
763            8_000_000.into()
764        );
765
766        // Liquidate the position
767        let liquidator: AccountId = "liquidator.near".parse().unwrap();
768        c.liquidate(
769            liquidator.clone(),
770            borrower.clone(),
771            1_000_000,
772            2_000_000,
773            &price_pair(1, 2),
774        );
775        assert_eq!(c.market.borrow_asset_balance, 10_500_000.into());
776        assert_eq!(
777            c.market.get_borrow_asset_available_to_borrow(),
778            9_000_000.into()
779        );
780
781        // Supply yield compounding
782        let expected_yield_amount: BorrowAssetAmount = (500_000 * 9 / 10).into();
783        let yield_amount = c
784            .accumulate_yield(supplier.clone())
785            .borrow_asset_yield
786            .get_total();
787        assert_eq!(yield_amount, expected_yield_amount);
788        assert_eq!(
789            c.market.borrow_asset_balance,
790            10_500_000.into(),
791            "Yield compounding does not affect the market's recorded balance",
792        );
793        assert_eq!(
794            c.market.get_borrow_asset_available_to_borrow(),
795            BorrowAssetAmount::new(9_000_000), // should still be in incoming
796        );
797
798        c.tick(); // move incoming to active
799        assert_eq!(c.market.borrow_asset_balance, 10_500_000.into());
800        assert_eq!(c.market.borrow_asset_deposited_active, 10_000_000.into());
801        // assert_eq!(c.market.supply.r#virtual(), 450_000.into());
802        assert_eq!(
803            c.market.get_borrow_asset_available_to_borrow(),
804            BorrowAssetAmount::new(10_000_000 * 9 / 10),
805        );
806
807        // Withdraw supply: initial
808        let initial = c.withdraw_supply_initial(supplier.clone(), 10_450_000);
809        let initial = match initial {
810            WithdrawalAttempt::Full(initial) => initial,
811            a => {
812                panic!("Should be full withdrawal: {a:?}");
813            }
814        };
815        assert_eq!(c.market.borrow_asset_balance, 50_000.into());
816        assert_eq!(c.market.get_borrow_asset_available_to_borrow(), 0.into());
817
818        // Withdraw supply: final
819        c.withdraw_supply_final(supplier.clone(), &initial);
820        assert_eq!(c.market.borrow_asset_balance, 50_000.into());
821        assert_eq!(c.market.get_borrow_asset_available_to_borrow(), 0.into());
822    }
823
824    #[rstest::rstest]
825    #[should_panic = "InsufficientBorrowAssetAvailable"]
826    #[case(65_000_000)]
827    #[case(63_500_000)]
828    fn balance_2(#[case] second_borrow_amount: u128) {
829        let mut configuration = configuration();
830
831        configuration.borrow_origination_fee = Fee::Flat(15_000_000.into());
832        configuration.borrow_interest_rate_strategy = InterestRateStrategy::zero();
833        configuration.borrow_asset_maximum_usage_ratio = Decimal::ONE;
834
835        let mut c = TestMarketController::new(configuration);
836
837        let supply_id: AccountId = "supply.near".parse().unwrap();
838        let borrow_id: AccountId = "borrow.near".parse().unwrap();
839        let borrow_2_id: AccountId = "borrow2.near".parse().unwrap();
840
841        // Supply 100
842        c.supply(supply_id.clone(), 100_000_000);
843        assert_eq!(c.market.borrow_asset_balance, 100_000_000.into());
844        assert_eq!(
845            c.market.get_borrow_asset_available_to_borrow(),
846            0.into() // still in incoming
847        );
848
849        // Collateralize 200
850        c.collateralize(borrow_id.clone(), 200_000_000);
851        assert_eq!(c.market.borrow_asset_balance, 100_000_000.into());
852        assert_eq!(
853            c.market.get_borrow_asset_available_to_borrow(),
854            100_000_000.into()
855        );
856
857        // Borrow 100 initial
858        let initial = c.borrow_initial(borrow_id.clone(), 100_000_000);
859        assert_eq!(c.market.borrow_asset_balance, 0.into());
860        assert_eq!(c.market.get_borrow_asset_available_to_borrow(), 0.into());
861
862        // Borrow final
863        c.borrow_final(borrow_id.clone(), &initial);
864        assert_eq!(c.market.borrow_asset_balance, 0.into());
865        assert_eq!(c.market.get_borrow_asset_available_to_borrow(), 0.into());
866
867        // Borrow repay 100% + fees
868        let amount_repaid = c
869            .accumulate_interest(borrow_id.clone())
870            .get_total_borrow_asset_liability();
871        let amount_remaining = c.repay(borrow_id.clone(), amount_repaid);
872        assert_eq!(amount_remaining, 0.into());
873        assert_eq!(c.market.borrow_asset_balance, amount_repaid);
874        assert_eq!(
875            c.market.get_borrow_asset_available_to_borrow(),
876            100_000_000.into()
877        );
878
879        // Supplier withdraws 50: initial
880        let yield_amount = c
881            .accumulate_yield(supply_id.clone())
882            .borrow_asset_yield
883            .get_total();
884        assert_eq!(
885            c.market.get_borrow_asset_available_to_borrow(),
886            BorrowAssetAmount::new(100_000_000)
887        );
888        let withdrawal = c.withdraw_supply_initial(supply_id.clone(), 50_000_000);
889        let WithdrawalAttempt::Full(withdrawal) = withdrawal else {
890            panic!("Expected full withdrawal");
891        };
892        assert_eq!(
893            c.market.borrow_asset_balance,
894            amount_repaid - BorrowAssetAmount::new(50_000_000),
895        );
896        c.print();
897        assert_eq!(
898            BorrowAssetAmount::new(50_000_000) + yield_amount,
899            63_500_000.into(),
900        );
901        assert_eq!(
902            c.market.get_borrow_asset_available_to_borrow(),
903            63_500_000.into(),
904        );
905
906        // Supply withdrawal final
907        c.withdraw_supply_final(supply_id.clone(), &withdrawal);
908        c.print();
909        assert_eq!(
910            c.market.borrow_asset_balance,
911            amount_repaid - BorrowAssetAmount::new(50_000_000),
912        );
913        assert_eq!(c.market.borrow_asset_deposited_incoming.len(), 0);
914        assert_eq!(
915            c.market.get_borrow_asset_available_to_borrow(),
916            63_500_000.into(),
917        );
918
919        c.accumulate_yield(supply_id.clone());
920        c.print();
921        assert_eq!(
922            c.market.get_borrow_asset_available_to_borrow(),
923            63_500_000.into(),
924        );
925
926        // Collateralize2 200
927        c.collateralize(borrow_2_id.clone(), 200_000_000);
928        assert_eq!(
929            c.market.borrow_asset_balance,
930            amount_repaid - BorrowAssetAmount::new(50_000_000),
931        );
932        assert_eq!(
933            c.market.get_borrow_asset_available_to_borrow(),
934            63_500_000.into(),
935        );
936
937        c.print();
938
939        // Borrow2 initial
940        let initial = c.borrow_initial(borrow_2_id.clone(), second_borrow_amount);
941        assert_eq!(
942            c.market.borrow_asset_balance,
943            amount_repaid - BorrowAssetAmount::new(50_000_000) - second_borrow_amount,
944        );
945        assert_eq!(
946            c.market.get_borrow_asset_available_to_borrow(),
947            BorrowAssetAmount::new(63_500_000) - second_borrow_amount,
948        );
949
950        eprintln!("Borrow2 final");
951        eprintln!("{initial:?}");
952        eprintln!("{}", c.market.borrow_asset_borrowed_in_flight);
953
954        // Borrow2 final
955        c.borrow_final(borrow_2_id.clone(), &initial);
956        assert_eq!(
957            c.market.borrow_asset_balance,
958            amount_repaid - BorrowAssetAmount::new(50_000_000) - second_borrow_amount,
959        );
960
961        c.print();
962    }
963
964    #[test]
965    fn balance_3() {
966        let mut configuration = configuration();
967        configuration.borrow_origination_fee = Fee::Flat(10_000_000.into());
968        configuration.borrow_interest_rate_strategy = InterestRateStrategy::zero();
969        configuration.yield_weights = YieldWeights::new_with_supply_weight(1);
970        configuration.borrow_asset_maximum_usage_ratio = Decimal::ONE;
971
972        let supply_id: AccountId = "supply.near".parse().unwrap();
973        let borrow_id: AccountId = "borrow.near".parse().unwrap();
974
975        let mut c = TestMarketController::new(configuration);
976
977        // Supply
978        c.supply(supply_id.clone(), 100_000_000);
979        assert_eq!(c.market.borrow_asset_balance, 100_000_000.into());
980        assert_eq!(
981            c.market.get_borrow_asset_available_to_borrow(),
982            0.into(),
983            "still incoming, not yet active",
984        );
985
986        // Collateralize
987        c.collateralize(borrow_id.clone(), 100_000_000);
988        assert_eq!(c.market.borrow_asset_balance, 100_000_000.into());
989        assert_eq!(
990            c.market.get_borrow_asset_available_to_borrow(),
991            100_000_000.into()
992        );
993
994        // Borrow: initial
995        let initial = c.borrow_initial(borrow_id.clone(), 60_000_000);
996        c.print();
997        assert_eq!(c.market.borrow_asset_balance, 40_000_000.into());
998        assert_eq!(
999            c.market.get_borrow_asset_available_to_borrow(),
1000            40_000_000.into()
1001        );
1002
1003        // Borrow: final
1004        c.borrow_final(borrow_id.clone(), &initial);
1005        assert_eq!(c.market.borrow_asset_borrowed, 60_000_000.into());
1006        assert_eq!(c.market.borrow_asset_balance, 40_000_000.into());
1007        assert_eq!(
1008            c.market.get_borrow_asset_available_to_borrow(),
1009            40_000_000.into()
1010        );
1011
1012        c.accumulate_yield(supply_id.clone())
1013            .borrow_asset_yield
1014            .get_total();
1015        assert_eq!(c.market.borrow_asset_borrowed, 60_000_000.into());
1016        assert_eq!(c.market.borrow_asset_balance, 40_000_000.into());
1017        assert_eq!(
1018            c.market.get_borrow_asset_available_to_borrow(),
1019            40_000_000.into()
1020        );
1021
1022        c.tick();
1023        c.tick();
1024        c.print();
1025
1026        assert_eq!(c.market.borrow_asset_borrowed, 60_000_000.into());
1027        assert_eq!(c.market.borrow_asset_balance, 40_000_000.into());
1028        assert_eq!(
1029            c.market.get_borrow_asset_available_to_borrow(),
1030            40_000_000.into()
1031        );
1032    }
1033}