templar_market_contract/
impl_market_external.rs

1use std::collections::HashMap;
2
3use near_sdk::{env, near, require, AccountId, Promise, PromiseOrValue};
4use templar_common::{
5    accumulator::Accumulator,
6    asset::{BorrowAsset, BorrowAssetAmount, CollateralAssetAmount},
7    borrow::{BorrowPosition, BorrowStatus},
8    contract::list,
9    market::{BorrowAssetMetrics, HarvestYieldMode, MarketConfiguration, MarketExternalInterface},
10    oracle::pyth::OracleResponse,
11    self_ext,
12    snapshot::Snapshot,
13    supply::{SupplyPosition, WithdrawalAttempt},
14    withdrawal_queue::{
15        WithdrawalQueueExecutionResult, WithdrawalQueueStatus, WithdrawalRequestStatus,
16    },
17};
18use templar_primitives::Decimal;
19
20use crate::{Contract, ContractExt};
21
22#[near]
23impl MarketExternalInterface for Contract {
24    fn get_configuration(&self) -> MarketConfiguration {
25        self.configuration.clone()
26    }
27
28    fn get_current_snapshot(&self) -> Snapshot {
29        self.current_snapshot()
30    }
31
32    fn get_finalized_snapshots_len(&self) -> u32 {
33        self.finalized_snapshots.len()
34    }
35
36    fn get_borrow_asset_metrics(&self) -> BorrowAssetMetrics {
37        BorrowAssetMetrics {
38            available: self.get_borrow_asset_available_to_borrow(),
39            deposited_active: self.borrow_asset_deposited_active,
40            deposited_incoming: self
41                .borrow_asset_deposited_incoming
42                .iter()
43                .map(|incoming| (incoming.activate_at_snapshot_index, incoming.amount))
44                .collect(),
45            borrowed: self.borrowed(),
46            paid_to_fees: self.borrow_asset_paid_to_fees,
47        }
48    }
49
50    fn list_borrow_positions(
51        &self,
52        offset: Option<u32>,
53        count: Option<u32>,
54    ) -> HashMap<AccountId, BorrowPosition> {
55        list(self.iter_borrow_positions(), offset, count)
56    }
57
58    fn list_finalized_snapshots(&self, offset: Option<u32>, count: Option<u32>) -> Vec<&Snapshot> {
59        list(&self.finalized_snapshots, offset, count)
60    }
61
62    fn list_supply_positions(
63        &self,
64        offset: Option<u32>,
65        count: Option<u32>,
66    ) -> HashMap<AccountId, SupplyPosition> {
67        list(self.iter_supply_positions(), offset, count)
68    }
69
70    fn get_borrow_position(&self, account_id: AccountId) -> Option<BorrowPosition> {
71        let borrow_position = self.borrow_position_ref(account_id)?;
72        Some(borrow_position.inner().clone())
73    }
74
75    fn get_borrow_position_pending_interest(
76        &self,
77        account_id: AccountId,
78        snapshot_limit: Option<u32>,
79    ) -> Option<BorrowAssetAmount> {
80        let limit = snapshot_limit.unwrap_or(u32::MAX);
81        let position = self.borrow_position_ref(account_id)?;
82        Some(position.calculate_interest(limit).get_amount())
83    }
84
85    fn get_borrow_status(
86        &self,
87        account_id: AccountId,
88        oracle_response: OracleResponse,
89    ) -> Option<BorrowStatus> {
90        let borrow_position = self.borrow_position_ref(account_id)?;
91
92        let price_pair = self
93            .configuration
94            .price_oracle_configuration
95            .create_price_pair(&oracle_response)
96            .unwrap_or_else(|e| templar_common::panic_with_message(&e.to_string()));
97
98        Some(borrow_position.status(&price_pair, env::block_timestamp_ms()))
99    }
100
101    fn borrow(&mut self, amount: BorrowAssetAmount) -> Promise {
102        require!(!amount.is_zero(), "Borrow amount must be greater than zero");
103        let account_id = env::predecessor_account_id();
104        require!(
105            self.borrow_position_ref(account_id.clone()).is_some(),
106            "Borrow position does not exist",
107        );
108
109        self.configuration
110            .price_oracle_configuration
111            .retrieve_price_pair()
112            .then(
113                self_ext!(Self::GAS_BORROW_01_CONSUME_PRICE)
114                    .borrow_01_consume_price(account_id, amount),
115            )
116    }
117
118    fn withdraw_collateral(&mut self, amount: CollateralAssetAmount) -> Promise {
119        let account_id = env::predecessor_account_id();
120
121        let snapshot = self.snapshot();
122        let Some(mut borrow_position) = self.borrow_position_guard(snapshot, account_id.clone())
123        else {
124            templar_common::panic_with_message(
125                "No borrower record. Please deposit collateral first.",
126            );
127        };
128
129        if borrow_position
130            .inner()
131            .get_total_borrow_asset_liability()
132            .is_zero()
133        {
134            // No need to retrieve prices, since there is zero liability.
135            let proof = borrow_position.accumulate_interest();
136            borrow_position.record_collateral_asset_withdrawal_initial(proof, amount);
137            drop(borrow_position);
138
139            self.configuration
140                .collateral_asset
141                .transfer(account_id.clone(), amount)
142                .then(
143                    self_ext!(Self::GAS_WITHDRAW_COLLATERAL_02_FINALIZE)
144                        .withdraw_collateral_02_finalize(account_id, amount),
145                )
146        } else {
147            drop(borrow_position);
148            // They still have liability, so we need to check prices.
149            self.configuration
150                .price_oracle_configuration
151                .retrieve_price_pair()
152                .then(
153                    self_ext!(Self::GAS_WITHDRAW_COLLATERAL_01_CONSUME_PRICE)
154                        .withdraw_collateral_01_consume_price(account_id, amount),
155                )
156        }
157    }
158
159    fn apply_interest(&mut self, account_id: Option<AccountId>, snapshot_limit: Option<u32>) {
160        let account_id = account_id.unwrap_or_else(env::predecessor_account_id);
161        let snapshot = self.snapshot();
162        if let Some(mut borrow_position) = self.borrow_position_guard(snapshot, account_id) {
163            borrow_position.accumulate_interest_partial(snapshot_limit.unwrap_or(u32::MAX));
164        }
165    }
166
167    fn get_supply_position(&self, account_id: AccountId) -> Option<SupplyPosition> {
168        let position = self.supply_position_ref(account_id)?;
169        Some(position.inner().clone())
170    }
171
172    fn get_supply_position_pending_yield(
173        &self,
174        account_id: AccountId,
175        snapshot_limit: Option<u32>,
176    ) -> Option<BorrowAssetAmount> {
177        let limit = snapshot_limit.unwrap_or(u32::MAX);
178        let position = self.supply_position_ref(account_id)?;
179        Some(position.calculate_yield(limit).get_amount())
180    }
181
182    /// If the predecessor has already entered the queue, calling this function
183    /// will reset the position to the back of the queue.
184    fn create_supply_withdrawal_request(&mut self, amount: BorrowAssetAmount) {
185        require!(
186            !amount.is_zero(),
187            "Amount to withdraw must be greater than zero",
188        );
189        let predecessor = env::predecessor_account_id();
190        let Some(supply_position) = self
191            .supply_position_ref(predecessor.clone())
192            .filter(|supply_position| !supply_position.total_deposit().is_zero())
193        else {
194            templar_common::panic_with_message("Supply position does not exist");
195        };
196
197        // We do check here, as well as during the execution.
198        // This check really only ensures that the `depth` reported by
199        // get_supply_withdrawal_queue_status() is realistically accurate.
200        require!(
201            supply_position.total_deposit() >= amount,
202            "Attempt to withdraw more than current deposit",
203        );
204        require!(
205            self.configuration.supply_withdrawal_range.contains(amount),
206            "Withdrawal amount is outside of allowable range",
207        );
208
209        self.withdrawal_queue.remove(&predecessor);
210        self.withdrawal_queue.insert_or_update(&predecessor, amount);
211    }
212
213    fn cancel_supply_withdrawal_request(&mut self) {
214        self.withdrawal_queue.remove(&env::predecessor_account_id());
215    }
216
217    fn execute_next_supply_withdrawal_request(
218        &mut self,
219        batch_limit: Option<u32>,
220    ) -> PromiseOrValue<WithdrawalQueueExecutionResult> {
221        let batch_limit = batch_limit.unwrap_or(1);
222        let mut batch = Vec::with_capacity(batch_limit.min(self.withdrawal_queue.len()) as usize);
223        let snapshot_proof = self.snapshot();
224        let block_timestamp_ms = env::block_timestamp_ms();
225        let queue_len_start = self.withdrawal_queue.len();
226        let mut depth_cleared = BorrowAssetAmount::zero();
227
228        while let Some((account_id, requested_amount)) = self.withdrawal_queue.peek() {
229            if batch.len() >= batch_limit as usize {
230                break;
231            }
232
233            let withdrawal_attempt = {
234                let Some(mut position_guard) =
235                    self.supply_position_guard(snapshot_proof, account_id.clone())
236                else {
237                    // Somehow the account does not exist. This should not happen,
238                    // but it is recoverable if it does.
239                    self.withdrawal_queue.pop();
240                    depth_cleared += requested_amount;
241                    continue;
242                };
243                let accumulation_proof = position_guard.accumulate_yield();
244                position_guard.record_withdrawal_initial(
245                    accumulation_proof,
246                    requested_amount,
247                    block_timestamp_ms,
248                )
249            };
250
251            match withdrawal_attempt {
252                WithdrawalAttempt::EmptyPosition => {
253                    self.withdrawal_queue.pop();
254                    depth_cleared += requested_amount;
255                }
256                WithdrawalAttempt::NoLiquidity => {
257                    break;
258                }
259                WithdrawalAttempt::Full(withdrawal) => {
260                    batch.push(withdrawal);
261                    self.withdrawal_queue.pop();
262                    depth_cleared += requested_amount;
263                }
264                WithdrawalAttempt::Partial {
265                    withdrawal,
266                    remaining,
267                } => {
268                    batch.push(withdrawal);
269                    self.withdrawal_queue.mut_head(|a| *a = remaining);
270                    depth_cleared += requested_amount - remaining;
271                    break;
272                }
273            }
274        }
275
276        let result = WithdrawalQueueExecutionResult {
277            depth: depth_cleared,
278            length: queue_len_start - self.withdrawal_queue.len(),
279        };
280
281        let Some(transfers) = batch
282            .iter()
283            .map(|resolution| {
284                self.configuration
285                    .borrow_asset
286                    .transfer(resolution.account_id.clone(), resolution.amount_to_account)
287            })
288            .reduce(|a, b| a.and(b))
289        else {
290            return PromiseOrValue::Value(result);
291        };
292
293        PromiseOrValue::Promise(
294            transfers.then(
295                self_ext!(Self::GAS_EXECUTE_NEXT_SUPPLY_WITHDRAWAL_REQUEST_01_FINALIZE)
296                    .execute_next_supply_withdrawal_request_01_finalize(batch, result),
297            ),
298        )
299    }
300
301    fn get_supply_withdrawal_request_status(
302        &self,
303        account_id: AccountId,
304    ) -> Option<WithdrawalRequestStatus> {
305        self.withdrawal_queue.get_request_status(&account_id)
306    }
307
308    fn get_supply_withdrawal_queue_status(&self) -> WithdrawalQueueStatus {
309        self.withdrawal_queue.get_status()
310    }
311
312    fn harvest_yield(
313        &mut self,
314        account_id: Option<AccountId>,
315        mode: Option<HarvestYieldMode>,
316    ) -> BorrowAssetAmount {
317        let mode = mode.unwrap_or_default();
318        let predecessor = env::predecessor_account_id();
319        let account_id = account_id.unwrap_or_else(|| predecessor.clone());
320
321        let snapshot = self.snapshot();
322        let Some(mut supply_position) = self.supply_position_guard(snapshot, account_id) else {
323            return BorrowAssetAmount::zero();
324        };
325
326        match mode {
327            HarvestYieldMode::SnapshotLimit(limit) => {
328                supply_position.accumulate_yield_partial(limit);
329            }
330            HarvestYieldMode::Default => {
331                supply_position.accumulate_yield();
332            }
333        }
334
335        BorrowAssetAmount::zero()
336    }
337
338    fn get_last_yield_rate(&self) -> Decimal {
339        self.configuration
340            .supply_yield_rate_from_interest(&self.current_snapshot())
341    }
342
343    fn get_static_yield(&self, account_id: AccountId) -> Option<Accumulator<BorrowAsset>> {
344        self.static_yield.get(&account_id)
345    }
346
347    fn accumulate_static_yield(
348        &mut self,
349        account_id: Option<AccountId>,
350        snapshot_limit: Option<u32>,
351    ) {
352        self.market
353            .accumulate_static_yield(
354                &account_id.unwrap_or_else(env::predecessor_account_id),
355                snapshot_limit.unwrap_or(u32::MAX),
356            )
357            .unwrap_or_else(|_| {
358                templar_common::panic_with_message("This account does not earn static yield")
359            });
360    }
361
362    fn withdraw_static_yield(&mut self, amount: Option<BorrowAssetAmount>) -> Promise {
363        let predecessor = env::predecessor_account_id();
364        let Some(mut yield_record) = self.static_yield.get(&predecessor) else {
365            templar_common::panic_with_message("Yield record does not exist");
366        };
367
368        let available = yield_record.get_total();
369        let amount = amount.unwrap_or(available);
370        require!(
371            amount <= available,
372            "Attempt to withdraw more than accumulated static yield",
373        );
374
375        yield_record.remove(amount);
376
377        self.static_yield.insert(&predecessor, &yield_record);
378        self.borrow_asset_balance = self.borrow_asset_balance.unwrap_sub(
379            amount,
380            "Invariant violation: static yield withdrawal exceeds market borrow asset balance",
381        );
382
383        self.configuration
384            .borrow_asset
385            .transfer(predecessor.clone(), amount)
386            .then(
387                self_ext!(Self::GAS_WITHDRAW_STATIC_YIELD_01_FINALIZE)
388                    .withdraw_static_yield_01_finalize(predecessor, amount),
389            )
390    }
391}