templar_market_contract/
impl_helper.rs

1use near_sdk::{env, near, require, serde_json, AccountId, Gas, Promise, PromiseResult};
2use templar_common::{
3    asset::{
4        BorrowAsset, BorrowAssetAmount, CollateralAsset, CollateralAssetAmount, FungibleAsset,
5        ReturnStyle,
6    },
7    borrow::InitialBorrow,
8    market::{LiquidateMsg, Withdrawal},
9    oracle::pyth::OracleResponse,
10    price::PricePair,
11    self_ext,
12    withdrawal_queue::WithdrawalQueueExecutionResult,
13};
14
15use crate::{Contract, ContractExt};
16
17/// Internal helpers.
18impl Contract {
19    pub fn retrieve_price_pair(&self) -> Promise {
20        self.configuration
21            .price_oracle_configuration
22            .retrieve_price_pair()
23    }
24
25    pub fn price_pair(&self, oracle_response: OracleResponse) -> PricePair {
26        self.configuration
27            .price_oracle_configuration
28            .create_price_pair(&oracle_response)
29            .unwrap_or_else(|e| templar_common::panic_with_message(&e.to_string()))
30    }
31
32    pub fn execute_supply(&mut self, account_id: AccountId, amount: BorrowAssetAmount) {
33        if self.supply_position_ref(account_id.clone()).is_none() {
34            self.charge_for_storage(
35                &account_id,
36                self.storage_usage_supply_position + self.storage_usage_snapshot * 2,
37            );
38        }
39
40        let snapshot = self.snapshot();
41        let mut supply_position = self.get_or_create_supply_position_guard(snapshot, account_id);
42        let proof = supply_position.accumulate_yield();
43        supply_position.record_deposit(proof, amount, env::block_timestamp_ms());
44        require!(
45            supply_position.is_within_allowable_range(),
46            "New supply position is outside of allowable range",
47        );
48    }
49
50    pub fn execute_collateralize(
51        &mut self,
52        account_id: AccountId,
53        amount: CollateralAssetAmount,
54        price_pair: &PricePair,
55    ) {
56        // TODO: This creates a borrow record implicitly. If we
57        // require a discrete "sign-up" step, we will need to add
58        // checks before this function call.
59        //
60        // The sign-up step would only be NFT gating or something of
61        // that sort, which is just an additional pre condition check.
62        // -- https://github.com/Templar-Protocol/contract-mvp/pull/6#discussion_r1923871982
63        if self.borrow_position_ref(account_id.clone()).is_none() {
64            self.charge_for_storage(
65                &account_id,
66                self.storage_usage_borrow_position + self.storage_usage_snapshot * 2,
67            );
68        }
69
70        let snapshot = self.snapshot();
71        let mut borrow_position = self.get_or_create_borrow_position_guard(snapshot, account_id);
72        let proof = borrow_position.accumulate_interest();
73        borrow_position.record_collateral_asset_deposit(proof, amount);
74        require!(
75            !borrow_position
76                .status(price_pair, env::block_timestamp_ms())
77                .is_liquidation(),
78            "Position is eligible for liquidation after collateralization",
79        );
80    }
81
82    /// Returns the amount that should be returned to the account.
83    pub fn execute_repay(
84        &mut self,
85        account_id: AccountId,
86        amount: BorrowAssetAmount,
87        price_pair: &PricePair,
88    ) -> BorrowAssetAmount {
89        let snapshot = self.snapshot();
90        let Some(mut borrow_position) = self.borrow_position_guard(snapshot, account_id) else {
91            // No borrow exists: just return the whole amount.
92            return amount;
93        };
94        let proof = borrow_position.accumulate_interest();
95        require!(
96            !borrow_position
97                .status(price_pair, env::block_timestamp_ms())
98                .is_liquidation(),
99            "Cannot repay when eligible for liquidation",
100        );
101        // Returns the amount that should be returned to the borrower.
102        borrow_position.record_repay(proof, amount)
103    }
104}
105
106/// External helpers.
107#[near]
108impl Contract {
109    // 3.9 Tgas
110    pub const GAS_BORROW_01_CONSUME_PRICE: Gas = Gas::from_tgas(6)
111        .saturating_add(FungibleAsset::<BorrowAsset>::GAS_FT_TRANSFER)
112        .saturating_add(Self::GAS_BORROW_02_FINALIZE);
113
114    #[private]
115    pub fn borrow_01_consume_price(
116        &mut self,
117        account_id: AccountId,
118        amount: BorrowAssetAmount,
119        #[callback_unwrap] oracle_response: OracleResponse,
120    ) -> Promise {
121        let price_pair = self.price_pair(oracle_response);
122        let snapshot = self.snapshot();
123
124        let Some(mut borrow_position) = self.borrow_position_guard(snapshot, account_id.clone())
125        else {
126            templar_common::panic_with_message(
127                "No borrower record. Please deposit collateral first.",
128            );
129        };
130
131        let interest = borrow_position.accumulate_interest();
132
133        let initial_borrow = borrow_position
134            .record_borrow_initial(
135                snapshot,
136                interest,
137                amount,
138                &price_pair,
139                env::block_timestamp_ms(),
140            )
141            .unwrap_or_else(|e| templar_common::panic_with_message(&e.to_string()));
142
143        drop(borrow_position);
144
145        self.configuration
146            .borrow_asset
147            .transfer(account_id.clone(), amount)
148            .then(
149                self_ext!(Self::GAS_BORROW_02_FINALIZE)
150                    .borrow_02_finalize(account_id, initial_borrow),
151            )
152    }
153
154    // 3.1 Tgas
155    pub const GAS_BORROW_02_FINALIZE: Gas = Gas::from_tgas(6);
156
157    #[private]
158    pub fn borrow_02_finalize(&mut self, account_id: AccountId, initial_borrow: InitialBorrow) {
159        let snapshot = self.snapshot();
160        let Some(mut borrow_position) = self.borrow_position_guard(snapshot, account_id) else {
161            templar_common::panic_with_message(
162                "Invariant violation: borrow position does not exist after transfer.",
163            );
164        };
165
166        let proof = borrow_position.accumulate_interest();
167        #[allow(deprecated)]
168        let success = matches!(env::promise_result(0), PromiseResult::Successful(_));
169        borrow_position.record_borrow_final(
170            snapshot,
171            proof,
172            &initial_borrow,
173            success,
174            env::block_timestamp_ms(),
175        );
176    }
177
178    // ~5.8 Tgas
179    pub const GAS_EXECUTE_NEXT_SUPPLY_WITHDRAWAL_REQUEST_01_FINALIZE: Gas = Gas::from_tgas(8);
180
181    #[private]
182    pub fn execute_next_supply_withdrawal_request_01_finalize(
183        &mut self,
184        resolutions: Vec<Withdrawal>,
185        result: WithdrawalQueueExecutionResult,
186    ) -> WithdrawalQueueExecutionResult {
187        let snapshot = self.snapshot();
188
189        for (i, resolution) in resolutions.iter().enumerate() {
190            #[allow(deprecated)]
191            let succeeded = matches!(env::promise_result(i as u64), PromiseResult::Successful(_));
192
193            if let Some(mut position) =
194                self.supply_position_guard(snapshot, resolution.account_id.clone())
195            {
196                position.record_withdrawal_final(resolution, succeeded);
197            }
198
199            if succeeded && self.cleanup_supply_position(&resolution.account_id) {
200                self.refund_for_storage(&resolution.account_id, self.storage_usage_supply_position);
201            }
202        }
203
204        result
205    }
206
207    // ~3.4 TGas
208    pub const GAS_COLLATERALIZE_TRANSFER_CALL_01_CONSUME_PRICE: Gas = Gas::from_tgas(6);
209
210    #[private]
211    pub fn collateralize_transfer_call_01_consume_price(
212        &mut self,
213        account_id: AccountId,
214        amount: CollateralAssetAmount,
215        return_style: ReturnStyle,
216        #[callback_unwrap] oracle_response: OracleResponse,
217    ) -> serde_json::Value {
218        let price_pair = self.price_pair(oracle_response);
219
220        self.execute_collateralize(account_id, amount, &price_pair);
221
222        return_style.serialize(CollateralAssetAmount::zero())
223    }
224
225    // ~4.3 TGas
226    pub const GAS_REPAY_TRANSFER_CALL_01_CONSUME_PRICE: Gas = Gas::from_tgas(7);
227
228    #[private]
229    pub fn repay_transfer_call_01_consume_price(
230        &mut self,
231        account_id: AccountId,
232        amount: BorrowAssetAmount,
233        return_style: ReturnStyle,
234        #[callback_unwrap] oracle_response: OracleResponse,
235    ) -> serde_json::Value {
236        let price_pair = self.price_pair(oracle_response);
237
238        let amount = self.execute_repay(account_id, amount, &price_pair);
239
240        return_style.serialize(amount)
241    }
242
243    // ~5.3 Tgas
244    pub const GAS_LIQUIDATE_TRANSFER_CALL_01_CONSUME_PRICE: Gas = Gas::from_tgas(7)
245        .saturating_add(FungibleAsset::<CollateralAsset>::GAS_FT_TRANSFER)
246        .saturating_add(Self::GAS_LIQUIDATE_TRANSFER_CALL_02_FINALIZE);
247
248    #[private]
249    pub fn liquidate_transfer_call_01_consume_price(
250        &mut self,
251        liquidator_id: AccountId,
252        amount: BorrowAssetAmount,
253        msg: LiquidateMsg,
254        return_style: ReturnStyle,
255        #[callback_unwrap] oracle_response: OracleResponse,
256    ) -> Promise {
257        let price_pair = self
258            .configuration
259            .price_oracle_configuration
260            .create_price_pair(&oracle_response)
261            .unwrap_or_else(|e| templar_common::panic_with_message(&e.to_string()));
262
263        let result = {
264            let snapshot = self.snapshot();
265            let mut borrow_position = self
266                .borrow_position_guard(snapshot, msg.account_id.clone())
267                .unwrap_or_else(|| {
268                    templar_common::panic_with_message("Borrow position does not exist")
269                });
270
271            let proof = borrow_position.accumulate_interest();
272
273            borrow_position
274                .record_liquidation(
275                    proof,
276                    liquidator_id.clone(),
277                    amount,
278                    msg.amount,
279                    &price_pair,
280                    env::block_timestamp_ms(),
281                )
282                .unwrap_or_else(|e| templar_common::panic_with_message(&e.to_string()))
283        };
284
285        if self.cleanup_borrow_position(&msg.account_id) {
286            self.refund_for_storage(&msg.account_id, self.storage_usage_borrow_position);
287        }
288
289        self.configuration
290            .collateral_asset
291            .transfer(liquidator_id.clone(), result.liquidated)
292            .then(
293                self_ext!(Self::GAS_LIQUIDATE_TRANSFER_CALL_02_FINALIZE)
294                    .liquidate_transfer_call_02_finalize(result.refund, return_style),
295            )
296    }
297
298    // ~2.1 Tgas
299    pub const GAS_LIQUIDATE_TRANSFER_CALL_02_FINALIZE: Gas = Gas::from_tgas(4);
300
301    /// Called during liquidation process; checks whether the transfer of
302    /// collateral to the liquidator was successful.
303    #[private]
304    pub fn liquidate_transfer_call_02_finalize(
305        &mut self,
306        refund: BorrowAssetAmount,
307        return_style: ReturnStyle,
308    ) -> serde_json::Value {
309        // let success = matches!(env::promise_result(0), PromiseResult::Successful(_));
310        // If the transfer of collateral failed, it could mean:
311        //
312        // 1. The liquidator has opted-out of storage management for the
313        //  collateral token. This can be due to negligence or malice, but
314        //  we cannot be sure, so we cannot refund the tokens, because
315        //  that would allow a borrow position in liquidation to
316        //  indefinitely lock their collateral from being liquidated.
317        //
318        // 2. Somehow the contract does not have enough collateral
319        //  available. This would be indicative of a *fundamental flaw*
320        //  in the contract (i.e. this should never happen).
321
322        return_style.serialize(refund)
323    }
324
325    // ~5.0 Tgas
326    pub const GAS_WITHDRAW_COLLATERAL_01_CONSUME_PRICE: Gas = Gas::from_tgas(7)
327        .saturating_add(FungibleAsset::<CollateralAsset>::GAS_FT_TRANSFER)
328        .saturating_add(Self::GAS_WITHDRAW_COLLATERAL_02_FINALIZE);
329
330    #[private]
331    pub fn withdraw_collateral_01_consume_price(
332        &mut self,
333        account_id: AccountId,
334        amount: CollateralAssetAmount,
335        #[callback_unwrap] oracle_response: OracleResponse,
336    ) -> Promise {
337        let price_pair = self.price_pair(oracle_response);
338
339        let snapshot = self.snapshot();
340        let Some(mut borrow_position) = self.borrow_position_guard(snapshot, account_id.clone())
341        else {
342            templar_common::panic_with_message(
343                "No borrower record. Please deposit collateral first.",
344            );
345        };
346
347        let proof = borrow_position.accumulate_interest();
348        borrow_position.record_collateral_asset_withdrawal_initial(proof, amount);
349
350        require!(
351            borrow_position
352                .status(&price_pair, env::block_timestamp_ms())
353                .is_healthy(),
354            "Borrow position must be healthy after collateral withdrawal",
355        );
356
357        drop(borrow_position);
358
359        self.configuration
360            .collateral_asset
361            .transfer(account_id.clone(), amount)
362            .then(
363                self_ext!(Self::GAS_WITHDRAW_COLLATERAL_02_FINALIZE)
364                    .withdraw_collateral_02_finalize(account_id, amount),
365            )
366    }
367
368    // ~2.2 Tgas
369    pub const GAS_WITHDRAW_COLLATERAL_02_FINALIZE: Gas = Gas::from_tgas(5);
370
371    #[private]
372    pub fn withdraw_collateral_02_finalize(
373        &mut self,
374        account_id: AccountId,
375        amount: CollateralAssetAmount,
376    ) {
377        #[allow(deprecated)]
378        let succeeded = matches!(env::promise_result(0), PromiseResult::Successful(_));
379
380        let snapshot = self.snapshot();
381        let Some(mut position) = self.borrow_position_guard(snapshot, account_id.clone()) else {
382            templar_common::panic_with_message(
383                "Invariant violation: Borrow position must exist after collateral withdrawal.",
384            );
385        };
386
387        let proof = position.accumulate_interest();
388        position.record_collateral_asset_withdrawal_final(proof, amount, succeeded);
389
390        if succeeded {
391            drop(position);
392            if self.cleanup_borrow_position(&account_id) {
393                self.refund_for_storage(&account_id, self.storage_usage_borrow_position);
394            }
395        }
396    }
397
398    // ~2.1 Tgas
399    pub const GAS_WITHDRAW_STATIC_YIELD_01_FINALIZE: Gas = Gas::from_tgas(5);
400
401    #[private]
402    pub fn withdraw_static_yield_01_finalize(
403        &mut self,
404        account_id: AccountId,
405        amount: BorrowAssetAmount,
406    ) {
407        #[allow(deprecated)]
408        if matches!(env::promise_result(0), PromiseResult::Failed) {
409            let mut yield_record = self.static_yield.get(&account_id).unwrap_or_else(|| {
410                templar_common::panic_with_message(
411                    "Invariant violation: static yield entry must exist during callback",
412                )
413            });
414            yield_record.add_once(amount);
415            self.static_yield.insert(&account_id, &yield_record);
416            self.borrow_asset_balance += amount;
417        }
418    }
419}