Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: interest rate calculations #25

Merged
merged 4 commits into from
Sep 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 144 additions & 38 deletions contracts/loan_manager/src/contract.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
use crate::interest::get_interest;
use crate::oracle::{self, Asset};
use crate::positions;
use crate::storage_types::{LoansDataKey, POSITIONS_BUMP_AMOUNT, POSITIONS_LIFETIME_THRESHOLD};

use soroban_sdk::{
contract, contractimpl, vec, Address, Env, IntoVal, Map, String, Symbol, TryFromVal, Val, Vec,
use crate::storage_types::{
Loan, LoansDataKey, DAY_IN_LEDGERS, POSITIONS_BUMP_AMOUNT, POSITIONS_LIFETIME_THRESHOLD,
};

use soroban_sdk::{contract, contractimpl, vec, Address, Env, String, Symbol, Vec};

mod loan_pool {
soroban_sdk::contractimport!(
file = "../../target/wasm32-unknown-unknown/release/loan_pool.wasm"
Expand Down Expand Up @@ -34,6 +35,7 @@ pub trait LoansTrait {
token_collateral_ticker: Symbol,
token_collateral_amount: i128,
) -> i128;
fn get_loan(e: &Env, addr: Address) -> Loan;
}

#[allow(dead_code)]
Expand Down Expand Up @@ -99,12 +101,17 @@ impl LoansTrait for LoansContract {
if !addresses.contains(&user) {
addresses.push_back(user);
}
e.storage()
.persistent()
.set(&Symbol::new(&e, "Addresses"), &addresses);
let key = Symbol::new(&e, "Addresses");
e.storage().persistent().set(&key, &addresses);
e.storage().persistent().extend_ttl(
&key,
POSITIONS_LIFETIME_THRESHOLD,
POSITIONS_BUMP_AMOUNT,
);
}

fn add_interest(e: Env) {
const DECIMAL: i128 = 1000000;
/*
We calculate interest for ledgers_between from a given APY approximation simply by dividing the rate r with ledgers in a year
and multiplying it with ledgers_between. This would result in slightly different total yearly interest, e.g. 12% -> 12.7% total.
Expand All @@ -115,17 +122,11 @@ impl LoansTrait for LoansContract {
let current_ledger = e.ledger().sequence();

let key: LoansDataKey = LoansDataKey::LastUpdated;
let previous_ledger_val: Val = e
.storage()
.persistent()
.get(&key)
.unwrap_or(current_ledger.into_val(&e)); // If there is no previous ledger, use current.
let previous_ledger: u32 =
u32::try_from_val(&e, &previous_ledger_val).expect("Failed to convert Val to u32");

let _ledgers_since_update: u32 = current_ledger - previous_ledger; // Currently unused but is a placeholder for interest calculations. Now time is handled.
let previous_ledger: u32 = e.storage().persistent().get(&key).unwrap_or(current_ledger); // If there is no previous ledger, use current.

// Update current ledger as the new 'last time'
let ledgers_since_update: u32 = current_ledger - previous_ledger; // Currently unused but is a placeholder for interest calculations. Now time is handled.
let ledger_ratio: i128 =
(i128::from(ledgers_since_update) * DECIMAL) / (i128::from(DAY_IN_LEDGERS * 365));

// Iterate over loans and add interest to capital borrowed.
// In the same iteration add the amount to the liabilities of the lending pool.
Expand All @@ -138,29 +139,29 @@ impl LoansTrait for LoansContract {
for user in addresses.iter() {
let key = (Symbol::new(&e, "Loan"), user.clone());

let loan: Val = e
.storage()
.persistent()
.get::<(Symbol, Address), Val>(&key)
.unwrap();

let mut loan_map: Map<Symbol, Val> =
Map::try_from_val(&e, &loan).expect("Failed to convert Val to Map");
let mut loan: Loan = e.storage().persistent().get(&key).unwrap();

let borrowed: Val = loan_map.get(Symbol::new(&e, "borrowed")).unwrap();
let borrowed_as_int: i128 =
i128::try_from_val(&e, &borrowed).expect("Failed to convert Val to i128");
let borrowed: i128 = loan.borrowed_amount;

// FIXME: the calculation doesn't work, perhaps because of the change in types. OR it could be that the value is not retrieved properly
let interest_rate: i128 = 12000;
let interest_amount: i128 = borrowed_as_int * interest_rate;
let new_borrowed: i128 = borrowed_as_int + interest_amount;
let interest_rate: i128 = get_interest(e.clone(), loan.borrowed_from.clone());
let interest_amount_in_year: i128 = (borrowed * interest_rate) / DECIMAL; // TODO: this is 10x what it should be currently
let interest_since_update: i128 = (interest_amount_in_year * ledger_ratio) / DECIMAL;
let new_borrowed: i128 = borrowed + interest_since_update;
// Insert the new value to the loan_map
loan_map.set(Symbol::new(&e, "borrowed"), new_borrowed.into_val(&e));
// Transform the Map back to Val
let new_loan: Val = loan_map.into_val(&e);
// Set it to storage
e.storage().persistent().set(&key, &new_loan);
loan.borrowed_amount = new_borrowed;
// Get updated health_factor
let token_ticker: Symbol = Symbol::new(&e, "USDC"); // temporary
let collateral_ticker: Symbol = Symbol::new(&e, "XLM"); // temporary
loan.health_factor = Self::calculate_health_factor(
&e,
token_ticker,
new_borrowed,
collateral_ticker,
loan.collateral_amount,
); // It now calls reflector for each address. This is safe but might end up being costly
// Set it to storage
e.storage().persistent().set(&key, &loan);
e.storage().persistent().extend_ttl(
&key,
POSITIONS_LIFETIME_THRESHOLD,
Expand All @@ -170,6 +171,11 @@ impl LoansTrait for LoansContract {
}

e.storage().persistent().set(&key, &current_ledger);
e.storage().persistent().extend_ttl(
&key,
POSITIONS_LIFETIME_THRESHOLD,
POSITIONS_BUMP_AMOUNT,
);
}

fn calculate_health_factor(
Expand All @@ -196,16 +202,24 @@ impl LoansTrait for LoansContract {
const DECIMAL_TO_INT_MULTIPLIER: i128 = 10000000;
collateral_value * DECIMAL_TO_INT_MULTIPLIER / borrowed_value
}

fn get_loan(e: &Env, addr: Address) -> Loan {
if let Some(loan) = positions::read_positions(e, addr) {
loan
} else {
panic!() // TODO: It should be panic_with_error or something and give out detailed error.
}
}
}

#[cfg(test)]
#[allow(dead_code, unused_imports)]
mod tests {
use super::*;
use soroban_sdk::{
testutils::Address as _,
testutils::{Address as _, Ledger},
token::{Client as TokenClient, StellarAssetClient},
Env, TryIntoVal,
Env,
};

#[test]
Expand Down Expand Up @@ -269,4 +283,96 @@ mod tests {
assert_eq!(loan_token.balance(&user), 10);
assert_eq!(collateral_token.balance(&user), 900);
}

#[test]
fn add_interest() {
// ARRANGE
let e = Env::default();
e.mock_all_auths_allowing_non_root_auth();
e.ledger().with_mut(|li| {
li.sequence_number = 100_000;
li.min_persistent_entry_ttl = 1_000_000;
li.min_temp_entry_ttl = 1_000_000;
li.max_entry_ttl = 1_000_001;
});

let admin = Address::generate(&e);
let loan_token_contract_id = e.register_stellar_asset_contract(admin.clone());
let loan_asset = StellarAssetClient::new(&e, &loan_token_contract_id);
loan_asset.mint(&admin, &1_000_000);
let loan_currency = loan_pool::Currency {
token_address: loan_token_contract_id.clone(),
ticker: Symbol::new(&e, "XLM"),
};

let admin2 = Address::generate(&e);
let collateral_token_contract_id = e.register_stellar_asset_contract(admin2.clone());
let collateral_asset = StellarAssetClient::new(&e, &collateral_token_contract_id);
let collateral_token = TokenClient::new(&e, &collateral_token_contract_id);
let collateral_currency = loan_pool::Currency {
token_address: collateral_token_contract_id.clone(),
ticker: Symbol::new(&e, "USDC"),
};

// Register mock Reflector contract.
let reflector_addr = Address::from_string(&String::from_str(&e, REFLECTOR_ADDRESS));
e.register_contract_wasm(&reflector_addr, oracle::WASM);

// Mint the user some coins
let user = Address::generate(&e);
collateral_asset.mint(&user, &1_000_000);

assert_eq!(collateral_token.balance(&user), 1_000_000);

// Set up a loan pool with funds for borrowing.
let loan_pool_id = e.register_contract_wasm(None, loan_pool::WASM);
let loan_pool_client = loan_pool::Client::new(&e, &loan_pool_id);

// Set up a loan_pool for the collaterals.
let collateral_pool_id = e.register_contract_wasm(None, loan_pool::WASM);
let collateral_pool_client = loan_pool::Client::new(&e, &collateral_pool_id);

// Register loan manager contract.
let contract_id = e.register_contract(None, LoansContract);
let contract_client = LoansContractClient::new(&e, &contract_id);

// ACT
// Initialize the loan pool and deposit some of the admin's funds.
loan_pool_client.initialize(&loan_currency, &800_000);
loan_pool_client.deposit(&admin, &1_000_000);

collateral_pool_client.initialize(&collateral_currency, &800_000);

// Create a loan.
contract_client.initialize(&user, &10_000, &loan_pool_id, &100_000, &collateral_pool_id);

let user_loan = contract_client.get_loan(&user);

assert_eq!(user_loan.borrowed_amount, 10_000);
assert_eq!(collateral_token.balance(&user), 900_000);

contract_client.add_interest();

// Here borrowed amount should be the same as time has not moved. add_interest() is only called to store the LastUpdate sequence number.
assert_eq!(user_loan.borrowed_amount, 10_000);
assert_eq!(user_loan.health_factor, 100_000_000);
assert_eq!(collateral_token.balance(&user), 900_000);

// Move time
e.ledger().with_mut(|li| {
li.sequence_number = 100_000 + 10_000;
});

// A new instance of reflector mock needs to be created, they only live for one ledger.
let reflector_addr = Address::from_string(&String::from_str(&e, REFLECTOR_ADDRESS));
e.register_contract_wasm(&reflector_addr, oracle::WASM);

contract_client.add_interest();

let user_loan = contract_client.get_loan(&user);

assert_eq!(user_loan.borrowed_amount, 10_003);
assert_eq!(user_loan.health_factor, 99_970_008);
assert_eq!(user_loan.collateral_amount, 100_000);
}
}
20 changes: 20 additions & 0 deletions contracts/loan_manager/src/interest.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
use soroban_sdk::{Address, Env};

mod loan_pool {
soroban_sdk::contractimport!(
file = "../../target/wasm32-unknown-unknown/release/loan_pool.wasm"
);
}
#[allow(dead_code)]
pub const BASE_INTEREST_RATE: i128 = 200_000; // TODO: This should be based on what pool it is. Current 2 %.

#[allow(dead_code, unused_variables)]

pub fn get_interest(e: Env, pool: Address) -> i128 {
Comment on lines +11 to +13
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Miksi tässä välissä newline? Funktiota ei myöskään käytetä, tehty tulevaisuuden varalle?

let pool_client = loan_pool::Client::new(&e, &pool);
//let available = pool_client.get_available_balance
//let total = pool_client.get_total_balance

//let borrowed_rate = (available - total) * decimal / total
BASE_INTEREST_RATE
}
1 change: 1 addition & 0 deletions contracts/loan_manager/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#![no_std]

mod contract;
mod interest;
mod oracle;
mod positions;
mod storage_types;
8 changes: 8 additions & 0 deletions contracts/loan_manager/src/positions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,11 @@ fn write_positions(
.persistent()
.extend_ttl(&key, POSITIONS_LIFETIME_THRESHOLD, POSITIONS_BUMP_AMOUNT);
}

pub fn read_positions(e: &Env, addr: Address) -> Option<Loan> {
let key = LoansDataKey::Loan(addr);

let value: Option<Loan> = e.storage().persistent().get(&key)?;

value
}
2 changes: 1 addition & 1 deletion contracts/loan_pool/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ contractmeta!(
);

// TODO: get this dynamically when creating the contract.
const LOAN_MANAGER_ADDRESS: &str = "CC62OM25XWZY6QECTFQR4NP7PINU6JN46FSDUOXC7Y5PUFJX3ZPVHZDF";
const LOAN_MANAGER_ADDRESS: &str = "CDIK5MXMF5F3TRW7XHPMUXGHDKBH2SESVDSG2RFGOP2GHXOPCDHTC7VO";

#[allow(dead_code)]
pub trait LoanPoolTrait {
Expand Down
2 changes: 1 addition & 1 deletion src/pages/_borrow/BorrowableAssetCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { Currency } from 'src/currencies';
import { useWallet } from 'src/stellar-wallet';

// Temporary hack to use XLM pool for all loans and collaterals.
const XLM_LOAN_POOL_ID = 'CBGVA2KV423WKDNYILW3BPZSNPDCQI3LXA7XYR7H36XUDOPO3UCNQ3RC';
const XLM_LOAN_POOL_ID = 'CCME5C2PJFCWGW5O5MCO4O6X3AUJLD6XHVH3ZJHC7SD7XK5OTPJRPHF7';

interface BorrowableAssetCardProps {
currency: Currency;
Expand Down
Loading