diff --git a/contracts/loan_manager/src/contract.rs b/contracts/loan_manager/src/contract.rs index aeffdb05..7aee8b02 100644 --- a/contracts/loan_manager/src/contract.rs +++ b/contracts/loan_manager/src/contract.rs @@ -83,19 +83,19 @@ impl LoanManager { // Deposit collateral let collateral_pool_client = loan_pool::Client::new(&e, &collateral_from); - let deposited_collateral = collateral_pool_client.deposit_collateral(&user, &collateral); + collateral_pool_client.deposit_collateral(&user, &collateral); // Borrow the funds let borrow_pool_client = loan_pool::Client::new(&e, &borrowed_from); - let borrowed_funds = borrow_pool_client.borrow(&user, &borrowed); + borrow_pool_client.borrow(&user, &borrowed); // FIXME: Currently one can call initialize multiple times to change same addresses loan positions::init_loan( &e, user.clone(), - borrowed_funds, + borrowed, borrowed_from, - deposited_collateral, + collateral, collateral_from, health_factor, ); diff --git a/contracts/loan_pool/src/contract.rs b/contracts/loan_pool/src/contract.rs index fbeb8225..8fb8f1d6 100644 --- a/contracts/loan_pool/src/contract.rs +++ b/contracts/loan_pool/src/contract.rs @@ -1,12 +1,11 @@ -use crate::pool; -use crate::pool::Currency; -use crate::positions; -use crate::storage_types::extend_instance; - -use soroban_sdk::{ - contract, contractimpl, contractmeta, token, Address, Env, Map, Symbol, TryFromVal, Val, +use crate::{ + loan_pool_env_extensions::LoanPoolEnvExtensions, + positions, + storage_types::{Currency, Positions, PositionsInput}, }; +use soroban_sdk::{contract, contractimpl, contractmeta, token, Address, Env}; + // Metadata that is added on to the WASM custom section contractmeta!( key = "Desc", @@ -26,137 +25,128 @@ impl LoanPoolContract { currency: Currency, liquidation_threshold: i128, ) { - pool::write_loan_manager_addr(&e, loan_manager_addr); - pool::write_currency(&e, currency); - pool::write_liquidation_threshold(&e, liquidation_threshold); - pool::write_total_shares(&e, 0); - pool::write_total_balance(&e, 0); - pool::write_available_balance(&e, 0); + e.set_loan_manager_address(&loan_manager_addr); + e.set_currency(currency); + e.set_liquidation_threshold(liquidation_threshold); + e.set_total_shares(0); + e.set_total_balance(0); + e.set_available_balance(0); } /// Deposits token. Also, mints pool shares for the "user" Identifier. - pub fn deposit(e: Env, user: Address, amount: i128) -> i128 { + pub fn deposit(e: Env, user: Address, amount: i128) -> Positions { user.require_auth(); // Depositor needs to authorize the deposit assert!(amount > 0, "Amount must be positive!"); - // Extend instance storage rent - extend_instance(e.clone()); + e.extend_instance_rent(); - let client = token::Client::new(&e, &pool::read_currency(&e).token_address); + let client = token::Client::new(&e, &e.get_currency().token_address); client.transfer(&user, &e.current_contract_address(), &amount); // TODO: these need to be replaced with increase rather than write so that it wont overwrite the values. - pool::write_available_balance(&e, amount); - pool::write_total_shares(&e, amount); - pool::increase_total_balance(&e, amount); + e.set_total_balance(amount); + e.set_total_shares(amount); + e.increase_total_balance(amount); // Increase users position in pool as they deposit // as this is deposit amount is added to receivables and // liabilities & collateral stays intact - let liabilities: i128 = 0; // temp test param - let collateral: i128 = 0; // temp test param - positions::increase_positions(&e, user.clone(), amount, liabilities, collateral); + let input = PositionsInput { + receivables: Some(amount), + liabilities: None, + collateral: None, + }; - amount + positions::update_positions(&e, &user, input) } /// Transfers share tokens back, burns them and gives corresponding amount of tokens back to user. Returns amount of tokens withdrawn - pub fn withdraw(e: Env, user: Address, amount: i128) -> (i128, i128) { + pub fn withdraw(e: Env, user: Address, amount: i128) -> Positions { user.require_auth(); // Extend instance storage rent - extend_instance(e.clone()); - - // Get users receivables - let receivables_val: Val = positions::read_positions(&e, user.clone()); - let receivables_map: Map = Map::try_from_val(&e, &receivables_val).unwrap(); - let receivables: i128 = receivables_map.get_unchecked(Symbol::new(&e, "receivables")); - - // Check that user is not trying to move more than receivables (TODO: also include collateral?) - assert!( - amount <= receivables, - "Amount can not be greater than receivables!" - ); + e.extend_instance_rent(); // TODO: Decrease AvailableBalance // TODO: Decrease TotalShares // TODO: Decrease TotalBalance // Decrease users position in pool as they withdraw - let liabilities: i128 = 0; - let collateral: i128 = 0; - positions::decrease_positions(&e, user.clone(), amount, liabilities, collateral); + let input = PositionsInput { + receivables: Some(-amount), + liabilities: None, + collateral: None, + }; // Transfer tokens from pool to user - let token_address = &pool::read_currency(&e).token_address; - let client = token::Client::new(&e, token_address); + let client = token::Client::new(&e, &e.get_currency().token_address); client.transfer(&e.current_contract_address(), &user, &amount); - (amount, amount) + positions::update_positions(&e, &user, input) } /// Borrow tokens from the pool - pub fn borrow(e: Env, user: Address, amount: i128) -> i128 { + pub fn borrow(e: Env, user: Address, amount: i128) -> Positions { /* Borrow should only be callable from the loans contract. This is as the loans contract will include the logic and checks that the borrowing can be actually done. Therefore we need to include a check that the caller is the loans contract. */ - let loan_manager_addr = pool::read_loan_manager_addr(&e); + let loan_manager_addr = e.get_loan_manager_address(); loan_manager_addr.require_auth(); user.require_auth(); // Extend instance storage rent - extend_instance(e.clone()); + e.extend_instance_rent(); - let balance = pool::read_available_balance(&e); assert!( - amount < balance, + amount < e.get_available_balance(), "Borrowed amount has to be less than available balance!" - ); // Check that there is enough available balance + ); // Increase users position in pool as they deposit // as this is debt amount is added to liabilities and // collateral & receivables stays intact - let collateral: i128 = 0; // temp test param - let receivables: i128 = 0; // temp test param - positions::increase_positions(&e, user.clone(), receivables, amount, collateral); + let input = PositionsInput { + receivables: None, + liabilities: Some(amount), + collateral: None, + }; - let token_address = &pool::read_currency(&e).token_address; - let client = token::Client::new(&e, token_address); + let client = token::Client::new(&e, &e.get_currency().token_address); client.transfer(&e.current_contract_address(), &user, &amount); - amount + positions::update_positions(&e, &user, input) } /// Deposit tokens to the pool to be used as collateral - pub fn deposit_collateral(e: Env, user: Address, amount: i128) -> i128 { + pub fn deposit_collateral(e: Env, user: Address, amount: i128) -> Positions { user.require_auth(); assert!(amount > 0, "Amount must be positive!"); // Extend instance storage rent - extend_instance(e.clone()); + e.extend_instance_rent(); - let token_address = &pool::read_currency(&e).token_address; - let client = token::Client::new(&e, token_address); + let token_address = e.get_currency().token_address; + let client = token::Client::new(&e, &token_address); client.transfer(&user, &e.current_contract_address(), &amount); // Increase users position in pool as they deposit // as this is collateral amount is added to collateral and // liabilities & receivables stays intact - let liabilities: i128 = 0; // temp test param - let receivables: i128 = 0; // temp test param - positions::increase_positions(&e, user.clone(), receivables, liabilities, amount); - - amount + let input = PositionsInput { + receivables: None, + liabilities: None, + collateral: Some(amount), + }; + positions::update_positions(&e, &user, input) } /// Get contract data entries pub fn get_contract_balance(e: Env) -> i128 { // Extend instance storage rent - extend_instance(e.clone()); - - pool::read_total_balance(&e) + e.extend_instance_rent(); + e.get_total_balance() } } @@ -166,7 +156,7 @@ mod test { use soroban_sdk::{ testutils::Address as _, token::{Client as TokenClient, StellarAssetClient}, - Env, + Env, Symbol, }; const TEST_LIQUIDATION_THRESHOLD: i128 = 800_000; @@ -227,9 +217,9 @@ mod test { &TEST_LIQUIDATION_THRESHOLD, ); - let result: i128 = contract_client.deposit(&user, &amount); + let positions = contract_client.deposit(&user, &amount); - assert_eq!(result, amount); + assert_eq!(positions.receivables, amount); } #[test] @@ -298,13 +288,13 @@ mod test { &TEST_LIQUIDATION_THRESHOLD, ); - let result: i128 = contract_client.deposit(&user, &amount); + let positions = contract_client.deposit(&user, &amount); - assert_eq!(result, amount); + assert_eq!(positions.receivables, amount); - let withdraw_result: (i128, i128) = contract_client.withdraw(&user, &amount); + let withdraw_result = contract_client.withdraw(&user, &amount); - assert_eq!(withdraw_result, (amount, amount)); + assert_eq!(withdraw_result.receivables, 0); } #[test] @@ -368,9 +358,9 @@ mod test { &TEST_LIQUIDATION_THRESHOLD, ); - let result: i128 = contract_client.deposit(&user, &amount); + let positions = contract_client.deposit(&user, &amount); - assert_eq!(result, amount); + assert_eq!(positions.receivables, amount); contract_client.withdraw(&user, &(amount * 2)); } diff --git a/contracts/loan_pool/src/lib.rs b/contracts/loan_pool/src/lib.rs index 0a0d0d7b..5001e416 100644 --- a/contracts/loan_pool/src/lib.rs +++ b/contracts/loan_pool/src/lib.rs @@ -1,6 +1,6 @@ #![no_std] mod contract; -mod pool; +mod loan_pool_env_extensions; mod positions; mod storage_types; diff --git a/contracts/loan_pool/src/loan_pool_env_extensions.rs b/contracts/loan_pool/src/loan_pool_env_extensions.rs new file mode 100644 index 00000000..39520457 --- /dev/null +++ b/contracts/loan_pool/src/loan_pool_env_extensions.rs @@ -0,0 +1,172 @@ +use crate::storage_types::{Currency, Positions}; +use soroban_sdk::{contracttype, Address, Env}; + +/* Ledger Thresholds */ +pub(crate) const DAY_IN_LEDGERS: u32 = 17280; // if ledger takes 5 seconds + +pub(crate) const INSTANCE_BUMP_AMOUNT: u32 = 7 * DAY_IN_LEDGERS; +pub(crate) const INSTANCE_LIFETIME_THRESHOLD: u32 = INSTANCE_BUMP_AMOUNT - DAY_IN_LEDGERS; + +pub(crate) const POSITIONS_BUMP_AMOUNT: u32 = 30 * DAY_IN_LEDGERS; +pub(crate) const POSITIONS_LIFETIME_THRESHOLD: u32 = POSITIONS_BUMP_AMOUNT - DAY_IN_LEDGERS; + +#[contracttype] +pub enum PoolDataKey { + // Address of the loan manager for authorization. + LoanManagerAddress, + // Pool's token's address & ticker + Currency, + // The threshold when a loan should liquidate, unit is one-millionth + LiquidationThreshold, + // Users positions in the pool + Positions(Address), + // Total amount of shares in circulation + TotalShares, + // Total balance of pool + TotalBalance, + // Available balance of pool + AvailableBalance, +} + +pub trait LoanPoolEnvExtensions { + fn extend_instance_rent(&self); + + fn set_loan_manager_address(&self, addr: &Address); + + fn get_loan_manager_address(&self) -> Address; + + fn set_currency(&self, currency: Currency); + + fn get_currency(&self) -> Currency; + + fn set_liquidation_threshold(&self, liquidation_threshold: i128); + + fn set_total_shares(&self, shares: i128); + + fn set_total_balance(&self, shares: i128); + + fn get_total_balance(&self) -> i128; + + fn increase_total_balance(&self, amount: i128); + + fn set_available_balance(&self, shares: i128); + + fn get_available_balance(&self) -> i128; + + fn get_positions(&self, user: &Address) -> Positions; + + fn set_positions(&self, user: &Address, positions: &Positions); + + fn extend_positions_ttl(&self, key: &PoolDataKey); +} + +impl LoanPoolEnvExtensions for Env { + fn extend_instance_rent(&self) { + self.storage() + .instance() + .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT) + } + + fn set_loan_manager_address(&self, addr: &Address) { + self.storage() + .persistent() + .set(&PoolDataKey::LoanManagerAddress, addr) + } + + fn get_loan_manager_address(&self) -> Address { + self.storage() + .persistent() + .get(&PoolDataKey::LoanManagerAddress) + .unwrap() + } + + fn set_currency(&self, currency: Currency) { + self.storage() + .persistent() + .set(&PoolDataKey::Currency, ¤cy) + } + + fn get_currency(&self) -> Currency { + self.storage() + .persistent() + .get(&PoolDataKey::Currency) + .unwrap() + } + + fn set_liquidation_threshold(&self, liquidation_threshold: i128) { + self.storage() + .persistent() + .set(&PoolDataKey::LiquidationThreshold, &liquidation_threshold) + } + + fn set_total_shares(&self, shares: i128) { + self.storage() + .persistent() + .set(&PoolDataKey::TotalShares, &shares) + } + + fn set_total_balance(&self, shares: i128) { + self.storage() + .persistent() + .set(&PoolDataKey::TotalBalance, &shares) + } + + fn get_total_balance(&self) -> i128 { + self.storage() + .persistent() + .get(&PoolDataKey::TotalBalance) + .unwrap() + } + + fn increase_total_balance(&self, amount: i128) { + self.set_total_balance(amount + self.get_total_balance()); + } + + fn set_available_balance(&self, shares: i128) { + self.storage() + .persistent() + .set(&PoolDataKey::AvailableBalance, &shares) + } + + fn get_available_balance(&self) -> i128 { + self.storage() + .persistent() + .get(&PoolDataKey::AvailableBalance) + .unwrap() + } + + fn get_positions(&self, user: &Address) -> Positions { + let key = PoolDataKey::Positions(user.clone()); + let opt_positions = self.storage().persistent().get(&key); + + // Extend the TTL if the position exists and return the positions + if let Some(positions) = opt_positions { + self.extend_positions_ttl(&key); + return positions; + } + + // If the position doesn't exist, return a default empty value + Positions { + receivables: 0, + liabilities: 0, + collateral: 0, + } + } + + fn set_positions(&self, user: &Address, positions: &Positions) { + let key: PoolDataKey = PoolDataKey::Positions(user.clone()); + + self.storage().persistent().set(&key, positions); + + // Extend TTL after setting the positions + self.extend_positions_ttl(&key); + } + + fn extend_positions_ttl(&self, key: &PoolDataKey) { + self.storage().persistent().extend_ttl( + key, + POSITIONS_LIFETIME_THRESHOLD, + POSITIONS_BUMP_AMOUNT, + ); + } +} diff --git a/contracts/loan_pool/src/positions.rs b/contracts/loan_pool/src/positions.rs index 27e85e37..a645249c 100644 --- a/contracts/loan_pool/src/positions.rs +++ b/contracts/loan_pool/src/positions.rs @@ -1,99 +1,38 @@ -use crate::storage_types::{ - PoolDataKey, Positions, POSITIONS_BUMP_AMOUNT, POSITIONS_LIFETIME_THRESHOLD, +use crate::{ + loan_pool_env_extensions::LoanPoolEnvExtensions, + storage_types::{Positions, PositionsInput}, }; -use soroban_sdk::{Address, Env, IntoVal, TryFromVal, Val}; +use soroban_sdk::{Address, Env}; -pub fn read_positions(e: &Env, addr: Address) -> Val { - let key = PoolDataKey::Positions(addr); - // If positions exist, read them and return them as Val - if let Some(positions) = e.storage().persistent().get::(&key) { - e.storage().persistent().extend_ttl( - &key, - POSITIONS_LIFETIME_THRESHOLD, - POSITIONS_BUMP_AMOUNT, - ); - positions - } else { - let positions: Positions = Positions { - receivables: 0, - liabilities: 0, - collateral: 0, - }; - - positions.into_val(e) - } -} - -fn write_positions(e: &Env, addr: Address, receivables: i128, liabilities: i128, collateral: i128) { - let key: PoolDataKey = PoolDataKey::Positions(addr); - - let positions: Positions = Positions { +pub fn update_positions(e: &Env, user: &Address, input: PositionsInput) -> Positions { + let Positions { receivables, liabilities, collateral, - }; - - let val: Val = positions.into_val(e); - - e.storage().persistent().set(&key, &val); - - e.storage() - .persistent() - .extend_ttl(&key, POSITIONS_LIFETIME_THRESHOLD, POSITIONS_BUMP_AMOUNT); -} - -pub fn increase_positions( - e: &Env, - addr: Address, - receivables: i128, - liabilities: i128, - collateral: i128, -) { - let positions_val: Val = read_positions(e, addr.clone()); + } = e.get_positions(user); - let positions: Positions = Positions::try_from_val(e, &positions_val).unwrap(); + // Calculate the new positions. + let receivables = input.receivables.map_or(receivables, |r| receivables + r); + let liabilities = input.liabilities.map_or(liabilities, |l| liabilities + l); + let collateral = input.collateral.map_or(collateral, |c| collateral + c); - let receivables_now: i128 = positions.receivables; - let liabilities_now: i128 = positions.liabilities; - let collateral_now = positions.collateral; - write_positions( - e, - addr, - receivables_now + receivables, - liabilities_now + liabilities, - collateral_now + collateral, - ); -} - -pub fn decrease_positions( - e: &Env, - addr: Address, - receivables: i128, - liabilities: i128, - collateral: i128, -) { - let positions_val = read_positions(e, addr.clone()); - - let positions: Positions = Positions::try_from_val(e, &positions_val).unwrap(); - // TODO: Might need to use get rather than get_unchecked and convert from Option to V - let receivables_now = positions.receivables; - let liabilities_now = positions.liabilities; - let collateral_now = positions.collateral; - - if receivables_now < receivables { - panic!("insufficient receivables"); + if receivables < 0 { + panic!("Insufficient receivables"); } - if liabilities_now < liabilities { + if liabilities < 0 { panic!("insufficient liabilities"); } - if collateral_now < collateral { + if collateral < 0 { panic!("insufficient collateral"); } - write_positions( - e, - addr, - receivables_now - receivables, - liabilities_now - liabilities, - collateral_now - collateral, - ); + + let new_positions = Positions { + receivables, + liabilities, + collateral, + }; + + e.set_positions(user, &new_positions); + + new_positions } diff --git a/contracts/loan_pool/src/storage_types.rs b/contracts/loan_pool/src/storage_types.rs index 22ffeb68..bf0f598e 100644 --- a/contracts/loan_pool/src/storage_types.rs +++ b/contracts/loan_pool/src/storage_types.rs @@ -1,14 +1,4 @@ -use soroban_sdk::{contracttype, Address, Env}; - -/* Ledger Thresholds */ - -pub(crate) const DAY_IN_LEDGERS: u32 = 17280; // if ledger takes 5 seconds - -pub(crate) const INSTANCE_BUMP_AMOUNT: u32 = 7 * DAY_IN_LEDGERS; -pub(crate) const INSTANCE_LIFETIME_THRESHOLD: u32 = INSTANCE_BUMP_AMOUNT - DAY_IN_LEDGERS; - -pub(crate) const POSITIONS_BUMP_AMOUNT: u32 = 30 * DAY_IN_LEDGERS; -pub(crate) const POSITIONS_LIFETIME_THRESHOLD: u32 = POSITIONS_BUMP_AMOUNT - DAY_IN_LEDGERS; +use soroban_sdk::{contracttype, Address, Symbol}; /* Storage Types */ @@ -20,6 +10,12 @@ pub struct PoolConfig { pub status: u32, // Status of the pool } +#[contracttype] +pub struct Currency { + pub token_address: Address, + pub ticker: Symbol, +} + #[derive(Clone)] #[contracttype] pub struct Positions { @@ -29,28 +25,10 @@ pub struct Positions { pub collateral: i128, } -#[derive(Clone)] -#[contracttype] -pub enum PoolDataKey { - // Address of the loan manager for authorization. - LoanManagerAddress, - // Pool's token's address & ticker - Currency, - // The threshold when a loan should liquidate, unit is one-millionth - LiquidationThreshold, - // Users positions in the pool - Positions(Address), - // Total amount of shares in circulation - TotalShares, - // Total balance of pool - TotalBalance, - // Available balance of pool - AvailableBalance, -} +/* Input types */ -/* Instance rent bumper */ -pub fn extend_instance(e: Env) { - e.storage() - .instance() - .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); +pub struct PositionsInput { + pub receivables: Option, + pub liabilities: Option, + pub collateral: Option, }