kaccy-core 0.2.0

Core business logic for Kaccy Protocol - batching, fee optimization, and transaction management
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
//! Lending and borrowing system for DeFi primitives
//!
//! This module provides:
//! - Collateralized lending pools
//! - Interest rate models (linear and exponential)
//! - Liquidation engine with health factor monitoring
//! - Borrow/repay operations with automatic interest accrual

use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use uuid::Uuid;

/// Interest rate model type
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum InterestRateModel {
    /// Linear interest rate: base_rate + utilization * slope
    Linear {
        /// Minimum interest rate at zero utilization.
        base_rate: Decimal,
        /// Rate increase per unit of utilization.
        slope: Decimal,
    },
    /// Exponential interest rate: base_rate * e^(utilization * multiplier)
    Exponential {
        /// Minimum interest rate at zero utilization.
        base_rate: Decimal,
        /// Exponential growth multiplier.
        multiplier: Decimal,
    },
    /// Kinked rate model (optimal utilization with kink)
    Kinked {
        /// Minimum interest rate at zero utilization.
        base_rate: Decimal,
        /// Rate slope below the optimal utilization point.
        slope1: Decimal,
        /// Rate slope above the optimal utilization point.
        slope2: Decimal,
        /// Utilization ratio at which the slope changes.
        optimal_utilization: Decimal,
    },
}

impl InterestRateModel {
    /// Calculate interest rate based on utilization
    pub fn calculate_rate(&self, utilization: Decimal) -> Decimal {
        match self {
            Self::Linear { base_rate, slope } => base_rate + (utilization * slope),
            Self::Exponential {
                base_rate,
                multiplier,
            } => {
                // Approximate e^x using Taylor series for small x
                let x = utilization * multiplier;
                let exp_x = dec!(1) + x + (x * x / dec!(2)) + (x * x * x / dec!(6));
                base_rate * exp_x
            }
            Self::Kinked {
                base_rate,
                slope1,
                slope2,
                optimal_utilization,
            } => {
                if utilization <= *optimal_utilization {
                    base_rate + (utilization * slope1)
                } else {
                    let base = base_rate + (optimal_utilization * slope1);
                    base + ((utilization - optimal_utilization) * slope2)
                }
            }
        }
    }
}

/// Lending pool for a specific asset
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LendingPool {
    /// Unique identifier for this lending pool
    pub pool_id: Uuid,
    /// Asset supported by this pool
    pub asset_id: Uuid,
    /// Total amount deposited into the pool
    pub total_deposits: Decimal,
    /// Total amount currently borrowed from the pool
    pub total_borrows: Decimal,
    /// Model used to calculate interest rates
    pub interest_rate_model: InterestRateModel,
    /// Collateral factor (0.0 to 1.0) - how much can be borrowed against deposits
    pub collateral_factor: Decimal,
    /// Liquidation threshold (0.0 to 1.0)
    pub liquidation_threshold: Decimal,
    /// Liquidation penalty (e.g., 0.05 = 5%)
    pub liquidation_penalty: Decimal,
    /// Reserve factor (portion of interest going to reserves)
    pub reserve_factor: Decimal,
    /// Unix timestamp when the pool was created
    pub created_at: i64,
    /// Unix timestamp of the most recent pool state update
    pub last_update: i64,
}

impl LendingPool {
    /// Create a new lending pool with the given parameters
    pub fn new(
        asset_id: Uuid,
        interest_rate_model: InterestRateModel,
        collateral_factor: Decimal,
        liquidation_threshold: Decimal,
        liquidation_penalty: Decimal,
    ) -> Result<Self, &'static str> {
        if collateral_factor > dec!(1) || collateral_factor < Decimal::ZERO {
            return Err("Collateral factor must be between 0 and 1");
        }

        if liquidation_threshold > dec!(1) || liquidation_threshold < Decimal::ZERO {
            return Err("Liquidation threshold must be between 0 and 1");
        }

        if liquidation_penalty > dec!(1) || liquidation_penalty < Decimal::ZERO {
            return Err("Liquidation penalty must be between 0 and 1");
        }

        let now = chrono::Utc::now().timestamp();
        Ok(Self {
            pool_id: Uuid::new_v4(),
            asset_id,
            total_deposits: Decimal::ZERO,
            total_borrows: Decimal::ZERO,
            interest_rate_model,
            collateral_factor,
            liquidation_threshold,
            liquidation_penalty,
            reserve_factor: dec!(0.1), // 10% to reserves by default
            created_at: now,
            last_update: now,
        })
    }

    /// Calculate current utilization rate
    pub fn utilization_rate(&self) -> Decimal {
        if self.total_deposits == Decimal::ZERO {
            Decimal::ZERO
        } else {
            self.total_borrows / self.total_deposits
        }
    }

    /// Calculate current borrow APR
    pub fn borrow_apr(&self) -> Decimal {
        let utilization = self.utilization_rate();
        self.interest_rate_model.calculate_rate(utilization)
    }

    /// Calculate current supply APR
    pub fn supply_apr(&self) -> Decimal {
        let borrow_apr = self.borrow_apr();
        let utilization = self.utilization_rate();
        borrow_apr * utilization * (dec!(1) - self.reserve_factor)
    }

    /// Get available liquidity
    pub fn available_liquidity(&self) -> Decimal {
        self.total_deposits - self.total_borrows
    }
}

/// User position in lending pool
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LendingPosition {
    /// Unique identifier for this position
    pub position_id: Uuid,
    /// User who owns this position
    pub user_id: Uuid,
    /// Pool this position belongs to
    pub pool_id: Uuid,
    /// Amount the user has deposited into the pool
    pub deposited: Decimal,
    /// Amount the user has borrowed from the pool
    pub borrowed: Decimal,
    /// Accumulated interest since last update
    pub accrued_interest: Decimal,
    /// Unix timestamp of the most recent position update
    pub last_update: i64,
}

impl LendingPosition {
    /// Create a new empty lending position for the given user and pool
    pub fn new(user_id: Uuid, pool_id: Uuid) -> Self {
        let now = chrono::Utc::now().timestamp();
        Self {
            position_id: Uuid::new_v4(),
            user_id,
            pool_id,
            deposited: Decimal::ZERO,
            borrowed: Decimal::ZERO,
            accrued_interest: Decimal::ZERO,
            last_update: now,
        }
    }

    /// Calculate total debt (borrowed + accrued interest)
    pub fn total_debt(&self) -> Decimal {
        self.borrowed + self.accrued_interest
    }

    /// Accrue interest based on time elapsed and borrow rate
    pub fn accrue_interest(&mut self, borrow_apr: Decimal, current_time: i64) {
        if self.borrowed == Decimal::ZERO {
            self.last_update = current_time;
            return;
        }

        let time_elapsed = Decimal::from(current_time - self.last_update);
        let seconds_per_year = dec!(31536000); // 365 days

        // Interest = principal * rate * time
        let interest = self.borrowed * borrow_apr * time_elapsed / seconds_per_year;
        self.accrued_interest += interest;
        self.last_update = current_time;
    }
}

/// Health factor calculation for liquidation monitoring
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HealthFactor {
    /// User whose health factor this describes
    pub user_id: Uuid,
    /// USD/BTC value of all collateral across pools
    pub total_collateral_value: Decimal,
    /// USD/BTC value of total outstanding debt including accrued interest
    pub total_debt_value: Decimal,
    /// Computed health factor; below 1.0 the position is liquidatable
    pub health_factor: Decimal,
    /// Weighted average liquidation threshold across all positions
    pub liquidation_threshold: Decimal,
}

impl HealthFactor {
    /// Calculate health factor
    /// health_factor = (collateral_value * liquidation_threshold) / debt_value
    /// health_factor < 1.0 means position can be liquidated
    pub fn calculate(
        total_collateral_value: Decimal,
        total_debt_value: Decimal,
        liquidation_threshold: Decimal,
    ) -> Decimal {
        if total_debt_value == Decimal::ZERO {
            return Decimal::MAX; // No debt = infinite health
        }

        (total_collateral_value * liquidation_threshold) / total_debt_value
    }

    /// Return true if this position can currently be liquidated
    pub fn is_liquidatable(&self) -> bool {
        self.health_factor < dec!(1)
    }

    /// Maximum debt amount that can be repaid in a single liquidation call
    pub fn liquidation_amount(&self) -> Decimal {
        if !self.is_liquidatable() {
            return Decimal::ZERO;
        }

        // Can liquidate up to 50% of debt to restore health
        self.total_debt_value / dec!(2)
    }
}

/// Record of a liquidation event
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Liquidation {
    /// Unique identifier for this liquidation
    pub liquidation_id: Uuid,
    /// User who performed the liquidation
    pub liquidator_id: Uuid,
    /// User whose position was liquidated
    pub borrower_id: Uuid,
    /// Pool in which the liquidation occurred
    pub pool_id: Uuid,
    /// Debt amount repaid by the liquidator
    pub debt_repaid: Decimal,
    /// Collateral seized from the borrower
    pub collateral_seized: Decimal,
    /// Bonus amount earned by the liquidator
    pub liquidation_bonus: Decimal,
    /// Unix timestamp when the liquidation occurred
    pub timestamp: i64,
}

/// Lending manager
pub struct LendingManager {
    /// All active lending pools indexed by pool ID
    pools: Arc<RwLock<HashMap<Uuid, LendingPool>>>,
    /// User positions keyed by (user_id, pool_id)
    positions: Arc<RwLock<HashMap<(Uuid, Uuid), LendingPosition>>>,
    /// Current asset prices used for health factor calculation
    asset_prices: Arc<RwLock<HashMap<Uuid, Decimal>>>,
}

impl LendingManager {
    /// Create a new lending manager with no pools or positions
    pub fn new() -> Self {
        Self {
            pools: Arc::new(RwLock::new(HashMap::new())),
            positions: Arc::new(RwLock::new(HashMap::new())),
            asset_prices: Arc::new(RwLock::new(HashMap::new())),
        }
    }

    /// Create a new lending pool
    pub async fn create_pool(&self, pool: LendingPool) -> Result<Uuid, &'static str> {
        let pool_id = pool.pool_id;
        self.pools.write().await.insert(pool_id, pool);
        Ok(pool_id)
    }

    /// Update asset price (for oracle integration)
    pub async fn update_price(&self, asset_id: Uuid, price: Decimal) {
        self.asset_prices.write().await.insert(asset_id, price);
    }

    /// Deposit assets into lending pool
    pub async fn deposit(
        &self,
        user_id: Uuid,
        pool_id: Uuid,
        amount: Decimal,
    ) -> Result<(), &'static str> {
        if amount <= Decimal::ZERO {
            return Err("Deposit amount must be positive");
        }

        // Update pool
        let mut pools = self.pools.write().await;
        let pool = pools.get_mut(&pool_id).ok_or("Pool not found")?;
        pool.total_deposits += amount;
        pool.last_update = chrono::Utc::now().timestamp();

        // Update position
        let mut positions = self.positions.write().await;
        let position = positions
            .entry((user_id, pool_id))
            .or_insert_with(|| LendingPosition::new(user_id, pool_id));
        position.deposited += amount;

        Ok(())
    }

    /// Withdraw assets from lending pool
    pub async fn withdraw(
        &self,
        user_id: Uuid,
        pool_id: Uuid,
        amount: Decimal,
    ) -> Result<(), &'static str> {
        if amount <= Decimal::ZERO {
            return Err("Withdraw amount must be positive");
        }

        let mut positions = self.positions.write().await;
        let position = positions
            .get_mut(&(user_id, pool_id))
            .ok_or("Position not found")?;

        if position.deposited < amount {
            return Err("Insufficient deposits");
        }

        // Check if withdrawal would affect health factor
        drop(positions);
        let health = self.calculate_health_factor(user_id).await?;
        let collateral_value_after = health.total_collateral_value - amount;
        let new_health = HealthFactor::calculate(
            collateral_value_after,
            health.total_debt_value,
            health.liquidation_threshold,
        );

        if new_health < dec!(1.2) {
            // Require 20% buffer above liquidation
            return Err("Withdrawal would put position at risk of liquidation");
        }

        // Update pool
        let mut pools = self.pools.write().await;
        let pool = pools.get_mut(&pool_id).ok_or("Pool not found")?;

        if pool.available_liquidity() < amount {
            return Err("Insufficient pool liquidity");
        }

        pool.total_deposits -= amount;
        pool.last_update = chrono::Utc::now().timestamp();

        // Update position
        let mut positions = self.positions.write().await;
        let position = positions.get_mut(&(user_id, pool_id)).unwrap();
        position.deposited -= amount;

        Ok(())
    }

    /// Borrow assets from lending pool
    pub async fn borrow(
        &self,
        user_id: Uuid,
        pool_id: Uuid,
        amount: Decimal,
    ) -> Result<(), &'static str> {
        if amount <= Decimal::ZERO {
            return Err("Borrow amount must be positive");
        }

        let pools = self.pools.read().await;
        let pool = pools.get(&pool_id).ok_or("Pool not found")?;

        if pool.available_liquidity() < amount {
            return Err("Insufficient pool liquidity");
        }

        // Check if borrow would exceed collateral
        drop(pools);
        let health = self.calculate_health_factor(user_id).await?;
        let new_debt = health.total_debt_value + amount;
        let new_health = HealthFactor::calculate(
            health.total_collateral_value,
            new_debt,
            health.liquidation_threshold,
        );

        if new_health < dec!(1.5) {
            // Require 50% buffer
            return Err("Insufficient collateral");
        }

        // Update pool
        let mut pools = self.pools.write().await;
        let pool = pools.get_mut(&pool_id).unwrap();
        pool.total_borrows += amount;
        pool.last_update = chrono::Utc::now().timestamp();

        // Accrue interest and update position
        let borrow_apr = pool.borrow_apr();
        drop(pools);

        let mut positions = self.positions.write().await;
        let position = positions
            .entry((user_id, pool_id))
            .or_insert_with(|| LendingPosition::new(user_id, pool_id));

        let current_time = chrono::Utc::now().timestamp();
        position.accrue_interest(borrow_apr, current_time);
        position.borrowed += amount;

        Ok(())
    }

    /// Repay borrowed assets
    pub async fn repay(
        &self,
        user_id: Uuid,
        pool_id: Uuid,
        amount: Decimal,
    ) -> Result<Decimal, &'static str> {
        if amount <= Decimal::ZERO {
            return Err("Repay amount must be positive");
        }

        let pools = self.pools.read().await;
        let pool = pools.get(&pool_id).ok_or("Pool not found")?;
        let borrow_apr = pool.borrow_apr();
        drop(pools);

        let mut positions = self.positions.write().await;
        let position = positions
            .get_mut(&(user_id, pool_id))
            .ok_or("Position not found")?;

        // Accrue interest first
        let current_time = chrono::Utc::now().timestamp();
        position.accrue_interest(borrow_apr, current_time);

        let total_debt = position.total_debt();
        if total_debt == Decimal::ZERO {
            return Err("No debt to repay");
        }

        let actual_repay = amount.min(total_debt);

        // Repay accrued interest first, then principal
        if actual_repay >= position.accrued_interest {
            let remaining = actual_repay - position.accrued_interest;
            position.accrued_interest = Decimal::ZERO;
            position.borrowed -= remaining;
        } else {
            position.accrued_interest -= actual_repay;
        }

        // Update pool
        let mut pools = self.pools.write().await;
        let pool = pools.get_mut(&pool_id).unwrap();
        pool.total_borrows -= actual_repay.min(position.borrowed);
        pool.last_update = chrono::Utc::now().timestamp();

        Ok(actual_repay)
    }

    /// Calculate user's health factor across all positions
    pub async fn calculate_health_factor(
        &self,
        user_id: Uuid,
    ) -> Result<HealthFactor, &'static str> {
        let positions = self.positions.read().await;
        let pools = self.pools.read().await;
        let prices = self.asset_prices.read().await;

        let mut total_collateral_value = Decimal::ZERO;
        let mut total_debt_value = Decimal::ZERO;
        let mut weighted_threshold = Decimal::ZERO;

        for ((uid, pool_id), position) in positions.iter() {
            if *uid != user_id {
                continue;
            }

            let pool = pools.get(pool_id).ok_or("Pool not found")?;
            let price = prices.get(&pool.asset_id).copied().unwrap_or(dec!(1));

            // Add collateral value
            let collateral_value = position.deposited * price;
            total_collateral_value += collateral_value;
            weighted_threshold += collateral_value * pool.liquidation_threshold;

            // Add debt value
            total_debt_value += position.total_debt() * price;
        }

        let avg_threshold = if total_collateral_value > Decimal::ZERO {
            weighted_threshold / total_collateral_value
        } else {
            dec!(0.75) // Default threshold
        };

        let health_factor =
            HealthFactor::calculate(total_collateral_value, total_debt_value, avg_threshold);

        Ok(HealthFactor {
            user_id,
            total_collateral_value,
            total_debt_value,
            health_factor,
            liquidation_threshold: avg_threshold,
        })
    }

    /// Liquidate undercollateralized position
    pub async fn liquidate(
        &self,
        liquidator_id: Uuid,
        borrower_id: Uuid,
        pool_id: Uuid,
        debt_to_repay: Decimal,
    ) -> Result<Liquidation, &'static str> {
        // Check if borrower is liquidatable
        let health = self.calculate_health_factor(borrower_id).await?;
        if !health.is_liquidatable() {
            return Err("Position is not liquidatable");
        }

        let max_liquidation = health.liquidation_amount();
        if debt_to_repay > max_liquidation {
            return Err("Exceeds maximum liquidation amount");
        }

        let pools = self.pools.read().await;
        let pool = pools.get(&pool_id).ok_or("Pool not found")?;
        let liquidation_penalty = pool.liquidation_penalty;
        let asset_id = pool.asset_id;
        drop(pools);

        let prices = self.asset_prices.read().await;
        let price = prices.get(&asset_id).copied().unwrap_or(dec!(1));
        drop(prices);

        // Calculate collateral to seize (debt + penalty)
        let collateral_seized = debt_to_repay * (dec!(1) + liquidation_penalty) / price;
        let liquidation_bonus = debt_to_repay * liquidation_penalty;

        // Update borrower position
        let mut positions = self.positions.write().await;
        let borrower_pos = positions
            .get_mut(&(borrower_id, pool_id))
            .ok_or("Borrower position not found")?;

        if borrower_pos.deposited < collateral_seized {
            return Err("Insufficient collateral");
        }

        borrower_pos.deposited -= collateral_seized;
        borrower_pos.borrowed -= debt_to_repay;

        // Transfer collateral to liquidator
        let liquidator_pos = positions
            .entry((liquidator_id, pool_id))
            .or_insert_with(|| LendingPosition::new(liquidator_id, pool_id));
        liquidator_pos.deposited += collateral_seized;

        drop(positions);

        // Update pool
        let mut pools = self.pools.write().await;
        let pool = pools.get_mut(&pool_id).unwrap();
        pool.total_borrows -= debt_to_repay;
        pool.last_update = chrono::Utc::now().timestamp();

        Ok(Liquidation {
            liquidation_id: Uuid::new_v4(),
            liquidator_id,
            borrower_id,
            pool_id,
            debt_repaid: debt_to_repay,
            collateral_seized,
            liquidation_bonus,
            timestamp: chrono::Utc::now().timestamp(),
        })
    }

    /// Get pool information
    pub async fn get_pool(&self, pool_id: Uuid) -> Option<LendingPool> {
        self.pools.read().await.get(&pool_id).cloned()
    }

    /// Get user position
    pub async fn get_position(&self, user_id: Uuid, pool_id: Uuid) -> Option<LendingPosition> {
        self.positions
            .read()
            .await
            .get(&(user_id, pool_id))
            .cloned()
    }

    /// Get all positions for a user
    pub async fn get_user_positions(&self, user_id: Uuid) -> Vec<LendingPosition> {
        self.positions
            .read()
            .await
            .iter()
            .filter(|((uid, _), _)| *uid == user_id)
            .map(|(_, pos)| pos.clone())
            .collect()
    }
}

impl Default for LendingManager {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_linear_interest_rate() {
        let model = InterestRateModel::Linear {
            base_rate: dec!(0.02),
            slope: dec!(0.10),
        };

        // 50% utilization
        let rate = model.calculate_rate(dec!(0.5));
        assert_eq!(rate, dec!(0.07)); // 2% + 50% * 10% = 7%
    }

    #[test]
    fn test_kinked_interest_rate() {
        let model = InterestRateModel::Kinked {
            base_rate: dec!(0.02),
            slope1: dec!(0.05),
            slope2: dec!(0.50),
            optimal_utilization: dec!(0.8),
        };

        // Below kink
        let rate1 = model.calculate_rate(dec!(0.5));
        assert_eq!(rate1, dec!(0.045)); // 2% + 50% * 5% = 4.5%

        // Above kink
        let rate2 = model.calculate_rate(dec!(0.9));
        // 2% + 80% * 5% + (90% - 80%) * 50% = 2% + 4% + 5% = 11%
        assert_eq!(rate2, dec!(0.11));
    }

    #[test]
    fn test_lending_pool_creation() {
        let pool = LendingPool::new(
            Uuid::new_v4(),
            InterestRateModel::Linear {
                base_rate: dec!(0.02),
                slope: dec!(0.10),
            },
            dec!(0.75),
            dec!(0.80),
            dec!(0.05),
        );
        assert!(pool.is_ok());
    }

    #[test]
    fn test_pool_utilization() {
        let mut pool = LendingPool::new(
            Uuid::new_v4(),
            InterestRateModel::Linear {
                base_rate: dec!(0.02),
                slope: dec!(0.10),
            },
            dec!(0.75),
            dec!(0.80),
            dec!(0.05),
        )
        .unwrap();

        pool.total_deposits = dec!(1000);
        pool.total_borrows = dec!(750);

        assert_eq!(pool.utilization_rate(), dec!(0.75));
        assert_eq!(pool.borrow_apr(), dec!(0.095)); // 2% + 75% * 10%
    }

    #[test]
    fn test_health_factor_calculation() {
        let health = HealthFactor::calculate(dec!(1000), dec!(600), dec!(0.80));
        // (1000 * 0.80) / 600 = 800 / 600 = 1.333...
        assert!(health > dec!(1.33) && health < dec!(1.34));
        assert!(
            !HealthFactor {
                user_id: Uuid::new_v4(),
                total_collateral_value: dec!(1000),
                total_debt_value: dec!(600),
                health_factor: health,
                liquidation_threshold: dec!(0.80),
            }
            .is_liquidatable()
        );
    }

    #[test]
    fn test_health_factor_liquidatable() {
        let health = HealthFactor::calculate(dec!(1000), dec!(900), dec!(0.80));
        // (1000 * 0.80) / 900 = 800 / 900 = 0.888...
        assert!(health < dec!(1));
        assert!(
            HealthFactor {
                user_id: Uuid::new_v4(),
                total_collateral_value: dec!(1000),
                total_debt_value: dec!(900),
                health_factor: health,
                liquidation_threshold: dec!(0.80),
            }
            .is_liquidatable()
        );
    }

    #[tokio::test]
    async fn test_lending_deposit_withdraw() {
        let manager = LendingManager::new();
        let asset_id = Uuid::new_v4();
        let user_id = Uuid::new_v4();

        // Create pool
        let pool = LendingPool::new(
            asset_id,
            InterestRateModel::Linear {
                base_rate: dec!(0.02),
                slope: dec!(0.10),
            },
            dec!(0.75),
            dec!(0.80),
            dec!(0.05),
        )
        .unwrap();
        let pool_id = pool.pool_id;
        manager.create_pool(pool).await.unwrap();
        manager.update_price(asset_id, dec!(1)).await;

        // Deposit
        manager.deposit(user_id, pool_id, dec!(1000)).await.unwrap();

        let position = manager.get_position(user_id, pool_id).await.unwrap();
        assert_eq!(position.deposited, dec!(1000));

        // Withdraw
        manager.withdraw(user_id, pool_id, dec!(300)).await.unwrap();

        let position = manager.get_position(user_id, pool_id).await.unwrap();
        assert_eq!(position.deposited, dec!(700));
    }

    #[tokio::test]
    async fn test_borrow_repay() {
        let manager = LendingManager::new();
        let asset_id = Uuid::new_v4();
        let user_id = Uuid::new_v4();

        // Create pool
        let pool = LendingPool::new(
            asset_id,
            InterestRateModel::Linear {
                base_rate: dec!(0.02),
                slope: dec!(0.10),
            },
            dec!(0.75),
            dec!(0.80),
            dec!(0.05),
        )
        .unwrap();
        let pool_id = pool.pool_id;
        manager.create_pool(pool).await.unwrap();
        manager.update_price(asset_id, dec!(1)).await;

        // Deposit collateral
        manager.deposit(user_id, pool_id, dec!(1000)).await.unwrap();

        // Borrow (can borrow up to 75% * collateral with safety buffer)
        manager.borrow(user_id, pool_id, dec!(400)).await.unwrap();

        let position = manager.get_position(user_id, pool_id).await.unwrap();
        assert_eq!(position.borrowed, dec!(400));

        // Repay
        let repaid = manager.repay(user_id, pool_id, dec!(200)).await.unwrap();
        assert_eq!(repaid, dec!(200));

        let position = manager.get_position(user_id, pool_id).await.unwrap();
        assert_eq!(position.borrowed, dec!(200));
    }
}