diff --git a/Cargo.lock b/Cargo.lock index dc6d249a..e3688d83 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -365,9 +365,9 @@ checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" [[package]] name = "ecdsa" -version = "0.16.9" +version = "0.16.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +checksum = "0997c976637b606099b9985693efa3581e84e41f5c11ba5255f88711058ad428" dependencies = [ "der", "digest", @@ -941,9 +941,9 @@ dependencies = [ [[package]] name = "sec1" -version = "0.7.3" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +checksum = "f0aec48e813d6b90b15f0b8948af3c63483992dee44c03e9930b3eebdabe046e" dependencies = [ "base16ct", "der", @@ -1042,9 +1042,9 @@ dependencies = [ [[package]] name = "signature" -version = "2.2.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500" dependencies = [ "digest", "rand_core", @@ -1140,9 +1140,9 @@ dependencies = [ [[package]] name = "soroban-fixed-point-math" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "230e5902daf9de6e7591aa7864dcf763ff96914a4460a0294a5dfd62b3181e0f" +checksum = "7db9baa088cdf971a359b6a392ec3601290d8f3a6d6312e375e0310a4c1a3b7d" dependencies = [ "soroban-sdk", ] diff --git a/Cargo.toml b/Cargo.toml index c5f48bf2..132deb0d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,10 +32,10 @@ inherits = "release" debug-assertions = true [workspace.dependencies.soroban-sdk] -version = "20.4.0" +version = "20.5.0" [workspace.dependencies.soroban-token-sdk] -version = "20.4.0" +version = "20.5.0" [workspace.dependencies.soroban-fixed-point-math] -version = "1.0.0" +version = "1.1.0" diff --git a/contracts/common/src/fixedi128.rs b/contracts/common/src/fixedi128.rs index 89505138..c80aee4e 100644 --- a/contracts/common/src/fixedi128.rs +++ b/contracts/common/src/fixedi128.rs @@ -42,14 +42,6 @@ impl FixedI128 { .map(FixedI128) } - pub fn to_precision(self, precision: u32) -> Option { - let prec_denom = 10i128.checked_pow(precision)?; - - self.0 - .checked_mul(prec_denom)? - .checked_div(Self::DENOMINATOR) - } - /// Multiplication of two fixed values pub fn checked_mul(self, value: FixedI128) -> Option { self.0 @@ -82,6 +74,27 @@ impl FixedI128 { .checked_div(Self::DENOMINATOR) } + pub fn mul_int_ceil>(self, other: T) -> Option { + let other = other.into(); + if other == 0 { + return Some(0); + } + + let mb_res = self.0.checked_mul(other)?.checked_div(Self::DENOMINATOR); + + mb_res.map(|res| { + let res_1 = res.abs(); + + if res_1 == 0 { + 1 + } else if res_1 % Self::DENOMINATOR == 0 { + res + } else { + res + 1 + } + }) + } + /// Calculates division of non fixed int value and fixed value, e.g. other / self. /// Result is int value pub fn recip_mul_int>(self, other: T) -> Option { @@ -99,9 +112,13 @@ impl FixedI128 { } let mb_res = Self::DENOMINATOR.checked_mul(other)?.checked_div(self.0); mb_res.map(|res| { - if res == 0 { + let res_1 = res.abs(); + let other_1 = other.abs(); + let self_1 = self.0.abs(); + + if res_1 == 0 { 1 - } else if other >= self.0 && other % self.0 == 0 { + } else if other_1 % self_1 == 0 { res } else { res + 1 diff --git a/contracts/common/src/lib.rs b/contracts/common/src/lib.rs index b8842907..8982463f 100644 --- a/contracts/common/src/lib.rs +++ b/contracts/common/src/lib.rs @@ -15,3 +15,6 @@ pub const PERCENTAGE_FACTOR: u32 = 10_000; ///Seconds in year. Equal 365.25 * 24 * 60 * 60 pub const ONE_YEAR: u64 = 31_557_600; + +///Seconds in day. Equal 24 * 60 * 60 +pub const ONE_DAY: u64 = 86_400; diff --git a/contracts/deployer/src/lib.rs b/contracts/deployer/src/lib.rs index 88621d5c..b69b484c 100644 --- a/contracts/deployer/src/lib.rs +++ b/contracts/deployer/src/lib.rs @@ -1,7 +1,7 @@ #![deny(warnings)] #![no_std] -use pool_interface::types::ir_params::IRParams; +use pool_interface::types::pool_config::PoolConfig; use soroban_sdk::{ contract, contractimpl, vec, Address, BytesN, Env, IntoVal, String, Symbol, Val, }; @@ -20,21 +20,11 @@ impl Deployer { salt: BytesN<32>, wasm_hash: BytesN<32>, admin: Address, - treasury: Address, - flash_loan_fee: u32, - initial_health: u32, - ir_params: IRParams, + pool_config: PoolConfig, ) -> (Address, Val) { let id = env.deployer().with_current_contract(salt).deploy(wasm_hash); let init_fn = Symbol::new(&env, "initialize"); - let init_args = vec![ - &env, - admin.into_val(&env), - treasury.into_val(&env), - flash_loan_fee.into_val(&env), - initial_health.into_val(&env), - ir_params.into_val(&env), - ]; + let init_args = vec![&env, admin.into_val(&env), pool_config.into_val(&env)]; let res: Val = env.invoke_contract(&id, &init_fn, init_args); (id, res) } diff --git a/contracts/deployer/src/test.rs b/contracts/deployer/src/test.rs index 0f3b06a6..8b4cb2d6 100644 --- a/contracts/deployer/src/test.rs +++ b/contracts/deployer/src/test.rs @@ -2,7 +2,7 @@ extern crate std; use crate::{Deployer, DeployerClient}; -use pool_interface::types::ir_params::IRParams; +use pool_interface::types::pool_config::PoolConfig; use soroban_sdk::{ testutils::Address as _, token::Client as TokenClient, Address, BytesN, Env, String, }; @@ -26,15 +26,28 @@ fn deploy_pool_and_s_token() { let env = Env::default(); let client = DeployerClient::new(&env, &env.register_contract(None, Deployer)); - // Deploy pool - let pool_ir_params = IRParams { - alpha: 143, - initial_rate: 200, - max_rate: 50_000, - scaling_coeff: 9_000, - }; let flash_loan_fee = 5; let initial_health = 2_500; + let grace_period = 60 * 60 * 24; + + // Deploy pool + let pool_config = PoolConfig { + base_asset_address: Address::generate(&env), + base_asset_decimals: 7, + flash_loan_fee: flash_loan_fee, + initial_health: initial_health, + timestamp_window: 20, + grace_period: grace_period, + user_assets_limit: 4, + min_collat_amount: 0, + min_debt_amount: 0, + liquidation_protocol_fee: 0, + ir_alpha: 143, + ir_initial_rate: 200, + ir_max_rate: 50_000, + ir_scaling_coeff: 9_000, + }; + let pool_contract_id = { // Install the WASM code to be deployed from the deployer contract. let pool_wasm_hash = env.deployer().upload_contract_wasm(pool::WASM); @@ -42,17 +55,9 @@ fn deploy_pool_and_s_token() { // Deploy contract using deployer, and include an init function to call. let salt = BytesN::from_array(&env, &[0; 32]); let pool_admin = Address::generate(&env); - let treasury = Address::generate(&env); - - let (contract_id, init_result) = client.deploy_pool( - &salt, - &pool_wasm_hash, - &pool_admin, - &treasury, - &flash_loan_fee, - &initial_health, - &pool_ir_params, - ); + + let (contract_id, init_result) = + client.deploy_pool(&salt, &pool_wasm_hash, &pool_admin, &pool_config); assert!(init_result.is_void()); contract_id @@ -111,10 +116,16 @@ fn deploy_pool_and_s_token() { }; let _debt_token_client = debt_token::Client::new(&env, &debt_token_contract_id); - let ir_params = pool_client.ir_params().unwrap(); + let onchain_pool_config = pool_client.pool_configuration(); - assert_eq!(pool_ir_params.alpha, ir_params.alpha); - assert_eq!(pool_ir_params.initial_rate, ir_params.initial_rate); - assert_eq!(pool_ir_params.max_rate, ir_params.max_rate); - assert_eq!(pool_ir_params.scaling_coeff, ir_params.scaling_coeff); + assert_eq!(pool_config.ir_alpha, onchain_pool_config.ir_alpha); + assert_eq!( + pool_config.ir_initial_rate, + onchain_pool_config.ir_initial_rate + ); + assert_eq!(pool_config.ir_max_rate, onchain_pool_config.ir_max_rate); + assert_eq!( + pool_config.ir_scaling_coeff, + onchain_pool_config.ir_scaling_coeff + ); } diff --git a/contracts/pool/src/event.rs b/contracts/pool/src/event.rs index 1ff86c69..5ea79700 100644 --- a/contracts/pool/src/event.rs +++ b/contracts/pool/src/event.rs @@ -1,16 +1,30 @@ -use pool_interface::types::collateral_params_input::CollateralParamsInput; -use pool_interface::types::ir_params::IRParams; +use pool_interface::types::{ + collateral_params_input::CollateralParamsInput, pool_config::PoolConfig, +}; use soroban_sdk::{symbol_short, Address, Env, Symbol}; -pub(crate) fn initialized(e: &Env, admin: &Address, treasury: &Address, params: &IRParams) { - let topics = (Symbol::new(e, "initialize"), admin, treasury); +pub(crate) fn initialized(e: &Env, admin: &Address, pool_config: &PoolConfig) { + let topics = ( + Symbol::new(e, "initialize"), + admin, + pool_config.base_asset_address.clone(), + ); e.events().publish( topics, ( - params.alpha, - params.initial_rate, - params.max_rate, - params.scaling_coeff, + pool_config.ir_alpha, + pool_config.ir_initial_rate, + pool_config.ir_max_rate, + pool_config.ir_scaling_coeff, + pool_config.base_asset_decimals, + pool_config.initial_health, + pool_config.grace_period, + pool_config.timestamp_window, + pool_config.flash_loan_fee, + pool_config.user_assets_limit, + pool_config.min_collat_amount, + pool_config.min_debt_amount, + pool_config.liquidation_protocol_fee, ), ); } @@ -68,14 +82,9 @@ pub(crate) fn borrowing_disabled(e: &Env, asset: &Address) { e.events().publish(topics, ()); } -pub(crate) fn reserve_activated(e: &Env, asset: &Address) { - let topics = (Symbol::new(e, "reserve_activated"), asset.clone()); - e.events().publish(topics, ()); -} - -pub(crate) fn reserve_deactivated(e: &Env, asset: &Address) { - let topics = (Symbol::new(e, "reserve_deactivated"), asset.clone()); - e.events().publish(topics, ()); +pub(crate) fn reserve_status_changed(e: &Env, asset: &Address, activated: bool) { + let topics = (asset.clone(),); + e.events().publish(topics, activated); } pub(crate) fn liquidation(e: &Env, who: &Address, covered_debt: i128, liquidated_collateral: i128) { @@ -91,7 +100,8 @@ pub(crate) fn flash_loan( asset: &Address, amount: i128, premium: i128, + borrow: bool, ) { let topics = (Symbol::new(e, "flash_loan"), who, receiver, asset); - e.events().publish(topics, (amount, premium)); + e.events().publish(topics, (amount, premium, borrow)); } diff --git a/contracts/pool/src/lib.rs b/contracts/pool/src/lib.rs index ebea85ec..792771f2 100644 --- a/contracts/pool/src/lib.rs +++ b/contracts/pool/src/lib.rs @@ -2,25 +2,21 @@ #![no_std] use methods::{ - account_position::account_position, borrow::borrow, collat_coeff::collat_coeff, - configure_as_collateral::configure_as_collateral, debt_coeff::debt_coeff, deposit::deposit, + account_position::account_position, borrow::borrow, claim_protocol_fee::claim_protocol_fee, + collat_coeff::collat_coeff, configure_as_collateral::configure_as_collateral, + debt_coeff::debt_coeff, deposit::deposit, enable_borrowing_on_reserve::enable_borrowing_on_reserve, finalize_transfer::finalize_transfer, flash_loan::flash_loan, init_reserve::init_reserve, initialize::initialize, - liquidate::liquidate, repay::repay, set_as_collateral::set_as_collateral, - set_base_asset::set_base_asset, set_flash_loan_fee::set_flash_loan_fee, - set_initial_health::set_initial_health, set_ir_params::set_ir_params, set_pause::set_pause, - set_price_feeds::set_price_feeds, set_reserve_status::set_reserve_status, - set_reserve_timestamp_window::set_reserve_timestamp_window, - twap_median_price::twap_median_price, upgrade::upgrade, upgrade_debt_token::upgrade_debt_token, - upgrade_s_token::upgrade_s_token, withdraw::withdraw, + liquidate::liquidate, repay::repay, set_as_collateral::set_as_collateral, set_pause::set_pause, + set_pool_configuration::set_pool_configuration, set_price_feeds::set_price_feeds, + set_reserve_status::set_reserve_status, twap_median_price::twap_median_price, upgrade::upgrade, + upgrade_token::upgrade_token, withdraw::withdraw, }; -use pool_interface::types::reserve_type::ReserveType; use pool_interface::types::{ - account_position::AccountPosition, base_asset_config::BaseAssetConfig, - collateral_params_input::CollateralParamsInput, error::Error, flash_loan_asset::FlashLoanAsset, - ir_params::IRParams, price_feed_config::PriceFeedConfig, - price_feed_config_input::PriceFeedConfigInput, reserve_data::ReserveData, - user_config::UserConfiguration, + account_position::AccountPosition, collateral_params_input::CollateralParamsInput, + error::Error, flash_loan_asset::FlashLoanAsset, pause_info::PauseInfo, pool_config::PoolConfig, + price_feed_config::PriceFeedConfig, price_feed_config_input::PriceFeedConfigInput, + reserve_data::ReserveData, reserve_type::ReserveType, user_config::UserConfiguration, }; use pool_interface::LendingPoolTrait; use soroban_sdk::{contract, contractimpl, Address, Bytes, BytesN, Env, Vec}; @@ -39,38 +35,21 @@ pub struct LendingPool; #[contractimpl] impl LendingPoolTrait for LendingPool { - fn initialize( - env: Env, - admin: Address, - treasury: Address, - flash_loan_fee: u32, - initial_health: u32, - ir_params: IRParams, - ) -> Result<(), Error> { - initialize( - &env, - &admin, - &treasury, - flash_loan_fee, - initial_health, - &ir_params, - ) + fn initialize(env: Env, admin: Address, pool_config: PoolConfig) -> Result<(), Error> { + initialize(&env, &admin, &pool_config) } fn upgrade(env: Env, new_wasm_hash: BytesN<32>) -> Result<(), Error> { upgrade(&env, &new_wasm_hash) } - fn upgrade_s_token(env: Env, asset: Address, new_wasm_hash: BytesN<32>) -> Result<(), Error> { - upgrade_s_token(&env, &asset, &new_wasm_hash) - } - - fn upgrade_debt_token( + fn upgrade_token( env: Env, asset: Address, + s_token: bool, new_wasm_hash: BytesN<32>, ) -> Result<(), Error> { - upgrade_debt_token(&env, &asset, &new_wasm_hash) + upgrade_token(&env, &asset, &new_wasm_hash, s_token) } fn version() -> u32 { @@ -85,22 +64,6 @@ impl LendingPoolTrait for LendingPool { set_reserve_status(&env, &asset, is_active) } - fn set_ir_params(env: Env, input: IRParams) -> Result<(), Error> { - set_ir_params(&env, &input) - } - - fn reserve_timestamp_window(env: Env) -> u64 { - read_reserve_timestamp_window(&env) - } - - fn set_reserve_timestamp_window(env: Env, window: u64) -> Result<(), Error> { - set_reserve_timestamp_window(&env, window) - } - - fn ir_params(env: Env) -> Option { - read_ir_params(&env).ok() - } - fn enable_borrowing_on_reserve(env: Env, asset: Address, enabled: bool) -> Result<(), Error> { enable_borrowing_on_reserve(&env, &asset, enabled) } @@ -125,20 +88,12 @@ impl LendingPoolTrait for LendingPool { debt_coeff(&env, &asset) } - fn base_asset(env: Env) -> Result { - read_base_asset(&env) - } - - fn set_base_asset(env: Env, asset: Address, decimals: u32) -> Result<(), Error> { - set_base_asset(&env, &asset, decimals) + fn set_pool_configuration(env: Env, config: PoolConfig) -> Result<(), Error> { + set_pool_configuration(&env, &config, true) } - fn initial_health(env: Env) -> Result { - read_initial_health(&env) - } - - fn set_initial_health(env: Env, value: u32) -> Result<(), Error> { - set_initial_health(&env, value) + fn pool_configuration(env: Env) -> Result { + read_pool_config(&env) } fn set_price_feeds(env: Env, inputs: Vec) -> Result<(), Error> { @@ -198,25 +153,16 @@ impl LendingPoolTrait for LendingPool { set_pause(&env, value) } - fn paused(env: Env) -> bool { - paused(&env) - } - - fn treasury(e: Env) -> Address { - read_treasury(&e) + fn pause_info(env: Env) -> PauseInfo { + read_pause_info(&env) } fn account_position(env: Env, who: Address) -> Result { - account_position(&env, &who) + account_position(&env, &who, &read_pool_config(&env)?) } - fn liquidate( - env: Env, - liquidator: Address, - who: Address, - receive_stoken: bool, - ) -> Result<(), Error> { - liquidate(&env, &liquidator, &who, receive_stoken) + fn liquidate(env: Env, liquidator: Address, who: Address) -> Result<(), Error> { + liquidate(&env, &liquidator, &who) } fn set_as_collateral( @@ -232,10 +178,6 @@ impl LendingPoolTrait for LendingPool { read_user_config(&env, &who) } - fn stoken_underlying_balance(env: Env, stoken_address: Address) -> i128 { - read_stoken_underlying_balance(&env, &stoken_address) - } - fn token_balance(env: Env, token: Address, account: Address) -> i128 { read_token_balance(&env, &token, &account) } @@ -244,14 +186,6 @@ impl LendingPoolTrait for LendingPool { read_token_total_supply(&env, &token) } - fn set_flash_loan_fee(env: Env, fee: u32) -> Result<(), Error> { - set_flash_loan_fee(&env, fee) - } - - fn flash_loan_fee(env: Env) -> u32 { - read_flash_loan_fee(&env) - } - fn flash_loan( env: Env, who: Address, @@ -266,7 +200,11 @@ impl LendingPoolTrait for LendingPool { twap_median_price(env, asset, amount) } - fn balance(env: Env, id: Address, asset: Address) -> i128 { - read_token_balance(&env, &asset, &id) + fn protocol_fee(env: Env, asset: Address) -> i128 { + read_protocol_fee_vault(&env, &asset) + } + + fn claim_protocol_fee(env: Env, asset: Address, recipient: Address) -> Result<(), Error> { + claim_protocol_fee(&env, &asset, &recipient) } } diff --git a/contracts/pool/src/methods/account_position.rs b/contracts/pool/src/methods/account_position.rs index 342966cd..02cc1577 100644 --- a/contracts/pool/src/methods/account_position.rs +++ b/contracts/pool/src/methods/account_position.rs @@ -1,32 +1,36 @@ use common::FixedI128; use pool_interface::types::account_position::AccountPosition; -use pool_interface::types::asset_balance::AssetBalance; use pool_interface::types::error::Error; +use pool_interface::types::pool_config::PoolConfig; use pool_interface::types::reserve_data::ReserveData; use pool_interface::types::reserve_type::ReserveType; use pool_interface::types::user_config::UserConfiguration; use soroban_sdk::{assert_with_error, Address, Env, Map, Vec}; use crate::storage::{ - read_reserve, read_reserves, read_stoken_underlying_balance, read_token_balance, - read_token_total_supply, read_user_config, + read_reserve, read_reserves, read_token_balance, read_token_total_supply, read_user_config, }; use crate::types::account_data::AccountData; use crate::types::calc_account_data_cache::CalcAccountDataCache; use crate::types::liquidation_asset::LiquidationAsset; use crate::types::price_provider::PriceProvider; -use super::utils::get_collat_coeff::get_collat_coeff; +use super::utils::get_collat_coeff::get_compounded_amount; use super::utils::rate::get_actual_borrower_accrued_rate; -pub fn account_position(env: &Env, who: &Address) -> Result { +pub fn account_position( + env: &Env, + who: &Address, + pool_config: &PoolConfig, +) -> Result { let user_config = read_user_config(env, who)?; let account_data = calc_account_data( env, who, &CalcAccountDataCache::none(), + pool_config, &user_config, - &mut PriceProvider::new(env)?, + &mut PriceProvider::new(env, pool_config)?, false, )?; @@ -37,6 +41,7 @@ pub fn calc_account_data( env: &Env, who: &Address, cache: &CalcAccountDataCache, + pool_config: &PoolConfig, user_config: &UserConfiguration, price_provider: &mut PriceProvider, liquidation: bool, @@ -46,6 +51,7 @@ pub fn calc_account_data( } let mut total_discounted_collat_in_base: i128 = 0; + let mut total_collat_in_base: i128 = 0; let mut total_debt_in_base: i128 = 0; let mut sorted_collat_to_receive = Map::new(env); let mut sorted_debt_to_cover = Map::new(env); @@ -53,7 +59,6 @@ pub fn calc_account_data( let reserves_len = u8::try_from(reserves.len()).map_err(|_| Error::ReservesMaxCapacityExceeded)?; - // calc collateral and debt expressed in base token for i in 0..reserves_len { if !user_config.is_using_as_collateral_or_borrowing(env, i) { continue; @@ -68,39 +73,22 @@ pub fn calc_account_data( Error::NoActiveReserve ); - if let ReserveType::Fungible(s_token_address, debt_token_address) = - reserve.clone().reserve_type - { - calculate_fungible( - env, - who, - user_config, - cache, - reserve, - s_token_address, - debt_token_address, - asset, - liquidation, - price_provider, - &mut sorted_collat_to_receive, - &mut total_discounted_collat_in_base, - &mut total_debt_in_base, - &mut sorted_debt_to_cover, - )?; - } else { - calculate_rwa( - env, - who, - user_config, - cache.mb_rwa_balance, - reserve, - asset, - liquidation, - price_provider, - &mut sorted_collat_to_receive, - &mut total_discounted_collat_in_base, - )?; - } + calculate( + env, + who, + user_config, + cache, + reserve, + pool_config, + asset, + liquidation, + price_provider, + &mut sorted_collat_to_receive, + &mut total_collat_in_base, + &mut total_discounted_collat_in_base, + &mut total_debt_in_base, + &mut sorted_debt_to_cover, + )?; } let npv = total_discounted_collat_in_base @@ -120,6 +108,7 @@ pub fn calc_account_data( Ok(AccountData { discounted_collateral: total_discounted_collat_in_base, debt: total_debt_in_base, + collat: liquidation.then_some(total_collat_in_base), liq_debts: liquidation.then_some(sorted_debt_to_pay()), liq_collats: liquidation.then_some(sorted_collat_to_receive.values()), npv, @@ -127,18 +116,18 @@ pub fn calc_account_data( } #[allow(clippy::too_many_arguments)] -fn calculate_fungible( +fn calculate( env: &Env, who: &Address, user_config: &UserConfiguration, cache: &CalcAccountDataCache, reserve: ReserveData, - s_token_address: Address, - debt_token_address: Address, + pool_config: &PoolConfig, asset: Address, liquidation: bool, price_provider: &mut PriceProvider, sorted_collat_to_receive: &mut Map, + total_collat_in_base: &mut i128, total_discounted_collat_in_base: &mut i128, total_debt_in_base: &mut i128, sorted_debt_to_cover: &mut Map>, @@ -149,51 +138,63 @@ fn calculate_fungible( mb_s_token_supply, mb_debt_token_supply, mb_s_token_underlying_balance, - mb_rwa_balance: _, + mb_rwa_balance, } = cache; let reserve_index = reserve.get_id(); if user_config.is_using_as_collateral(env, reserve_index) { - let s_token_supply = mb_s_token_supply - .filter(|x| x.asset == s_token_address) - .map(|x| x.balance) - .unwrap_or_else(|| read_token_total_supply(env, &s_token_address)); - - let debt_token_supply = mb_debt_token_supply - .filter(|x| x.asset == debt_token_address) - .map(|x| x.balance) - .unwrap_or_else(|| read_token_total_supply(env, &debt_token_address)); - - let s_token_underlying_balance = mb_s_token_underlying_balance - .filter(|x| x.asset == s_token_address) - .map(|x| x.balance) - .unwrap_or_else(|| read_stoken_underlying_balance(env, &s_token_address)); - - let collat_coeff = get_collat_coeff( - env, - &reserve, - s_token_supply, - s_token_underlying_balance, - debt_token_supply, - )?; - - let who_collat = mb_who_collat - .filter(|x| x.asset == s_token_address) - .map(|x| x.balance) - .unwrap_or_else(|| read_token_balance(env, &s_token_address, who)); - let discount = FixedI128::from_percentage(reserve.configuration.discount) .ok_or(Error::CalcAccountDataMathError)?; + let (balance, who_collat) = + if let ReserveType::Fungible(s_token_address, debt_token_address) = + reserve.reserve_type.clone() + { + let s_token_supply = mb_s_token_supply + .filter(|x| x.asset == s_token_address) + .map(|x| x.balance) + .unwrap_or_else(|| read_token_total_supply(env, &s_token_address)); + + let debt_token_supply = mb_debt_token_supply + .filter(|x| x.asset == debt_token_address) + .map(|x| x.balance) + .unwrap_or_else(|| read_token_total_supply(env, &debt_token_address)); + + let s_token_underlying_balance = mb_s_token_underlying_balance + .filter(|x| x.asset == s_token_address) + .map(|x| x.balance) + .unwrap_or_else(|| read_token_balance(env, &asset, &s_token_address)); + + let who_collat = mb_who_collat + .filter(|x| x.asset == s_token_address) + .map(|x| x.balance) + .unwrap_or_else(|| read_token_balance(env, &s_token_address, who)); + + ( + get_compounded_amount( + env, + &reserve, + pool_config, + s_token_supply, + s_token_underlying_balance, + debt_token_supply, + who_collat, + )?, + Some(who_collat), + ) + } else { + ( + mb_rwa_balance + .filter(|x| x.asset == asset) + .map(|x| x.balance) + .unwrap_or_else(|| read_token_balance(env, &asset, who)), + None, + ) + }; - let compounded_balance = collat_coeff - .mul_int(who_collat) - .ok_or(Error::CalcAccountDataMathError)?; - - let compounded_balance_in_base = - price_provider.convert_to_base(&asset, compounded_balance)?; + let balance_in_base = price_provider.convert_to_base(&asset, balance)?; let discounted_balance_in_base = discount - .mul_int(compounded_balance_in_base) + .mul_int(balance_in_base) .ok_or(Error::CalcAccountDataMathError)?; *total_discounted_collat_in_base = total_discounted_collat_in_base @@ -201,113 +202,71 @@ fn calculate_fungible( .ok_or(Error::CalcAccountDataMathError)?; if liquidation { + *total_collat_in_base = total_collat_in_base + .checked_add(balance_in_base) + .ok_or(Error::CalcAccountDataMathError)?; + sorted_collat_to_receive.set( reserve.configuration.pen_order, LiquidationAsset { asset, reserve, - coeff: Some(collat_coeff.into_inner()), - lp_balance: Some(who_collat), - comp_balance: compounded_balance, + coeff: None, + lp_balance: who_collat, + comp_balance: balance, }, ); } } else if user_config.is_borrowing(env, reserve_index) { - let debt_coeff = get_actual_borrower_accrued_rate(env, &reserve)?; - - let who_debt = mb_who_debt - .filter(|x| x.asset == debt_token_address) - .map(|x| x.balance) - .unwrap_or_else(|| read_token_balance(env, &debt_token_address, who)); - - let compounded_balance = debt_coeff - .mul_int(who_debt) - .ok_or(Error::CalcAccountDataMathError)?; - - let debt_balance_in_base = price_provider.convert_to_base(&asset, compounded_balance)?; - - *total_debt_in_base = total_debt_in_base - .checked_add(debt_balance_in_base) - .ok_or(Error::CalcAccountDataMathError)?; - - if liquidation { - let s_token_supply = mb_s_token_supply - .filter(|x| x.asset == s_token_address) - .map(|x| x.balance) - .unwrap_or_else(|| read_token_total_supply(env, &s_token_address)); + if let ReserveType::Fungible(s_token_address, debt_token_address) = + reserve.reserve_type.clone() + { + let debt_coeff = get_actual_borrower_accrued_rate(env, &reserve, pool_config)?; - let debt_token_supply = mb_debt_token_supply + let who_debt = mb_who_debt .filter(|x| x.asset == debt_token_address) .map(|x| x.balance) - .unwrap_or_else(|| read_token_total_supply(env, &debt_token_address)); + .unwrap_or_else(|| read_token_balance(env, &debt_token_address, who)); - let utilization = FixedI128::from_rational(debt_token_supply, s_token_supply) - .ok_or(Error::CalcAccountDataMathError)? - .into_inner(); + let compounded_debt = debt_coeff + .mul_int(who_debt) + .ok_or(Error::CalcAccountDataMathError)?; - let mut debt_to_cover = sorted_debt_to_cover - .get(utilization) - .unwrap_or(Vec::new(env)); + let debt_balance_in_base = price_provider.convert_to_base(&asset, compounded_debt)?; - debt_to_cover.push_back(LiquidationAsset { - asset, - reserve, - coeff: Some(debt_coeff.into_inner()), - lp_balance: Some(who_debt), - comp_balance: compounded_balance, - }); + *total_debt_in_base = total_debt_in_base + .checked_add(debt_balance_in_base) + .ok_or(Error::CalcAccountDataMathError)?; - sorted_debt_to_cover.set(utilization, debt_to_cover); - } - } + if liquidation { + let s_token_supply = mb_s_token_supply + .filter(|x| x.asset == s_token_address) + .map(|x| x.balance) + .unwrap_or_else(|| read_token_total_supply(env, &s_token_address)); - Ok(()) -} + let debt_token_supply = mb_debt_token_supply + .filter(|x| x.asset == debt_token_address) + .map(|x| x.balance) + .unwrap_or_else(|| read_token_total_supply(env, &debt_token_address)); -#[allow(clippy::too_many_arguments)] -fn calculate_rwa( - env: &Env, - who: &Address, - user_config: &UserConfiguration, - mb_rwa_balance: Option<&AssetBalance>, - reserve: ReserveData, - asset: Address, - liquidation: bool, - price_provider: &mut PriceProvider, - sorted_collateral_to_receive: &mut Map, - total_discounted_collat_in_base: &mut i128, -) -> Result<(), Error> { - let reserve_index = reserve.get_id(); - if user_config.is_using_as_collateral(env, reserve_index) { - let discount = FixedI128::from_percentage(reserve.configuration.discount) - .ok_or(Error::CalcAccountDataMathError)?; + let utilization = FixedI128::from_rational(debt_token_supply, s_token_supply) + .ok_or(Error::CalcAccountDataMathError)? + .into_inner(); - let balance = mb_rwa_balance - .filter(|x| x.asset == asset) - .map(|x| x.balance) - .unwrap_or_else(|| read_token_balance(env, &asset, who)); + let mut debt_to_cover = sorted_debt_to_cover + .get(utilization) + .unwrap_or(Vec::new(env)); - let balance_in_base = price_provider.convert_to_base(&asset, balance)?; - - let discounted_balance_in_base = discount - .mul_int(balance_in_base) - .ok_or(Error::CalcAccountDataMathError)?; - - *total_discounted_collat_in_base = total_discounted_collat_in_base - .checked_add(discounted_balance_in_base) - .ok_or(Error::CalcAccountDataMathError)?; - - if liquidation { - sorted_collateral_to_receive.set( - reserve.configuration.pen_order, - LiquidationAsset { - reserve, + debt_to_cover.push_back(LiquidationAsset { asset, - lp_balance: None, - coeff: None, - comp_balance: balance, - }, - ); + reserve, + coeff: Some(debt_coeff.into_inner()), + lp_balance: Some(who_debt), + comp_balance: compounded_debt, + }); + + sorted_debt_to_cover.set(utilization, debt_to_cover); + } } } diff --git a/contracts/pool/src/methods/borrow.rs b/contracts/pool/src/methods/borrow.rs index ab2231ad..4b20c700 100644 --- a/contracts/pool/src/methods/borrow.rs +++ b/contracts/pool/src/methods/borrow.rs @@ -1,23 +1,28 @@ use debt_token_interface::DebtTokenClient; use pool_interface::types::asset_balance::AssetBalance; use pool_interface::types::error::Error; +use pool_interface::types::pool_config::PoolConfig; use pool_interface::types::reserve_data::ReserveData; use s_token_interface::STokenClient; use soroban_sdk::{Address, Env}; +use crate::add_token_balance; use crate::event; +use crate::read_pause_info; +use crate::read_pool_config; use crate::storage::{ - add_stoken_underlying_balance, read_reserve, read_token_balance, read_token_total_supply, - write_token_balance, write_token_total_supply, + read_reserve, read_token_balance, read_token_total_supply, write_token_balance, + write_token_total_supply, }; use crate::types::calc_account_data_cache::CalcAccountDataCache; use crate::types::price_provider::PriceProvider; use crate::types::user_configurator::UserConfigurator; use super::account_position::calc_account_data; -use super::utils::get_fungible_lp_tokens::get_fungible_lp_tokens; use super::utils::rate::get_actual_borrower_accrued_rate; use super::utils::recalculate_reserve_data::recalculate_reserve_data; +use super::utils::validation::require_min_position_amounts; +use super::utils::validation::require_not_in_grace_period; use super::utils::validation::{ require_active_reserve, require_borrowing_enabled, require_gte_initial_health, require_not_in_collateral_asset, require_not_paused, require_positive_amount, @@ -27,22 +32,27 @@ use super::utils::validation::{ pub fn borrow(env: &Env, who: &Address, asset: &Address, amount: i128) -> Result<(), Error> { who.require_auth(); - require_not_paused(env); + let pause_info = read_pause_info(env); + require_not_paused(env, &pause_info); + require_not_in_grace_period(env, &pause_info); + require_positive_amount(env, amount); let reserve = read_reserve(env, asset)?; require_active_reserve(env, &reserve); require_borrowing_enabled(env, &reserve); - let (s_token_address, debt_token_address) = get_fungible_lp_tokens(&reserve)?; + let (s_token_address, debt_token_address) = reserve.get_fungible()?; let s_token_supply = read_token_total_supply(env, s_token_address); + let pool_config = read_pool_config(env)?; let debt_token_supply_after = do_borrow( env, who, asset, &reserve, + &pool_config, read_token_balance(env, s_token_address, who), read_token_balance(env, debt_token_address, who), s_token_supply, @@ -56,6 +66,7 @@ pub fn borrow(env: &Env, who: &Address, asset: &Address, amount: i128) -> Result env, asset, &reserve, + &pool_config, s_token_supply, debt_token_supply_after, )?; @@ -69,6 +80,7 @@ pub fn do_borrow( who: &Address, asset: &Address, reserve: &ReserveData, + pool_config: &PoolConfig, who_collat: i128, who_debt: i128, s_token_supply: i128, @@ -78,33 +90,12 @@ pub fn do_borrow( debt_token_address: &Address, ) -> Result { require_not_in_collateral_asset(env, who_collat); - require_positive_amount(env, amount); - - let mut price_provider = PriceProvider::new(env)?; - let amount_in_base = price_provider.convert_to_base(asset, amount)?; - - let mut user_configurator = UserConfigurator::new(env, who, false); - let user_config = user_configurator.user_config()?; - - let account_data = calc_account_data( - env, - who, - &CalcAccountDataCache { - mb_who_collat: None, - mb_who_debt: Some(&AssetBalance::new(debt_token_address.clone(), who_debt)), - mb_s_token_supply: None, - mb_debt_token_supply: None, - mb_s_token_underlying_balance: None, // used only for withdraw - mb_rwa_balance: None, - }, - user_config, - &mut price_provider, - false, - )?; - require_gte_initial_health(env, &account_data, amount_in_base)?; + let mut user_configurator = + UserConfigurator::new(env, who, false, Some(pool_config.user_assets_limit)); + user_configurator.user_config()?; - let debt_coeff = get_actual_borrower_accrued_rate(env, reserve)?; + let debt_coeff = get_actual_borrower_accrued_rate(env, reserve, pool_config)?; let amount_of_debt_token = debt_coeff .recip_mul_int_ceil(amount) .ok_or(Error::MathOverflowError)?; @@ -124,17 +115,49 @@ pub fn do_borrow( let who_debt_after = who_debt .checked_add(amount_of_debt_token) .ok_or(Error::MathOverflowError)?; + let s_token_underlying_after = read_token_balance(env, asset, s_token_address) + .checked_sub(amount) + .ok_or(Error::MathOverflowError)?; + + user_configurator.borrow(reserve.get_id(), who_debt == 0)?; + + let account_data = calc_account_data( + env, + who, + &CalcAccountDataCache { + mb_who_collat: None, + mb_who_debt: Some(&AssetBalance::new( + debt_token_address.clone(), + who_debt_after, + )), + mb_s_token_supply: Some(&AssetBalance::new(s_token_address.clone(), s_token_supply)), + mb_debt_token_supply: Some(&AssetBalance::new( + debt_token_address.clone(), + debt_token_supply_after, + )), + mb_s_token_underlying_balance: Some(&AssetBalance::new( + s_token_address.clone(), + s_token_underlying_after, + )), + mb_rwa_balance: None, + }, + pool_config, + user_configurator.user_config()?, + &mut PriceProvider::new(env, pool_config)?, + false, + )?; + + require_min_position_amounts(env, &account_data, pool_config)?; + require_gte_initial_health(env, &account_data, pool_config)?; DebtTokenClient::new(env, debt_token_address).mint(who, &amount_of_debt_token); STokenClient::new(env, s_token_address).transfer_underlying_to(who, &amount); - add_stoken_underlying_balance(env, s_token_address, amount_to_sub)?; + add_token_balance(env, asset, s_token_address, amount_to_sub)?; write_token_total_supply(env, debt_token_address, debt_token_supply_after)?; write_token_balance(env, debt_token_address, who, who_debt_after)?; - user_configurator - .borrow(reserve.get_id(), who_debt == 0)? - .write(); + user_configurator.write(); event::borrow(env, who, asset, amount); diff --git a/contracts/pool/src/methods/claim_protocol_fee.rs b/contracts/pool/src/methods/claim_protocol_fee.rs new file mode 100644 index 00000000..5cbd66a3 --- /dev/null +++ b/contracts/pool/src/methods/claim_protocol_fee.rs @@ -0,0 +1,29 @@ +use pool_interface::types::{error::Error, reserve_type::ReserveType}; +use s_token_interface::STokenClient; +use soroban_sdk::{token, Address, Env}; + +use crate::{read_protocol_fee_vault, read_reserve, write_protocol_fee_vault}; + +use super::utils::validation::require_admin; + +pub fn claim_protocol_fee(env: &Env, asset: &Address, recipient: &Address) -> Result<(), Error> { + require_admin(env)?; + + let reserve_data = read_reserve(env, asset)?; + let amount = &read_protocol_fee_vault(env, asset); + + match reserve_data.reserve_type { + ReserveType::Fungible(s_token, _) => { + STokenClient::new(env, &s_token).transfer_underlying_to(recipient, amount); + } + ReserveType::RWA => token::Client::new(env, asset).transfer( + &env.current_contract_address(), + recipient, + amount, + ), + } + + write_protocol_fee_vault(env, asset, 0); + + Ok(()) +} diff --git a/contracts/pool/src/methods/collat_coeff.rs b/contracts/pool/src/methods/collat_coeff.rs index 8b2c36d0..18490efa 100644 --- a/contracts/pool/src/methods/collat_coeff.rs +++ b/contracts/pool/src/methods/collat_coeff.rs @@ -1,22 +1,25 @@ use pool_interface::types::error::Error; use soroban_sdk::{Address, Env}; -use crate::storage::{read_reserve, read_stoken_underlying_balance, read_token_total_supply}; - -use super::utils::{ - get_collat_coeff::get_collat_coeff, get_fungible_lp_tokens::get_fungible_lp_tokens, +use crate::{ + read_pool_config, read_token_balance, + storage::{read_reserve, read_token_total_supply}, }; +use super::utils::get_collat_coeff::get_collat_coeff; + pub fn collat_coeff(env: &Env, asset: &Address) -> Result { let reserve = read_reserve(env, asset)?; - let (s_token_address, debt_token_address) = get_fungible_lp_tokens(&reserve)?; + let (s_token_address, debt_token_address) = reserve.get_fungible()?; + let pool_config = read_pool_config(env)?; get_collat_coeff( env, &reserve, + &pool_config, read_token_total_supply(env, s_token_address), - read_stoken_underlying_balance(env, s_token_address), + read_token_balance(env, asset, s_token_address), read_token_total_supply(env, debt_token_address), ) .map(|fixed| fixed.into_inner()) diff --git a/contracts/pool/src/methods/debt_coeff.rs b/contracts/pool/src/methods/debt_coeff.rs index c704289a..a01ddf37 100644 --- a/contracts/pool/src/methods/debt_coeff.rs +++ b/contracts/pool/src/methods/debt_coeff.rs @@ -1,11 +1,11 @@ use pool_interface::types::error::Error; use soroban_sdk::{Address, Env}; -use crate::storage::read_reserve; +use crate::{read_pool_config, storage::read_reserve}; use super::utils::rate::get_actual_borrower_accrued_rate; pub fn debt_coeff(env: &Env, asset: &Address) -> Result { - let reserve = read_reserve(env, asset)?; - get_actual_borrower_accrued_rate(env, &reserve).map(|fixed| fixed.into_inner()) + get_actual_borrower_accrued_rate(env, &read_reserve(env, asset)?, &read_pool_config(env)?) + .map(|fixed| fixed.into_inner()) } diff --git a/contracts/pool/src/methods/deposit.rs b/contracts/pool/src/methods/deposit.rs index ea286056..3c9127d3 100644 --- a/contracts/pool/src/methods/deposit.rs +++ b/contracts/pool/src/methods/deposit.rs @@ -1,16 +1,21 @@ +use pool_interface::types::error::Error; +use pool_interface::types::pool_config::PoolConfig; +use pool_interface::types::reserve_data::ReserveData; use pool_interface::types::reserve_type::ReserveType; -use pool_interface::types::{error::Error, reserve_data::ReserveData}; use s_token_interface::STokenClient; use soroban_sdk::{token, Address, Env}; +use crate::add_token_balance; use crate::event; +use crate::read_pause_info; +use crate::read_pool_config; use crate::storage::{ - add_stoken_underlying_balance, read_reserve, read_stoken_underlying_balance, - read_token_balance, read_token_total_supply, write_token_balance, write_token_total_supply, + read_reserve, read_token_balance, read_token_total_supply, write_token_balance, + write_token_total_supply, }; use crate::types::user_configurator::UserConfigurator; -use super::utils::get_collat_coeff::get_collat_coeff; +use super::utils::get_collat_coeff::get_lp_amount; use super::utils::recalculate_reserve_data::recalculate_reserve_data; use super::utils::validation::{ require_active_reserve, require_liquidity_cap_not_exceeded, require_not_paused, @@ -20,13 +25,17 @@ use super::utils::validation::{ pub fn deposit(env: &Env, who: &Address, asset: &Address, amount: i128) -> Result<(), Error> { who.require_auth(); - require_not_paused(env); + let pause_info = read_pause_info(env); + require_not_paused(env, &pause_info); + require_positive_amount(env, amount); let reserve = read_reserve(env, asset)?; require_active_reserve(env, &reserve); - let mut user_configurator = UserConfigurator::new(env, who, true); + let pool_config = read_pool_config(env)?; + let mut user_configurator = + UserConfigurator::new(env, who, true, Some(pool_config.user_assets_limit)); let user_config = user_configurator.user_config()?; require_zero_debt(env, user_config, reserve.get_id()); @@ -39,6 +48,7 @@ pub fn deposit(env: &Env, who: &Address, asset: &Address, amount: i128) -> Resul who, asset, &reserve, + &pool_config, read_token_total_supply(env, s_token_address), debt_token_supply, read_token_balance(env, s_token_address, who), @@ -50,6 +60,7 @@ pub fn deposit(env: &Env, who: &Address, asset: &Address, amount: i128) -> Resul env, asset, &reserve, + &pool_config, s_token_supply_after, debt_token_supply, )?; @@ -74,26 +85,33 @@ fn do_deposit_fungible( who: &Address, asset: &Address, reserve: &ReserveData, + pool_config: &PoolConfig, s_token_supply: i128, debt_token_supply: i128, who_collat: i128, amount: i128, s_token_address: &Address, ) -> Result<(bool, i128), Error> { - let balance = read_stoken_underlying_balance(env, s_token_address); - require_liquidity_cap_not_exceeded(env, reserve, debt_token_supply, balance, amount)?; + let s_token_underlying_balance = read_token_balance(env, asset, s_token_address); + require_liquidity_cap_not_exceeded( + env, + reserve, + debt_token_supply, + s_token_underlying_balance, + amount, + )?; - let collat_coeff = get_collat_coeff( + let is_first_deposit = who_collat == 0; + let amount_to_mint = get_lp_amount( env, reserve, + pool_config, s_token_supply, - read_stoken_underlying_balance(env, s_token_address), + s_token_underlying_balance, debt_token_supply, + amount, + false, )?; - let is_first_deposit = who_collat == 0; - let amount_to_mint = collat_coeff - .recip_mul_int(amount) - .ok_or(Error::MathOverflowError)?; let s_token_supply_after = s_token_supply .checked_add(amount_to_mint) .ok_or(Error::MathOverflowError)?; @@ -104,7 +122,7 @@ fn do_deposit_fungible( token::Client::new(env, asset).transfer(who, s_token_address, &amount); STokenClient::new(env, s_token_address).mint(who, &amount_to_mint); - add_stoken_underlying_balance(env, s_token_address, amount)?; + add_token_balance(env, asset, s_token_address, amount)?; write_token_total_supply(env, s_token_address, s_token_supply_after)?; write_token_balance(env, s_token_address, who, who_collat_after)?; diff --git a/contracts/pool/src/methods/finalize_transfer.rs b/contracts/pool/src/methods/finalize_transfer.rs index ffff1792..49ab6290 100644 --- a/contracts/pool/src/methods/finalize_transfer.rs +++ b/contracts/pool/src/methods/finalize_transfer.rs @@ -6,11 +6,12 @@ use crate::storage::{read_reserve, read_token_total_supply, write_token_balance} use crate::types::calc_account_data_cache::CalcAccountDataCache; use crate::types::price_provider::PriceProvider; use crate::types::user_configurator::UserConfigurator; +use crate::{read_pause_info, read_pool_config}; use super::account_position::calc_account_data; -use super::utils::get_fungible_lp_tokens::get_fungible_lp_tokens; use super::utils::validation::{ - require_active_reserve, require_good_position, require_not_paused, require_zero_debt, + require_active_reserve, require_gte_initial_health, require_min_position_amounts, + require_not_in_grace_period, require_not_paused, require_zero_debt, }; #[allow(clippy::too_many_arguments)] @@ -24,14 +25,19 @@ pub fn finalize_transfer( balance_to_before: i128, s_token_supply: i128, ) -> Result<(), Error> { - require_not_paused(env); + let pause_info = read_pause_info(env); + require_not_paused(env, &pause_info); + require_not_in_grace_period(env, &pause_info); - let reserve = read_reserve(env, asset)?; + let reserve: pool_interface::types::reserve_data::ReserveData = read_reserve(env, asset)?; + let reserve_id = reserve.get_id(); require_active_reserve(env, &reserve); - let (s_token_address, debt_token_address) = get_fungible_lp_tokens(&reserve)?; + let (s_token_address, debt_token_address) = reserve.get_fungible()?; s_token_address.require_auth(); - let mut to_configurator = UserConfigurator::new(env, to, true); + let pool_config = read_pool_config(env)?; + let mut to_configurator = + UserConfigurator::new(env, to, true, Some(pool_config.user_assets_limit)); let to_config = to_configurator.user_config()?; require_zero_debt(env, to_config, reserve.get_id()); @@ -40,10 +46,15 @@ pub fn finalize_transfer( .checked_sub(amount) .ok_or(Error::InvalidAmount)?; - let mut from_configurator = UserConfigurator::new(env, from, false); - let from_config = from_configurator.user_config()?; + let mut from_configurator = UserConfigurator::new(env, from, false, None); + let is_using_as_collateral = from_configurator + .user_config()? + .is_using_as_collateral(env, reserve.get_id()); + let is_borrowing_any = from_configurator.user_config()?.is_borrowing_any(); + + if is_borrowing_any && is_using_as_collateral { + from_configurator.withdraw(reserve_id, asset, balance_from_after == 0)?; - if from_config.is_borrowing_any() && from_config.is_using_as_collateral(env, reserve.get_id()) { let from_account_data = calc_account_data( env, from, @@ -64,12 +75,14 @@ pub fn finalize_transfer( mb_s_token_underlying_balance: None, mb_rwa_balance: None, }, - from_config, - &mut PriceProvider::new(env)?, + &pool_config, + from_configurator.user_config()?, + &mut PriceProvider::new(env, &pool_config)?, false, )?; - require_good_position(env, &from_account_data); + require_min_position_amounts(env, &from_account_data, &pool_config)?; + require_gte_initial_health(env, &from_account_data, &pool_config)?; } if from != to { @@ -80,7 +93,6 @@ pub fn finalize_transfer( write_token_balance(env, s_token_address, from, balance_from_after)?; write_token_balance(env, s_token_address, to, balance_to_after)?; - let reserve_id = reserve.get_id(); let is_to_deposit = balance_to_before == 0 && amount != 0; from_configurator diff --git a/contracts/pool/src/methods/flash_loan.rs b/contracts/pool/src/methods/flash_loan.rs index 5ec220bb..9e78bde5 100644 --- a/contracts/pool/src/methods/flash_loan.rs +++ b/contracts/pool/src/methods/flash_loan.rs @@ -5,11 +5,9 @@ use pool_interface::types::flash_loan_asset::FlashLoanAsset; use s_token_interface::STokenClient; use soroban_sdk::{assert_with_error, token, vec, Address, Bytes, Env, Vec}; -use crate::event; -use crate::methods::utils::get_fungible_lp_tokens::get_fungible_lp_tokens; -use crate::storage::{ - read_flash_loan_fee, read_reserve, read_token_balance, read_token_total_supply, read_treasury, -}; +use crate::methods::utils::validation::require_not_in_grace_period; +use crate::storage::{read_reserve, read_token_balance, read_token_total_supply}; +use crate::{add_protocol_fee_vault, event, read_pause_info, read_pool_config}; use super::borrow::do_borrow; use super::utils::recalculate_reserve_data::recalculate_reserve_data; @@ -25,13 +23,15 @@ pub fn flash_loan( params: &Bytes, ) -> Result<(), Error> { who.require_auth(); - require_not_paused(env); + let pause_info = read_pause_info(env); + require_not_paused(env, &pause_info); + let pool_config = read_pool_config(env)?; let fee = - FixedI128::from_percentage(read_flash_loan_fee(env)).ok_or(Error::MathOverflowError)?; + FixedI128::from_percentage(pool_config.flash_loan_fee).ok_or(Error::MathOverflowError)?; let loan_asset_len = loan_assets.len(); - assert_with_error!(env, loan_asset_len > 0, Error::MustBePositive); + assert_with_error!(env, loan_asset_len > 0, Error::BellowMinValue); let mut receiver_assets = vec![env]; let mut reserves = vec![env]; @@ -45,7 +45,11 @@ pub fn flash_loan( require_active_reserve(env, &reserve); require_borrowing_enabled(env, &reserve); - let (s_token_address, _) = get_fungible_lp_tokens(&reserve)?; + if loan_asset.borrow { + require_not_in_grace_period(env, &pause_info); + } + + let (s_token_address, _) = reserve.get_fungible()?; let s_token = STokenClient::new(env, s_token_address); s_token.transfer_underlying_to(receiver, &loan_asset.amount); @@ -65,13 +69,11 @@ pub fn flash_loan( let loan_received = loan_receiver.receive(who, &receiver_assets, params); assert_with_error!(env, loan_received, Error::FlashLoanReceiverError); - let treasury = read_treasury(env); - for i in 0..loan_asset_len { let loan_asset = loan_assets.get_unchecked(i); let received_asset = receiver_assets.get_unchecked(i); let reserve = reserves.get_unchecked(i); - let (s_token_address, debt_token_address) = get_fungible_lp_tokens(&reserve)?; + let (s_token_address, debt_token_address) = reserve.get_fungible()?; if !loan_asset.borrow { let underlying_asset = token::Client::new(env, &received_asset.asset); @@ -81,12 +83,8 @@ pub fn flash_loan( s_token_address, &received_asset.amount, ); - underlying_asset.transfer_from( - &env.current_contract_address(), - receiver, - &treasury, - &received_asset.premium, - ); + + add_protocol_fee_vault(env, &received_asset.asset, received_asset.premium)?; } else { let s_token_supply = read_token_total_supply(env, s_token_address); @@ -95,6 +93,7 @@ pub fn flash_loan( who, &received_asset.asset, &reserve, + &pool_config, read_token_balance(env, s_token_address, who), read_token_balance(env, debt_token_address, who), s_token_supply, @@ -108,18 +107,26 @@ pub fn flash_loan( env, &received_asset.asset, &reserve, + &pool_config, s_token_supply, debt_token_supply_after, )?; } + let premium = if loan_asset.borrow { + 0 + } else { + received_asset.premium + }; + event::flash_loan( env, who, receiver, &received_asset.asset, received_asset.amount, - received_asset.premium, + premium, + loan_asset.borrow, ); } diff --git a/contracts/pool/src/methods/init_reserve.rs b/contracts/pool/src/methods/init_reserve.rs index f7ef529d..623a6cb3 100644 --- a/contracts/pool/src/methods/init_reserve.rs +++ b/contracts/pool/src/methods/init_reserve.rs @@ -2,13 +2,18 @@ use pool_interface::types::reserve_data::ReserveData; use pool_interface::types::{error::Error, reserve_type::ReserveType}; use soroban_sdk::{assert_with_error, Address, BytesN, Env}; +use crate::read_reserve; use crate::storage::{read_reserves, write_reserve, write_reserves}; -use super::utils::validation::{require_admin, require_uninitialized_reserve}; +use super::utils::validation::require_admin; pub fn init_reserve(env: &Env, asset: &Address, reserve_type: ReserveType) -> Result<(), Error> { require_admin(env)?; - require_uninitialized_reserve(env, asset); + assert_with_error!( + env, + read_reserve(env, asset).is_err(), + Error::AlreadyInitialized + ); let mut reserve_data = ReserveData::new(env, reserve_type); let mut reserves = read_reserves(env); diff --git a/contracts/pool/src/methods/initialize.rs b/contracts/pool/src/methods/initialize.rs index a78161e0..66e63478 100644 --- a/contracts/pool/src/methods/initialize.rs +++ b/contracts/pool/src/methods/initialize.rs @@ -1,31 +1,21 @@ -use pool_interface::types::{error::Error, ir_params::IRParams}; +use pool_interface::types::error::Error; +use pool_interface::types::pool_config::PoolConfig; use soroban_sdk::{Address, Env}; use crate::event; -use crate::storage::{ - write_admin, write_flash_loan_fee, write_initial_health, write_ir_params, write_treasury, -}; +use crate::storage::write_admin; -use super::utils::validation::{require_admin_not_exist, require_valid_ir_params}; +use super::set_pool_configuration::set_pool_configuration; +use super::utils::validation::require_admin_not_exist; -pub fn initialize( - env: &Env, - admin: &Address, - treasury: &Address, - flash_loan_fee: u32, - initial_health: u32, - ir_params: &IRParams, -) -> Result<(), Error> { +pub fn initialize(env: &Env, admin: &Address, pool_config: &PoolConfig) -> Result<(), Error> { require_admin_not_exist(env); - require_valid_ir_params(env, ir_params); write_admin(env, admin); - write_treasury(env, treasury); - write_ir_params(env, ir_params); - write_flash_loan_fee(env, flash_loan_fee); - write_initial_health(env, initial_health); - event::initialized(env, admin, treasury, ir_params); + set_pool_configuration(env, pool_config, false)?; + + event::initialized(env, admin, pool_config); Ok(()) } diff --git a/contracts/pool/src/methods/liquidate.rs b/contracts/pool/src/methods/liquidate.rs index 232a535e..87589728 100644 --- a/contracts/pool/src/methods/liquidate.rs +++ b/contracts/pool/src/methods/liquidate.rs @@ -1,41 +1,44 @@ use common::{FixedI128, PERCENTAGE_FACTOR}; use debt_token_interface::DebtTokenClient; -use pool_interface::types::{error::Error, reserve_type::ReserveType}; +use pool_interface::types::error::Error; +use pool_interface::types::pool_config::PoolConfig; +use pool_interface::types::reserve_type::ReserveType; use s_token_interface::STokenClient; use soroban_sdk::{assert_with_error, token, Address, Env}; use crate::methods::utils::recalculate_reserve_data::recalculate_reserve_data; +use crate::methods::utils::validation::require_not_in_grace_period; use crate::types::account_data::AccountData; use crate::types::calc_account_data_cache::CalcAccountDataCache; use crate::types::liquidation_asset::LiquidationAsset; use crate::types::price_provider::PriceProvider; use crate::types::user_configurator::UserConfigurator; use crate::{ - add_stoken_underlying_balance, event, read_initial_health, read_token_balance, - read_token_total_supply, write_token_balance, write_token_total_supply, + add_protocol_fee_vault, add_token_balance, event, read_pause_info, read_pool_config, + read_token_balance, read_token_total_supply, write_token_balance, write_token_total_supply, }; use super::account_position::calc_account_data; +use super::utils::get_collat_coeff::get_lp_amount; use super::utils::validation::require_not_paused; -pub fn liquidate( - env: &Env, - liquidator: &Address, - who: &Address, - receive_stoken: bool, -) -> Result<(), Error> { +pub fn liquidate(env: &Env, liquidator: &Address, who: &Address) -> Result<(), Error> { liquidator.require_auth(); - require_not_paused(env); + let pause_info = read_pause_info(env); + require_not_paused(env, &pause_info); + require_not_in_grace_period(env, &pause_info); - let mut user_configurator = UserConfigurator::new(env, who, false); + let mut user_configurator = UserConfigurator::new(env, who, false, None); let user_config = user_configurator.user_config()?; - let mut price_provider = PriceProvider::new(env)?; + let pool_config = read_pool_config(env)?; + let mut price_provider = PriceProvider::new(env, &pool_config)?; let account_data = calc_account_data( env, who, &CalcAccountDataCache::none(), + &pool_config, user_config, &mut price_provider, true, @@ -48,8 +51,8 @@ pub fn liquidate( liquidator, who, account_data, + &pool_config, &mut user_configurator, - receive_stoken, &mut price_provider, )?; @@ -63,120 +66,138 @@ fn do_liquidate( liquidator: &Address, who: &Address, account_data: AccountData, + pool_config: &PoolConfig, user_configurator: &mut UserConfigurator, - receive_stoken: bool, price_provider: &mut PriceProvider, ) -> Result<(i128, i128), Error> { let mut total_debt_after_in_base = account_data.debt; let mut total_collat_disc_after_in_base = account_data.discounted_collateral; let mut total_debt_to_cover_in_base = 0i128; let mut total_liq_in_base = 0i128; + let mut debt_covered_in_base = 0i128; + let total_collat_in_base = account_data.collat.ok_or(Error::LiquidateMathError)?; - let zero_percent = FixedI128::from_inner(0); - let initial_health_percent = FixedI128::from_percentage(read_initial_health(env)?).unwrap(); + let zero_percent = FixedI128::ZERO; + let initial_health_percent = FixedI128::from_percentage(pool_config.initial_health).unwrap(); let hundred_percent = FixedI128::from_percentage(PERCENTAGE_FACTOR).unwrap(); let npv_percent = FixedI128::from_rational(account_data.npv, total_collat_disc_after_in_base) .ok_or(Error::LiquidateMathError)?; let liq_bonus_percent = npv_percent.min(zero_percent).abs().min(hundred_percent); + let max_liq_bonus_percent = FixedI128::from_rational( + total_collat_in_base + .checked_sub(account_data.debt) + .ok_or(Error::LiquidateMathError)?, + total_collat_in_base, + ) + .ok_or(Error::LiquidateMathError)?; + + let full_liquidation = max_liq_bonus_percent <= liq_bonus_percent; + + let (total_debt_liq_bonus_percent, safe_collat_percent) = if !full_liquidation { + let total_debt_liq_bonus_percent = hundred_percent + .checked_sub(liq_bonus_percent) + .ok_or(Error::LiquidateMathError)?; - let total_debt_liq_bonus_percent = hundred_percent - .checked_sub(liq_bonus_percent) - .ok_or(Error::LiquidateMathError)?; + let safe_collat_percent = hundred_percent.checked_sub(initial_health_percent).unwrap(); + + (total_debt_liq_bonus_percent, safe_collat_percent) + } else { + (FixedI128::ZERO, FixedI128::ZERO) + }; - let safe_collat_percent = hundred_percent.checked_sub(initial_health_percent).unwrap(); + let liquidation_protocol_fee = FixedI128::from_percentage(pool_config.liquidation_protocol_fee) + .ok_or(Error::MathOverflowError)?; for collat in account_data.liq_collats.ok_or(Error::LiquidateMathError)? { - let discount_percent = - FixedI128::from_percentage(collat.reserve.configuration.discount).unwrap(); - - // the same for token-based RWA - let liq_comp_amount = calc_liq_amount( - price_provider, - &collat, - hundred_percent, - discount_percent, - liq_bonus_percent, - safe_collat_percent, - initial_health_percent, - total_collat_disc_after_in_base, - total_debt_after_in_base, - )?; + let (liq_comp_amount, debt_in_base) = if !full_liquidation { + let discount_percent = + FixedI128::from_percentage(collat.reserve.configuration.discount).unwrap(); + + let liq_comp_amount = calc_liq_amount( + price_provider, + &collat, + hundred_percent, + discount_percent, + liq_bonus_percent, + safe_collat_percent, + initial_health_percent, + total_collat_disc_after_in_base, + total_debt_after_in_base, + )?; - let total_sub_comp_amount = discount_percent - .mul_int(liq_comp_amount) - .ok_or(Error::LiquidateMathError)?; + let total_sub_comp_amount = discount_percent + .mul_int(liq_comp_amount) + .ok_or(Error::LiquidateMathError)?; - let total_sub_amount_in_base = - price_provider.convert_to_base(&collat.asset, total_sub_comp_amount)?; + let total_sub_amount_in_base = + price_provider.convert_to_base(&collat.asset, total_sub_comp_amount)?; - let debt_comp_amount = total_debt_liq_bonus_percent - .mul_int(liq_comp_amount) - .ok_or(Error::LiquidateMathError)?; + let debt_comp_amount = total_debt_liq_bonus_percent + .mul_int_ceil(liq_comp_amount) + .ok_or(Error::LiquidateMathError)?; - let debt_in_base = price_provider.convert_to_base(&collat.asset, debt_comp_amount)?; + let debt_in_base = price_provider.convert_to_base(&collat.asset, debt_comp_amount)?; - total_debt_after_in_base = total_debt_after_in_base - .checked_sub(debt_in_base) - .ok_or(Error::LiquidateMathError)?; + total_debt_after_in_base = total_debt_after_in_base + .checked_sub(debt_in_base) + .ok_or(Error::LiquidateMathError)?; - total_collat_disc_after_in_base = total_collat_disc_after_in_base - .checked_sub(total_sub_amount_in_base) - .ok_or(Error::LiquidateMathError)?; + total_collat_disc_after_in_base = total_collat_disc_after_in_base + .checked_sub(total_sub_amount_in_base) + .ok_or(Error::LiquidateMathError)?; + + (liq_comp_amount, debt_in_base) + } else { + (collat.comp_balance, 0) + }; total_liq_in_base = total_liq_in_base .checked_add(price_provider.convert_to_base(&collat.asset, liq_comp_amount)?) .ok_or(Error::LiquidateMathError)?; + let protocol_part_underlying = liquidation_protocol_fee + .mul_int(liq_comp_amount) + .ok_or(Error::MathOverflowError)?; + + let liquidator_part_underlying = liq_comp_amount - protocol_part_underlying; + if let ReserveType::Fungible(s_token_address, debt_token_address) = &collat.reserve.reserve_type { let mut s_token_supply = read_token_total_supply(env, s_token_address); let debt_token_supply = read_token_total_supply(env, debt_token_address); - let liq_lp_amount = FixedI128::from_inner(collat.coeff.unwrap()) - .recip_mul_int(liq_comp_amount) - .ok_or(Error::LiquidateMathError)?; - - let s_token = STokenClient::new(env, s_token_address); - - if receive_stoken { - let mut liquidator_configurator = UserConfigurator::new(env, liquidator, true); - let liquidator_config = liquidator_configurator.user_config()?; - - assert_with_error!( + let liq_lp_amount = if !full_liquidation && liq_comp_amount < collat.comp_balance { + get_lp_amount( env, - !liquidator_config.is_borrowing(env, collat.reserve.get_id()), - Error::MustNotHaveDebt - ); - - let liquidator_collat_before = - read_token_balance(env, &s_token.address, liquidator); - - let liquidator_collat_after = liquidator_collat_before - .checked_add(liq_lp_amount) - .ok_or(Error::LiquidateMathError)?; + &collat.reserve, + pool_config, + s_token_supply, + read_token_balance(env, &collat.asset, s_token_address), + debt_token_supply, + liq_comp_amount, + false, + )? + } else { + collat.lp_balance.unwrap() + }; - s_token.transfer_on_liquidation(who, liquidator, &liq_lp_amount); - write_token_balance(env, &s_token.address, liquidator, liquidator_collat_after)?; + let s_token = STokenClient::new(env, s_token_address); - let use_as_collat = liquidator_collat_before == 0; + let amount_to_sub = liq_comp_amount + .checked_neg() + .ok_or(Error::LiquidateMathError)?; + s_token_supply = s_token_supply + .checked_sub(liq_lp_amount) + .ok_or(Error::LiquidateMathError)?; - liquidator_configurator - .deposit(collat.reserve.get_id(), &collat.asset, use_as_collat)? - .write(); - } else { - let amount_to_sub = liq_lp_amount - .checked_neg() - .ok_or(Error::LiquidateMathError)?; - s_token_supply = s_token_supply - .checked_sub(liq_lp_amount) - .ok_or(Error::LiquidateMathError)?; - - s_token.burn(who, &liq_lp_amount, &liq_comp_amount, liquidator); - add_stoken_underlying_balance(env, &s_token.address, amount_to_sub)?; + if liq_lp_amount > 0 && liquidator_part_underlying > 0 { + s_token.burn(who, &liq_lp_amount, &liquidator_part_underlying, liquidator); } + add_token_balance(env, &collat.asset, &s_token.address, amount_to_sub)?; + write_token_total_supply(env, s_token_address, s_token_supply)?; write_token_balance( env, @@ -189,6 +210,7 @@ fn do_liquidate( env, &collat.asset, &collat.reserve, + pool_config, s_token_supply, debt_token_supply, )?; @@ -200,17 +222,26 @@ fn do_liquidate( token::Client::new(env, &collat.asset).transfer( &env.current_contract_address(), liquidator, - &liq_comp_amount, + &liquidator_part_underlying, ); + write_token_balance(env, &collat.asset, who, who_rwa_balance_after)?; } + if protocol_part_underlying > 0 { + add_protocol_fee_vault(env, &collat.asset, protocol_part_underlying)?; + } + user_configurator.withdraw( collat.reserve.get_id(), &collat.asset, collat.comp_balance == liq_comp_amount, )?; + if full_liquidation { + continue; + } + total_debt_to_cover_in_base += debt_in_base; let npv_after = total_collat_disc_after_in_base @@ -222,10 +253,8 @@ fn do_liquidate( } } - let debt_covered_in_base = total_debt_to_cover_in_base; - for debt in account_data.liq_debts.ok_or(Error::LiquidateMathError)? { - if total_debt_to_cover_in_base.eq(&0) { + if !full_liquidation && total_debt_to_cover_in_base.eq(&0) { break; } @@ -236,20 +265,25 @@ fn do_liquidate( price_provider.convert_to_base(&debt.asset, debt.comp_balance)?; let (debt_lp_to_burn, debt_comp_to_transfer) = - if total_debt_to_cover_in_base >= debt_comp_in_base { + if full_liquidation || total_debt_to_cover_in_base >= debt_comp_in_base { total_debt_to_cover_in_base -= debt_comp_in_base; + debt_covered_in_base += debt_comp_in_base; user_configurator.repay(debt.reserve.get_id(), true)?; (debt.lp_balance.unwrap(), debt.comp_balance) } else { - let debt_comp_amount = price_provider - .convert_from_base(&debt.asset, total_debt_to_cover_in_base)?; + let debt_comp_amount = price_provider.convert_from_base( + &debt.asset, + total_debt_to_cover_in_base, + true, + )?; let debt_lp_amount = FixedI128::from_inner(debt.coeff.unwrap()) .recip_mul_int(debt_comp_amount) .ok_or(Error::LiquidateMathError)?; + debt_covered_in_base += total_debt_to_cover_in_base; total_debt_to_cover_in_base = 0; (debt_lp_amount, debt_comp_amount) @@ -258,9 +292,13 @@ fn do_liquidate( let underlying_asset = token::Client::new(env, &debt.asset); let debt_token = DebtTokenClient::new(env, debt_token_address); - underlying_asset.transfer(liquidator, s_token_address, &debt_comp_to_transfer); + if debt_comp_to_transfer > 0 { + underlying_asset.transfer(liquidator, s_token_address, &debt_comp_to_transfer); + } - debt_token.burn(who, &debt_lp_to_burn); + if debt_lp_to_burn > 0 { + debt_token.burn(who, &debt_lp_to_burn); + } let mut debt_token_supply = read_token_total_supply(env, debt_token_address); let s_token_supply = read_token_total_supply(env, s_token_address); @@ -269,7 +307,7 @@ fn do_liquidate( .checked_sub(debt_lp_to_burn) .ok_or(Error::LiquidateMathError)?; - add_stoken_underlying_balance(env, s_token_address, debt_comp_to_transfer)?; + add_token_balance(env, &debt.asset, s_token_address, debt_comp_to_transfer)?; write_token_total_supply(env, debt_token_address, debt_token_supply)?; write_token_balance( env, @@ -282,6 +320,7 @@ fn do_liquidate( env, &debt.asset, &debt.reserve, + pool_config, s_token_supply, debt_token_supply, )?; @@ -323,13 +362,14 @@ fn calc_liq_amount( .checked_sub(safe_discount_percent) .ok_or(Error::LiquidateMathError)?; - let liq_comp_amount = price_provider.convert_from_base(&collat.asset, safe_collat_in_base)?; + let liq_comp_amount = + price_provider.convert_from_base(&collat.asset, safe_collat_in_base, false)?; let liq_comp_amount = safe_discount_percent .recip_mul_int(liq_comp_amount) .ok_or(Error::LiquidateMathError)?; - Ok(if liq_comp_amount.is_negative() { + Ok(if liq_comp_amount.is_negative() || liq_comp_amount == 0 { collat.comp_balance } else { collat.comp_balance.min(liq_comp_amount) diff --git a/contracts/pool/src/methods/mod.rs b/contracts/pool/src/methods/mod.rs index 7fbcf3ee..076a449a 100644 --- a/contracts/pool/src/methods/mod.rs +++ b/contracts/pool/src/methods/mod.rs @@ -1,5 +1,6 @@ pub mod account_position; pub mod borrow; +pub mod claim_protocol_fee; pub mod collat_coeff; pub mod configure_as_collateral; pub mod debt_coeff; @@ -12,17 +13,12 @@ pub mod initialize; pub mod liquidate; pub mod repay; pub mod set_as_collateral; -pub mod set_base_asset; -pub mod set_flash_loan_fee; -pub mod set_initial_health; -pub mod set_ir_params; pub mod set_pause; +pub mod set_pool_configuration; pub mod set_price_feeds; pub mod set_reserve_status; -pub mod set_reserve_timestamp_window; pub mod twap_median_price; pub mod upgrade; -pub mod upgrade_debt_token; -pub mod upgrade_s_token; +pub mod upgrade_token; pub mod utils; pub mod withdraw; diff --git a/contracts/pool/src/methods/repay.rs b/contracts/pool/src/methods/repay.rs index 1e5187aa..0851be6f 100644 --- a/contracts/pool/src/methods/repay.rs +++ b/contracts/pool/src/methods/repay.rs @@ -1,70 +1,62 @@ -use common::FixedI128; use debt_token_interface::DebtTokenClient; +use pool_interface::types::asset_balance::AssetBalance; use pool_interface::types::error::Error; +use pool_interface::types::pool_config::PoolConfig; +use pool_interface::types::reserve_data::ReserveData; use soroban_sdk::{token, Address, Env}; -use crate::event; use crate::storage::{ - add_stoken_underlying_balance, read_reserve, read_stoken_underlying_balance, - read_token_balance, read_token_total_supply, read_treasury, write_token_balance, + read_reserve, read_token_balance, read_token_total_supply, write_token_balance, write_token_total_supply, }; +use crate::types::calc_account_data_cache::CalcAccountDataCache; +use crate::types::price_provider::PriceProvider; use crate::types::user_configurator::UserConfigurator; +use crate::{add_protocol_fee_vault, add_token_balance, event, read_pause_info, read_pool_config}; +use super::account_position::calc_account_data; use super::utils::get_collat_coeff::get_collat_coeff; -use super::utils::get_fungible_lp_tokens::get_fungible_lp_tokens; use super::utils::rate::get_actual_borrower_accrued_rate; use super::utils::recalculate_reserve_data::recalculate_reserve_data; use super::utils::validation::{ - require_active_reserve, require_debt, require_not_paused, require_positive_amount, + require_active_reserve, require_debt, require_min_position_amounts, require_not_paused, + require_positive_amount, }; pub fn repay(env: &Env, who: &Address, asset: &Address, amount: i128) -> Result<(), Error> { who.require_auth(); - require_not_paused(env); + let pause_info = read_pause_info(env); + require_not_paused(env, &pause_info); + require_positive_amount(env, amount); let reserve = read_reserve(env, asset)?; require_active_reserve(env, &reserve); - let (s_token_address, debt_token_address) = get_fungible_lp_tokens(&reserve)?; - let mut user_configurator = UserConfigurator::new(env, who, false); - let user_config = user_configurator.user_config()?; - require_debt(env, user_config, reserve.get_id()); - + let (s_token_address, debt_token_address) = reserve.get_fungible()?; let s_token_supply = read_token_total_supply(env, s_token_address); let debt_token_supply = read_token_total_supply(env, debt_token_address); + let pool_config = read_pool_config(env)?; - let debt_coeff = get_actual_borrower_accrued_rate(env, &reserve)?; - let collat_coeff = get_collat_coeff( + let debt_token_supply_after = do_repay( env, + who, + asset, &reserve, + &pool_config, s_token_supply, - read_stoken_underlying_balance(env, s_token_address), debt_token_supply, - )?; - - let (is_repayed, debt_token_supply_after) = do_repay( - env, - who, - asset, s_token_address, debt_token_address, - collat_coeff, - debt_coeff, - debt_token_supply, amount, )?; - user_configurator - .repay(reserve.get_id(), is_repayed)? - .write(); - recalculate_reserve_data( env, asset, &reserve, + &pool_config, s_token_supply, debt_token_supply_after, )?; @@ -72,21 +64,34 @@ pub fn repay(env: &Env, who: &Address, asset: &Address, amount: i128) -> Result< Ok(()) } -/// Returns -/// bool: the flag indicating the debt is fully repayed -/// i128: total debt after repayment #[allow(clippy::too_many_arguments)] pub fn do_repay( env: &Env, who: &Address, asset: &Address, + reserve: &ReserveData, + pool_config: &PoolConfig, + s_token_supply: i128, + debt_token_supply: i128, s_token_address: &Address, debt_token_address: &Address, - collat_coeff: FixedI128, - debt_coeff: FixedI128, - debt_token_supply: i128, amount: i128, -) -> Result<(bool, i128), Error> { +) -> Result { + let mut user_configurator = UserConfigurator::new(env, who, false, None); + require_debt(env, user_configurator.user_config()?, reserve.get_id()); + + let debt_coeff = get_actual_borrower_accrued_rate(env, reserve, pool_config)?; + let s_token_underlying_balance = read_token_balance(env, asset, s_token_address); + + let collat_coeff = get_collat_coeff( + env, + reserve, + pool_config, + s_token_supply, + s_token_underlying_balance, + debt_token_supply, + )?; + let who_debt = read_token_balance(env, debt_token_address, who); let borrower_actual_debt = debt_coeff .mul_int(who_debt) @@ -118,21 +123,54 @@ pub fn do_repay( let who_debt_after = who_debt .checked_sub(borrower_debt_to_burn) .ok_or(Error::MathOverflowError)?; + let s_token_underlying_after = s_token_underlying_balance + .checked_add(lender_part) + .ok_or(Error::MathOverflowError)?; - let treasury_address = read_treasury(env); + user_configurator.repay(reserve.get_id(), is_repayed)?; + + let account_data = calc_account_data( + env, + who, + &CalcAccountDataCache { + mb_who_collat: None, + mb_who_debt: Some(&AssetBalance::new( + debt_token_address.clone(), + who_debt_after, + )), + mb_s_token_supply: Some(&AssetBalance::new(s_token_address.clone(), s_token_supply)), + mb_debt_token_supply: Some(&AssetBalance::new( + debt_token_address.clone(), + debt_token_supply_after, + )), + mb_s_token_underlying_balance: Some(&AssetBalance::new( + s_token_address.clone(), + s_token_underlying_after, + )), + mb_rwa_balance: None, + }, + pool_config, + user_configurator.user_config()?, + &mut PriceProvider::new(env, pool_config)?, + false, + )?; + + require_min_position_amounts(env, &account_data, pool_config)?; let underlying_asset = token::Client::new(env, asset); let debt_token = DebtTokenClient::new(env, debt_token_address); - underlying_asset.transfer(who, s_token_address, &lender_part); - underlying_asset.transfer(who, &treasury_address, &treasury_part); + underlying_asset.transfer(who, s_token_address, &borrower_payback_amount); + add_protocol_fee_vault(env, asset, treasury_part)?; debt_token.burn(who, &borrower_debt_to_burn); - add_stoken_underlying_balance(env, s_token_address, lender_part)?; + add_token_balance(env, asset, s_token_address, lender_part)?; write_token_total_supply(env, debt_token_address, debt_token_supply_after)?; write_token_balance(env, debt_token_address, who, who_debt_after)?; event::repay(env, who, asset, borrower_payback_amount); - Ok((is_repayed, debt_token_supply_after)) + user_configurator.write(); + + Ok(debt_token_supply_after) } diff --git a/contracts/pool/src/methods/set_as_collateral.rs b/contracts/pool/src/methods/set_as_collateral.rs index 1d4cc14e..4307b2ec 100644 --- a/contracts/pool/src/methods/set_as_collateral.rs +++ b/contracts/pool/src/methods/set_as_collateral.rs @@ -2,13 +2,14 @@ use pool_interface::types::error::Error; use soroban_sdk::{assert_with_error, Address, Env}; use crate::methods::account_position::calc_account_data; +use crate::methods::utils::validation::require_gte_initial_health; +use crate::methods::utils::validation::require_min_position_amounts; +use crate::read_pool_config; use crate::storage::read_reserve; use crate::types::calc_account_data_cache::CalcAccountDataCache; use crate::types::price_provider::PriceProvider; use crate::types::user_configurator::UserConfigurator; -use super::utils::validation::require_good_position; - pub fn set_as_collateral( env: &Env, who: &Address, @@ -17,33 +18,37 @@ pub fn set_as_collateral( ) -> Result<(), Error> { who.require_auth(); - let mut user_configurator = UserConfigurator::new(env, who, false); + let pool_config = read_pool_config(env)?; + let mut user_configurator = + UserConfigurator::new(env, who, false, Some(pool_config.user_assets_limit)); let user_config = user_configurator.user_config()?; let reserve_id = read_reserve(env, asset)?.get_id(); assert_with_error!( env, !user_config.is_borrowing(env, reserve_id), - Error::MustNotHaveDebt + Error::DebtError ); if !use_as_collateral && user_config.is_borrowing_any() && user_config.is_using_as_collateral(env, reserve_id) { + let pool_config = read_pool_config(env)?; user_configurator.withdraw(reserve_id, asset, true)?; - let user_config = user_configurator.user_config()?; let account_data = calc_account_data( env, who, &CalcAccountDataCache::none(), - user_config, - &mut PriceProvider::new(env)?, + &pool_config, + user_configurator.user_config()?, + &mut PriceProvider::new(env, &pool_config)?, false, )?; - require_good_position(env, &account_data); + require_min_position_amounts(env, &account_data, &pool_config)?; + require_gte_initial_health(env, &account_data, &pool_config)?; user_configurator.write(); diff --git a/contracts/pool/src/methods/set_base_asset.rs b/contracts/pool/src/methods/set_base_asset.rs deleted file mode 100644 index 51ded96c..00000000 --- a/contracts/pool/src/methods/set_base_asset.rs +++ /dev/null @@ -1,14 +0,0 @@ -use pool_interface::types::{base_asset_config::BaseAssetConfig, error::Error}; -use soroban_sdk::{Address, Env}; - -use crate::storage::write_base_asset; - -use super::utils::validation::require_admin; - -pub fn set_base_asset(env: &Env, asset: &Address, decimals: u32) -> Result<(), Error> { - require_admin(env)?; - - write_base_asset(env, &BaseAssetConfig::new(asset, decimals)); - - Ok(()) -} diff --git a/contracts/pool/src/methods/set_flash_loan_fee.rs b/contracts/pool/src/methods/set_flash_loan_fee.rs deleted file mode 100644 index e6b400dd..00000000 --- a/contracts/pool/src/methods/set_flash_loan_fee.rs +++ /dev/null @@ -1,12 +0,0 @@ -use pool_interface::types::error::Error; -use soroban_sdk::Env; - -use crate::storage::write_flash_loan_fee; - -use super::utils::validation::require_admin; - -pub fn set_flash_loan_fee(env: &Env, fee: u32) -> Result<(), Error> { - require_admin(env)?; - write_flash_loan_fee(env, fee); - Ok(()) -} diff --git a/contracts/pool/src/methods/set_initial_health.rs b/contracts/pool/src/methods/set_initial_health.rs deleted file mode 100644 index f3b83cae..00000000 --- a/contracts/pool/src/methods/set_initial_health.rs +++ /dev/null @@ -1,14 +0,0 @@ -use pool_interface::types::error::Error; -use soroban_sdk::Env; - -use crate::storage::write_initial_health; - -use super::utils::validation::require_admin; - -pub fn set_initial_health(env: &Env, value: u32) -> Result<(), Error> { - require_admin(env)?; - - write_initial_health(env, value); - - Ok(()) -} diff --git a/contracts/pool/src/methods/set_ir_params.rs b/contracts/pool/src/methods/set_ir_params.rs deleted file mode 100644 index d47242be..00000000 --- a/contracts/pool/src/methods/set_ir_params.rs +++ /dev/null @@ -1,15 +0,0 @@ -use pool_interface::types::{error::Error, ir_params::IRParams}; -use soroban_sdk::Env; - -use crate::storage::write_ir_params; - -use super::utils::validation::{require_admin, require_valid_ir_params}; - -pub fn set_ir_params(env: &Env, input: &IRParams) -> Result<(), Error> { - require_admin(env)?; - require_valid_ir_params(env, input); - - write_ir_params(env, input); - - Ok(()) -} diff --git a/contracts/pool/src/methods/set_pause.rs b/contracts/pool/src/methods/set_pause.rs index c15c6fe6..1aa44fdf 100644 --- a/contracts/pool/src/methods/set_pause.rs +++ b/contracts/pool/src/methods/set_pause.rs @@ -1,12 +1,19 @@ use pool_interface::types::error::Error; use soroban_sdk::Env; -use crate::storage::write_pause; +use crate::{read_pause_info, storage::write_pause_info}; use super::utils::validation::require_admin; pub fn set_pause(env: &Env, value: bool) -> Result<(), Error> { require_admin(env)?; - write_pause(env, value); + let mut pause_info = read_pause_info(env); + + if pause_info.paused && !value { + pause_info.unpaused_at = env.ledger().timestamp(); + } + + pause_info.paused = value; + write_pause_info(env, pause_info); Ok(()) } diff --git a/contracts/pool/src/methods/set_pool_configuration.rs b/contracts/pool/src/methods/set_pool_configuration.rs new file mode 100644 index 00000000..ca456eb5 --- /dev/null +++ b/contracts/pool/src/methods/set_pool_configuration.rs @@ -0,0 +1,31 @@ +use pool_interface::types::error::Error; +use pool_interface::types::pool_config::PoolConfig; +use soroban_sdk::Env; + +use crate::read_pause_info; +use crate::write_pause_info; +use crate::write_pool_config; + +use super::utils::validation::require_admin; +use super::utils::validation::require_valid_pool_config; + +pub fn set_pool_configuration( + env: &Env, + config: &PoolConfig, + check_admin: bool, +) -> Result<(), Error> { + if check_admin { + require_admin(env)?; + } + + require_valid_pool_config(env, config); + + write_pool_config(env, config); + + let mut pause_info = read_pause_info(env); + pause_info.grace_period_secs = config.grace_period; + + write_pause_info(env, pause_info); + + Ok(()) +} diff --git a/contracts/pool/src/methods/set_reserve_status.rs b/contracts/pool/src/methods/set_reserve_status.rs index b63c12d4..14d1065c 100644 --- a/contracts/pool/src/methods/set_reserve_status.rs +++ b/contracts/pool/src/methods/set_reserve_status.rs @@ -14,11 +14,7 @@ pub fn set_reserve_status(env: &Env, asset: &Address, is_active: bool) -> Result reserve.configuration.is_active = is_active; write_reserve(env, asset, &reserve); - if is_active { - event::reserve_activated(env, asset); - } else { - event::reserve_deactivated(env, asset); - } + event::reserve_status_changed(env, asset, is_active); Ok(()) } diff --git a/contracts/pool/src/methods/set_reserve_timestamp_window.rs b/contracts/pool/src/methods/set_reserve_timestamp_window.rs deleted file mode 100644 index 360915a5..00000000 --- a/contracts/pool/src/methods/set_reserve_timestamp_window.rs +++ /dev/null @@ -1,14 +0,0 @@ -use pool_interface::types::error::Error; -use soroban_sdk::Env; - -use crate::storage::write_reserve_timestamp_window; - -use super::utils::validation::require_admin; - -pub fn set_reserve_timestamp_window(env: &Env, window: u64) -> Result<(), Error> { - require_admin(env)?; - - write_reserve_timestamp_window(env, window); - - Ok(()) -} diff --git a/contracts/pool/src/methods/twap_median_price.rs b/contracts/pool/src/methods/twap_median_price.rs index ddc30b35..eb370385 100644 --- a/contracts/pool/src/methods/twap_median_price.rs +++ b/contracts/pool/src/methods/twap_median_price.rs @@ -1,8 +1,8 @@ use pool_interface::types::error::Error; use soroban_sdk::{Address, Env}; -use crate::types::price_provider::PriceProvider; +use crate::{read_pool_config, types::price_provider::PriceProvider}; pub fn twap_median_price(env: Env, asset: Address, amount: i128) -> Result { - PriceProvider::new(&env)?.convert_to_base(&asset, amount) + PriceProvider::new(&env, &read_pool_config(&env)?)?.convert_to_base(&asset, amount) } diff --git a/contracts/pool/src/methods/upgrade_debt_token.rs b/contracts/pool/src/methods/upgrade_debt_token.rs deleted file mode 100644 index 3b24a7f7..00000000 --- a/contracts/pool/src/methods/upgrade_debt_token.rs +++ /dev/null @@ -1,22 +0,0 @@ -use debt_token_interface::DebtTokenClient; -use pool_interface::types::error::Error; -use soroban_sdk::{Address, BytesN, Env}; - -use crate::storage::read_reserve; - -use super::utils::{get_fungible_lp_tokens::get_fungible_lp_tokens, validation::require_admin}; - -pub fn upgrade_debt_token( - env: &Env, - asset: &Address, - new_wasm_hash: &BytesN<32>, -) -> Result<(), Error> { - require_admin(env).unwrap(); - - let reserve = read_reserve(env, asset)?; - let (_, debt_token_address) = get_fungible_lp_tokens(&reserve)?; - let debt_token = DebtTokenClient::new(env, debt_token_address); - debt_token.upgrade(new_wasm_hash); - - Ok(()) -} diff --git a/contracts/pool/src/methods/upgrade_s_token.rs b/contracts/pool/src/methods/upgrade_s_token.rs deleted file mode 100644 index 5a534fc5..00000000 --- a/contracts/pool/src/methods/upgrade_s_token.rs +++ /dev/null @@ -1,22 +0,0 @@ -use pool_interface::types::error::Error; -use s_token_interface::STokenClient; -use soroban_sdk::{Address, BytesN, Env}; - -use crate::storage::read_reserve; - -use super::utils::{get_fungible_lp_tokens::get_fungible_lp_tokens, validation::require_admin}; - -pub fn upgrade_s_token( - env: &Env, - asset: &Address, - new_wasm_hash: &BytesN<32>, -) -> Result<(), Error> { - require_admin(env).unwrap(); - - let reserve = read_reserve(env, asset)?; - let (s_token_address, _) = get_fungible_lp_tokens(&reserve)?; - let s_token = STokenClient::new(env, s_token_address); - s_token.upgrade(new_wasm_hash); - - Ok(()) -} diff --git a/contracts/pool/src/methods/upgrade_token.rs b/contracts/pool/src/methods/upgrade_token.rs new file mode 100644 index 00000000..13f291f2 --- /dev/null +++ b/contracts/pool/src/methods/upgrade_token.rs @@ -0,0 +1,28 @@ +use debt_token_interface::DebtTokenClient; +use pool_interface::types::error::Error; +use s_token_interface::STokenClient; +use soroban_sdk::{Address, BytesN, Env}; + +use crate::storage::read_reserve; + +use super::utils::validation::require_admin; + +pub fn upgrade_token( + env: &Env, + asset: &Address, + new_wasm_hash: &BytesN<32>, + s_token: bool, +) -> Result<(), Error> { + require_admin(env)?; + + let reserve = read_reserve(env, asset)?; + let (s_token_address, debt_token_address) = reserve.get_fungible()?; + + if s_token { + STokenClient::new(env, s_token_address).upgrade(new_wasm_hash) + } else { + DebtTokenClient::new(env, debt_token_address).upgrade(new_wasm_hash); + } + + Ok(()) +} diff --git a/contracts/pool/src/methods/utils/get_collat_coeff.rs b/contracts/pool/src/methods/utils/get_collat_coeff.rs index faceb788..06ba0a9d 100644 --- a/contracts/pool/src/methods/utils/get_collat_coeff.rs +++ b/contracts/pool/src/methods/utils/get_collat_coeff.rs @@ -1,14 +1,15 @@ use common::FixedI128; -use pool_interface::types::{error::Error, reserve_data::ReserveData}; +use pool_interface::types::error::Error; +use pool_interface::types::pool_config::PoolConfig; +use pool_interface::types::reserve_data::ReserveData; use soroban_sdk::Env; use super::rate::get_actual_lender_accrued_rate; -/// Returns collateral coefficient -/// collateral_coeff = [underlying_balance + lender_ar * total_debt_token]/total_stoken pub fn get_collat_coeff( env: &Env, reserve: &ReserveData, + pool_config: &PoolConfig, s_token_supply: i128, s_token_underlying_balance: i128, debt_token_supply: i128, @@ -17,7 +18,7 @@ pub fn get_collat_coeff( return Ok(FixedI128::ONE); } - let collat_ar = get_actual_lender_accrued_rate(env, reserve)?; + let collat_ar = get_actual_lender_accrued_rate(env, reserve, pool_config)?; FixedI128::from_rational( s_token_underlying_balance @@ -31,3 +32,78 @@ pub fn get_collat_coeff( ) .ok_or(Error::CollateralCoeffMathError) } + +pub fn get_compounded_amount( + env: &Env, + reserve: &ReserveData, + pool_config: &PoolConfig, + s_token_supply: i128, + s_token_underlying_balance: i128, + debt_token_supply: i128, + amount: i128, +) -> Result { + if s_token_supply == 0 { + return Ok(amount); + } + + let collat_ar = get_actual_lender_accrued_rate(env, reserve, pool_config)?; + + let x1 = collat_ar + .mul_int(debt_token_supply) + .ok_or(Error::CollateralCoeffMathError)?; + + let x2 = s_token_underlying_balance + .checked_add(x1) + .ok_or(Error::CollateralCoeffMathError)?; + + x2.checked_mul(amount) + .ok_or(Error::CollateralCoeffMathError)? + .checked_div(s_token_supply) + .ok_or(Error::CollateralCoeffMathError) +} + +#[allow(clippy::too_many_arguments)] +pub fn get_lp_amount( + env: &Env, + reserve: &ReserveData, + pool_config: &PoolConfig, + s_token_supply: i128, + s_token_underlying_balance: i128, + debt_token_supply: i128, + amount: i128, + round_ceil: bool, +) -> Result { + if s_token_supply == 0 { + return Ok(amount); + } + + let collat_ar = get_actual_lender_accrued_rate(env, reserve, pool_config)?; + + let x1 = collat_ar + .mul_int(debt_token_supply) + .ok_or(Error::CollateralCoeffMathError)?; + + let nom = s_token_supply + .checked_mul(amount) + .ok_or(Error::CollateralCoeffMathError)?; + + let denom = s_token_underlying_balance + .checked_add(x1) + .ok_or(Error::CollateralCoeffMathError)?; + + let result = nom + .checked_div(denom) + .ok_or(Error::CollateralCoeffMathError)?; + + if !round_ceil { + return Ok(result); + } + + Ok(if result == 0 { + 1 + } else if nom % denom == 0 { + result + } else { + result + 1 + }) +} diff --git a/contracts/pool/src/methods/utils/get_elapsed_time.rs b/contracts/pool/src/methods/utils/get_elapsed_time.rs index 1972b68a..f40e80fa 100644 --- a/contracts/pool/src/methods/utils/get_elapsed_time.rs +++ b/contracts/pool/src/methods/utils/get_elapsed_time.rs @@ -1,15 +1,15 @@ use soroban_sdk::Env; -use crate::storage::read_reserve_timestamp_window; - -/// Returns (current_time, elapsed_time) -pub fn get_elapsed_time(env: &Env, last_update_timestamp: u64) -> (u64, u64) { +pub fn get_elapsed_time( + env: &Env, + last_update_timestamp: u64, + reserve_timestamp_window: u64, +) -> (u64, u64) { let current_time = env.ledger().timestamp(); - let window = read_reserve_timestamp_window(env); current_time .checked_sub(last_update_timestamp) - .and_then(|el| el.checked_rem(window)) + .and_then(|el| el.checked_rem(reserve_timestamp_window)) .and_then(|rem| current_time.checked_sub(rem)) .and_then(|cur_t| cur_t.checked_sub(last_update_timestamp)) .map_or((current_time, 0), |el| (current_time, el)) diff --git a/contracts/pool/src/methods/utils/get_fungible_lp_tokens.rs b/contracts/pool/src/methods/utils/get_fungible_lp_tokens.rs deleted file mode 100644 index bf3a9f2a..00000000 --- a/contracts/pool/src/methods/utils/get_fungible_lp_tokens.rs +++ /dev/null @@ -1,10 +0,0 @@ -use pool_interface::types::{error::Error, reserve_data::ReserveData, reserve_type::ReserveType}; -use soroban_sdk::Address; - -pub fn get_fungible_lp_tokens(reserve: &ReserveData) -> Result<(&Address, &Address), Error> { - if let ReserveType::Fungible(s_token_address, debt_token_address) = &reserve.reserve_type { - Ok((s_token_address, debt_token_address)) - } else { - Err(Error::NotFungible) - } -} diff --git a/contracts/pool/src/methods/utils/mod.rs b/contracts/pool/src/methods/utils/mod.rs index 5ba55bbd..9a016063 100644 --- a/contracts/pool/src/methods/utils/mod.rs +++ b/contracts/pool/src/methods/utils/mod.rs @@ -1,6 +1,5 @@ pub mod get_collat_coeff; pub mod get_elapsed_time; -pub mod get_fungible_lp_tokens; pub mod rate; pub mod recalculate_reserve_data; pub mod validation; diff --git a/contracts/pool/src/methods/utils/rate.rs b/contracts/pool/src/methods/utils/rate.rs index 6c2519de..03f4d812 100644 --- a/contracts/pool/src/methods/utils/rate.rs +++ b/contracts/pool/src/methods/utils/rate.rs @@ -1,24 +1,15 @@ use common::{FixedI128, ALPHA_DENOMINATOR}; use pool_interface::types::error::Error; -use pool_interface::types::ir_params::IRParams; +use pool_interface::types::pool_config::PoolConfig; use pool_interface::types::reserve_data::ReserveData; use soroban_sdk::Env; use super::get_elapsed_time::get_elapsed_time; -/// Calculate interest rate IR = MIN [ max_rate, base_rate / (1 - U)^alpha] -/// where -/// U - utilization, U = total_debt / total_collateral -/// ir_params.alpha - parameter, by default 1.43 expressed as 143 with denominator 100 -/// ir_params.max_rate - maximal value of interest rate, by default 500% expressed as 50000 with denominator 10000 -/// ir_params.initial_rate - base interest rate, by default 2%, expressed as 200 with denominator 10000 -/// -/// For (1-U)^alpha calculation use binomial approximation with four terms -/// (1-U)^a = 1 - alpha * U + alpha/2 * (alpha - 1) * U^2 - alpha/6 * (alpha-1) * (alpha-2) * U^3 + alpha/24 * (alpha-1) *(alpha-2) * (alpha-3) * U^4 pub fn calc_interest_rate( total_collateral: i128, total_debt: i128, - ir_params: &IRParams, + pool_config: &PoolConfig, ) -> Option { if total_collateral.is_negative() || total_debt.is_negative() { return None; @@ -30,26 +21,26 @@ pub fn calc_interest_rate( return Some(FixedI128::ZERO); } - let max_rate = FixedI128::from_percentage(ir_params.max_rate)?; + let max_rate = FixedI128::from_percentage(pool_config.ir_max_rate)?; if u >= FixedI128::ONE { - return Some(max_rate); // utilization shouldn't be greater or equal one + return Some(max_rate); } - let alpha = FixedI128::from_rational(ir_params.alpha, ALPHA_DENOMINATOR)?; + let alpha = FixedI128::from_rational(pool_config.ir_alpha, ALPHA_DENOMINATOR)?; let neg_u = u.mul_inner(-1)?; let first_term = alpha.checked_mul(neg_u)?; let num_of_iterations = if u > FixedI128::from_rational(1, 2)? { - 19 + 20 } else { - 3 + 4 }; let mut prev_term = first_term; let mut terms_sum = first_term; let mut alpha_mul = alpha; - for i in 2..(num_of_iterations + 2) { + for i in 2..=num_of_iterations { alpha_mul = alpha_mul.checked_sub(FixedI128::ONE)?; let next_term = prev_term .checked_mul(neg_u)? @@ -65,18 +56,13 @@ pub fn calc_interest_rate( return Some(max_rate); } - let initial_rate = FixedI128::from_percentage(ir_params.initial_rate)?; + let initial_rate = FixedI128::from_percentage(pool_config.ir_initial_rate)?; let ir = initial_rate.checked_div(denom)?; Some(FixedI128::min(ir, max_rate)) } -/// Calculate accrued rate on time `t` AR(t) = AR(t-1)*(1 + r(t-1)*elapsed_time) -/// where: -/// AR(t-1) - prev value of accrued rate -/// r(t-1) - prev value of interest rate -/// elapsed_time - elapsed time in seconds from last accrued rate update pub fn calc_next_accrued_rate( prev_ar: FixedI128, ir: FixedI128, @@ -94,17 +80,16 @@ pub struct AccruedRates { pub borrower_ir: FixedI128, } -/// Calculates lender and borrower accrued/interest rates pub fn calc_accrued_rates( total_collateral: i128, total_debt: i128, elapsed_time: u64, - ir_params: IRParams, + pool_config: &PoolConfig, reserve_data: &ReserveData, ) -> Option { - let borrower_ir = calc_interest_rate(total_collateral, total_debt, &ir_params)?; + let borrower_ir = calc_interest_rate(total_collateral, total_debt, pool_config)?; - let scale_coeff = FixedI128::from_percentage(ir_params.scaling_coeff)?; + let scale_coeff = FixedI128::from_percentage(pool_config.ir_scaling_coeff)?; let lender_ir = borrower_ir.checked_mul(scale_coeff)?; let borrower_ar = calc_next_accrued_rate( @@ -127,12 +112,16 @@ pub fn calc_accrued_rates( }) } -/// Returns lender accrued rate corrected for the current time pub fn get_actual_lender_accrued_rate( env: &Env, reserve: &ReserveData, + pool_config: &PoolConfig, ) -> Result { - let (_, elapsed_time) = get_elapsed_time(env, reserve.last_update_timestamp); + let (_, elapsed_time) = get_elapsed_time( + env, + reserve.last_update_timestamp, + pool_config.timestamp_window, + ); let prev_ar = FixedI128::from_inner(reserve.lender_ar); if elapsed_time == 0 { @@ -144,12 +133,16 @@ pub fn get_actual_lender_accrued_rate( } } -/// Returns borrower accrued rate corrected for the current time pub fn get_actual_borrower_accrued_rate( env: &Env, reserve: &ReserveData, + pool_config: &PoolConfig, ) -> Result { - let (_, elapsed_time) = get_elapsed_time(env, reserve.last_update_timestamp); + let (_, elapsed_time) = get_elapsed_time( + env, + reserve.last_update_timestamp, + pool_config.timestamp_window, + ); let prev_ar = FixedI128::from_inner(reserve.borrower_ar); if elapsed_time == 0 { diff --git a/contracts/pool/src/methods/utils/recalculate_reserve_data.rs b/contracts/pool/src/methods/utils/recalculate_reserve_data.rs index ca71a8fe..ffb23a84 100644 --- a/contracts/pool/src/methods/utils/recalculate_reserve_data.rs +++ b/contracts/pool/src/methods/utils/recalculate_reserve_data.rs @@ -1,7 +1,9 @@ -use pool_interface::types::{error::Error, reserve_data::ReserveData}; +use pool_interface::types::error::Error; +use pool_interface::types::pool_config::PoolConfig; +use pool_interface::types::reserve_data::ReserveData; use soroban_sdk::{Address, Env}; -use crate::storage::{read_ir_params, write_reserve}; +use crate::storage::write_reserve; use super::{get_elapsed_time::get_elapsed_time, rate::calc_accrued_rates}; @@ -9,21 +11,25 @@ pub fn recalculate_reserve_data( env: &Env, asset: &Address, reserve: &ReserveData, + pool_config: &PoolConfig, s_token_supply: i128, debt_token_supply: i128, ) -> Result { - let (current_time, elapsed_time) = get_elapsed_time(env, reserve.last_update_timestamp); + let (current_time, elapsed_time) = get_elapsed_time( + env, + reserve.last_update_timestamp, + pool_config.timestamp_window, + ); if elapsed_time == 0 || s_token_supply == 0 { return Ok(reserve.clone()); } - let ir_params = read_ir_params(env)?; let accrued_rates = calc_accrued_rates( s_token_supply, debt_token_supply, elapsed_time, - ir_params, + pool_config, reserve, ) .ok_or(Error::AccruedRateMathError)?; diff --git a/contracts/pool/src/methods/utils/validation.rs b/contracts/pool/src/methods/utils/validation.rs index 913db504..3ba7f700 100644 --- a/contracts/pool/src/methods/utils/validation.rs +++ b/contracts/pool/src/methods/utils/validation.rs @@ -1,13 +1,16 @@ -use common::{FixedI128, PERCENTAGE_FACTOR}; +use common::FixedI128; +use common::ONE_DAY; +use common::PERCENTAGE_FACTOR; use pool_interface::types::collateral_params_input::CollateralParamsInput; use pool_interface::types::error::Error; -use pool_interface::types::ir_params::IRParams; +use pool_interface::types::pause_info::PauseInfo; +use pool_interface::types::pool_config::PoolConfig; use pool_interface::types::reserve_data::ReserveData; use pool_interface::types::reserve_type::ReserveType; use pool_interface::types::user_config::UserConfiguration; use soroban_sdk::{assert_with_error, panic_with_error, Address, Env}; -use crate::storage::{has_admin, has_reserve, paused, read_admin, read_initial_health}; +use crate::storage::{has_admin, read_admin}; use crate::types::account_data::AccountData; use crate::{read_reserve, read_reserves}; @@ -23,24 +26,10 @@ pub fn require_admin(env: &Env) -> Result<(), Error> { Ok(()) } -pub fn require_valid_ir_params(env: &Env, params: &IRParams) { - require_lte_percentage_factor(env, params.initial_rate); - require_gt_percentage_factor(env, params.max_rate); - require_lt_percentage_factor(env, params.scaling_coeff); -} - pub fn require_valid_collateral_params(env: &Env, params: &CollateralParamsInput) { require_lte_percentage_factor(env, params.discount); require_lte_percentage_factor(env, params.util_cap); - require_positive(env, params.liq_cap); -} - -pub fn require_uninitialized_reserve(env: &Env, asset: &Address) { - assert_with_error!( - env, - !has_reserve(env, asset), - Error::ReserveAlreadyInitialized - ); + assert_with_error!(env, params.liq_cap > 0, Error::BellowMinValue); } pub fn require_lte_percentage_factor(env: &Env, value: u32) { @@ -67,8 +56,8 @@ pub fn require_gt_percentage_factor(env: &Env, value: u32) { ); } -pub fn require_positive(env: &Env, value: i128) { - assert_with_error!(env, value > 0, Error::MustBePositive); +pub fn require_non_negative(env: &Env, value: i128) { + assert_with_error!(env, value >= 0, Error::MustBeNonNegative); } pub fn require_positive_amount(env: &Env, amount: i128) { @@ -83,11 +72,10 @@ pub fn require_borrowing_enabled(env: &Env, reserve: &ReserveData) { assert_with_error!( env, reserve.configuration.borrowing_enabled, - Error::BorrowingNotEnabled + Error::BorrowingDisabled ); } -/// Check that balance + deposit + debt * ar_lender <= reserve.configuration.liquidity_cap pub fn require_liquidity_cap_not_exceeded( env: &Env, reserve: &ReserveData, @@ -106,7 +94,7 @@ pub fn require_liquidity_cap_not_exceeded( assert_with_error!( env, balance_after_deposit <= reserve.configuration.liquidity_cap, - Error::LiqCapExceeded + Error::ExceededMaxValue ); Ok(()) @@ -126,7 +114,7 @@ pub fn require_util_cap_not_exceeded( .ok_or(Error::ValidateBorrowMathError)?; let util_cap = FixedI128::from_percentage(util_cap).ok_or(Error::ValidateBorrowMathError)?; - assert_with_error!(env, utilization <= util_cap, Error::UtilizationCapExceeded); + assert_with_error!(env, utilization <= util_cap, Error::ExceededMaxValue); Ok(()) } @@ -134,45 +122,55 @@ pub fn require_util_cap_not_exceeded( pub fn require_gte_initial_health( env: &Env, account_data: &AccountData, - borrow_amount_in_base: i128, + pool_config: &PoolConfig, ) -> Result<(), Error> { - let npv_after = account_data - .npv - .checked_sub(borrow_amount_in_base) - .ok_or(Error::MathOverflowError)?; - let npv_after_percent = FixedI128::from_rational(npv_after, account_data.discounted_collateral) - .ok_or(Error::MathOverflowError)?; + if account_data.npv == 0 && account_data.discounted_collateral == 0 { + return Ok(()); + } + + assert_with_error!( + env, + account_data.npv >= 0 && account_data.discounted_collateral >= 0, + Error::BellowMinValue + ); + + let npv_after_percent = + FixedI128::from_rational(account_data.npv, account_data.discounted_collateral) + .ok_or(Error::MathOverflowError)?; let initial_health_percent = - FixedI128::from_percentage(read_initial_health(env)?).ok_or(Error::MathOverflowError)?; + FixedI128::from_percentage(pool_config.initial_health).ok_or(Error::MathOverflowError)?; assert_with_error!( env, npv_after_percent >= initial_health_percent, - Error::BelowInitialHealth + Error::BellowMinValue ); Ok(()) } -pub fn require_good_position(env: &Env, account_data: &AccountData) { - assert_with_error!(env, account_data.is_good_position(), Error::BadPosition); -} - pub fn require_not_in_collateral_asset(env: &Env, collat_balance: i128) { - // `is_using_as_collateral` is skipped to avoid case when user: - // makes deposit => disables `is_using_as_collateral` => borrows the asset assert_with_error!(env, collat_balance == 0, Error::MustNotBeInCollateralAsset); } -pub fn require_not_paused(env: &Env) { - assert_with_error!(env, !paused(env), Error::Paused); +pub fn require_not_paused(env: &Env, pause_info: &PauseInfo) { + assert_with_error!(env, !pause_info.paused, Error::Paused); +} + +pub fn require_not_in_grace_period(env: &Env, pause_info: &PauseInfo) { + let now = env.ledger().timestamp(); + assert_with_error!( + env, + now >= pause_info.grace_period_ends_at(), + Error::GracePeriod + ); } pub fn require_debt(env: &Env, user_config: &UserConfiguration, reserve_id: u8) { assert_with_error!( env, user_config.is_borrowing(env, reserve_id), - Error::MustHaveDebt + Error::DebtError ); } @@ -180,7 +178,7 @@ pub fn require_zero_debt(env: &Env, user_config: &UserConfiguration, reserve_id: assert_with_error!( env, !user_config.is_borrowing(env, reserve_id), - Error::MustNotHaveDebt + Error::DebtError ); } @@ -213,3 +211,62 @@ pub fn require_unique_liquidation_order( Ok(()) } + +pub fn require_not_exceed_assets_limit(env: &Env, assets_total: u32, assets_limit: u32) { + assert_with_error!(env, assets_total <= assets_limit, Error::ExceededMaxValue); +} + +pub fn require_min_position_amounts( + env: &Env, + account_data: &AccountData, + pool_config: &PoolConfig, +) -> Result<(), Error> { + if account_data.debt == 0 { + return Ok(()); + } + + assert_with_error!( + env, + account_data.discounted_collateral >= pool_config.min_collat_amount, + Error::BellowMinValue + ); + assert_with_error!( + env, + account_data.debt >= pool_config.min_debt_amount, + Error::BellowMinValue + ); + + Ok(()) +} + +pub fn require_valid_pool_config(env: &Env, config: &PoolConfig) { + require_lte_percentage_factor(env, config.initial_health); + require_lte_percentage_factor(env, config.flash_loan_fee); + require_lte_percentage_factor(env, config.liquidation_protocol_fee); + require_non_negative(env, config.min_collat_amount); + require_non_negative(env, config.min_debt_amount); + require_lte_percentage_factor(env, config.ir_initial_rate); + require_gt_percentage_factor(env, config.ir_max_rate); + require_lt_percentage_factor(env, config.ir_scaling_coeff); + + assert_with_error!(env, config.ir_scaling_coeff > 0, Error::MustBeNonNegative); + assert_with_error!( + env, + config.ir_initial_rate <= config.ir_max_rate, + Error::ExceededMaxValue + ); + + assert_with_error!( + env, + config.base_asset_decimals <= 38, + Error::ExceededMaxValue + ); + assert_with_error!(env, config.grace_period != 0, Error::BellowMinValue); + assert_with_error!(env, config.grace_period <= ONE_DAY, Error::ExceededMaxValue); + assert_with_error!( + env, + config.timestamp_window <= ONE_DAY, + Error::ExceededMaxValue + ); + assert_with_error!(env, config.user_assets_limit > 0, Error::BellowMinValue); +} diff --git a/contracts/pool/src/methods/withdraw.rs b/contracts/pool/src/methods/withdraw.rs index f945eb07..ae1e7257 100644 --- a/contracts/pool/src/methods/withdraw.rs +++ b/contracts/pool/src/methods/withdraw.rs @@ -1,11 +1,16 @@ -use crate::event; +use crate::add_token_balance; +use crate::methods::utils::get_collat_coeff::get_compounded_amount; +use crate::methods::utils::get_collat_coeff::get_lp_amount; +use crate::methods::utils::validation::require_gte_initial_health; +use crate::read_pool_config; use crate::storage::{ - add_stoken_underlying_balance, read_reserve, read_stoken_underlying_balance, - read_token_balance, read_token_total_supply, write_token_balance, write_token_total_supply, + read_reserve, read_token_balance, read_token_total_supply, write_token_balance, + write_token_total_supply, }; use crate::types::calc_account_data_cache::CalcAccountDataCache; use crate::types::price_provider::PriceProvider; use crate::types::user_configurator::UserConfigurator; +use crate::{event, read_pause_info}; use pool_interface::types::asset_balance::AssetBalance; use pool_interface::types::error::Error; use pool_interface::types::reserve_type::ReserveType; @@ -13,10 +18,10 @@ use s_token_interface::STokenClient; use soroban_sdk::{assert_with_error, token, Address, Env}; use super::account_position::calc_account_data; -use super::utils::get_collat_coeff::get_collat_coeff; use super::utils::recalculate_reserve_data::recalculate_reserve_data; use super::utils::validation::{ - require_active_reserve, require_good_position, require_not_paused, require_positive_amount, + require_active_reserve, require_min_position_amounts, require_not_in_grace_period, + require_not_paused, require_positive_amount, }; pub fn withdraw( @@ -28,39 +33,52 @@ pub fn withdraw( ) -> Result<(), Error> { who.require_auth(); - require_not_paused(env); + let pause_info = read_pause_info(env); + require_not_paused(env, &pause_info); + require_not_in_grace_period(env, &pause_info); + require_positive_amount(env, amount); let reserve = read_reserve(env, asset)?; require_active_reserve(env, &reserve); - let mut user_configurator = UserConfigurator::new(env, who, false); - let user_config = user_configurator.user_config()?; + let mut user_configurator = UserConfigurator::new(env, who, false, None); + + let pool_config = read_pool_config(env)?; - let (withdraw_amount, is_full_withdraw) = + let withdraw_amount = if let ReserveType::Fungible(s_token_address, debt_token_address) = &reserve.reserve_type { let s_token_supply = read_token_total_supply(env, s_token_address); let debt_token_supply = read_token_total_supply(env, debt_token_address); - let collat_coeff = get_collat_coeff( + + let s_token = STokenClient::new(env, s_token_address); + + let collat_balance = read_token_balance(env, s_token_address, who); + let stoken_underlying_balance = read_token_balance(env, asset, s_token_address); + + let underlying_balance = get_compounded_amount( env, &reserve, + &pool_config, s_token_supply, - read_stoken_underlying_balance(env, s_token_address), + stoken_underlying_balance, debt_token_supply, + collat_balance, )?; - let s_token = STokenClient::new(env, s_token_address); - - let collat_balance = read_token_balance(env, s_token_address, who); - let underlying_balance = collat_coeff - .mul_int(collat_balance) - .ok_or(Error::MathOverflowError)?; - let (underlying_to_withdraw, s_token_to_burn) = if amount >= underlying_balance { (underlying_balance, collat_balance) } else { - let s_token_to_burn = collat_coeff - .recip_mul_int(amount) - .ok_or(Error::MathOverflowError)?; + let s_token_to_burn = get_lp_amount( + env, + &reserve, + &pool_config, + s_token_supply, + stoken_underlying_balance, + debt_token_supply, + amount, + true, + )?; + (amount, s_token_to_burn) }; @@ -76,13 +94,15 @@ pub fn withdraw( let s_token_supply_after = s_token_supply .checked_sub(s_token_to_burn) .ok_or(Error::InvalidAmount)?; - let s_token_underlying_after = read_stoken_underlying_balance(env, s_token_address) + let s_token_underlying_after = stoken_underlying_balance .checked_sub(underlying_to_withdraw) .ok_or(Error::MathOverflowError)?; - if user_config.is_borrowing_any() - && user_config.is_using_as_collateral(env, reserve.get_id()) - { + user_configurator.withdraw(reserve.get_id(), asset, collat_balance_after == 0)?; + + let is_borrowing_any = user_configurator.user_config()?.is_borrowing_any(); + + if is_borrowing_any { let account_data = calc_account_data( env, who, @@ -106,20 +126,23 @@ pub fn withdraw( )), mb_rwa_balance: None, }, - user_config, - &mut PriceProvider::new(env)?, + &pool_config, + user_configurator.user_config()?, + &mut PriceProvider::new(env, &pool_config)?, false, )?; - // TODO: do we need to check for initial_health? - require_good_position(env, &account_data); + + require_min_position_amounts(env, &account_data, &pool_config)?; + require_gte_initial_health(env, &account_data, &pool_config)?; } + let amount_to_sub = underlying_to_withdraw .checked_neg() .ok_or(Error::MathOverflowError)?; s_token.burn(who, &s_token_to_burn, &underlying_to_withdraw, to); - add_stoken_underlying_balance(env, &s_token.address, amount_to_sub)?; + add_token_balance(env, asset, &s_token.address, amount_to_sub)?; write_token_total_supply(env, &s_token.address, s_token_supply_after)?; write_token_balance(env, &s_token.address, who, collat_balance_after)?; @@ -127,23 +150,23 @@ pub fn withdraw( env, asset, &reserve, + &pool_config, s_token_supply_after, debt_token_supply, )?; - ( - underlying_to_withdraw, - underlying_to_withdraw == underlying_balance, - ) + underlying_to_withdraw } else { let rwa_balance = read_token_balance(env, asset, who); let withdraw_amount = amount.min(rwa_balance); let rwa_balance_after = rwa_balance - withdraw_amount; - if user_config.is_borrowing_any() - && user_config.is_using_as_collateral(env, reserve.get_id()) - { + user_configurator.withdraw(reserve.get_id(), asset, rwa_balance_after == 0)?; + + let is_borrowing_any = user_configurator.user_config()?.is_borrowing_any(); + + if is_borrowing_any { let account_data = calc_account_data( env, who, @@ -155,14 +178,16 @@ pub fn withdraw( mb_s_token_underlying_balance: None, mb_rwa_balance: Some(&AssetBalance::new(asset.clone(), rwa_balance_after)), }, - user_config, - &mut PriceProvider::new(env)?, + &pool_config, + user_configurator.user_config()?, + &mut PriceProvider::new(env, &pool_config)?, false, )?; - // TODO: do we need to check for initial_health? - require_good_position(env, &account_data); + require_min_position_amounts(env, &account_data, &pool_config)?; + require_gte_initial_health(env, &account_data, &pool_config)?; } + token::Client::new(env, asset).transfer( &env.current_contract_address(), who, @@ -171,12 +196,10 @@ pub fn withdraw( write_token_balance(env, asset, who, rwa_balance_after)?; - (withdraw_amount, rwa_balance_after == 0) + withdraw_amount }; - user_configurator - .withdraw(reserve.get_id(), asset, is_full_withdraw)? - .write(); + user_configurator.write(); event::withdraw(env, who, asset, to, withdraw_amount); diff --git a/contracts/pool/src/storage.rs b/contracts/pool/src/storage.rs index 0886e1a3..c3a90903 100644 --- a/contracts/pool/src/storage.rs +++ b/contracts/pool/src/storage.rs @@ -1,6 +1,6 @@ -use pool_interface::types::base_asset_config::BaseAssetConfig; use pool_interface::types::error::Error; -use pool_interface::types::ir_params::IRParams; +use pool_interface::types::pause_info::PauseInfo; +use pool_interface::types::pool_config::PoolConfig; use pool_interface::types::price_feed_config::PriceFeedConfig; use pool_interface::types::price_feed_config_input::PriceFeedConfigInput; use pool_interface::types::reserve_data::ReserveData; @@ -9,52 +9,41 @@ use soroban_sdk::{assert_with_error, contracttype, vec, Address, Env, Vec}; pub(crate) const DAY_IN_LEDGERS: u32 = 17_280; -pub(crate) const LOW_USER_DATA_BUMP_LEDGERS: u32 = 10 * DAY_IN_LEDGERS; // 20 days -pub(crate) const HIGH_USER_DATA_BUMP_LEDGERS: u32 = 20 * DAY_IN_LEDGERS; // 30 days +pub(crate) const LOW_USER_DATA_BUMP_LEDGERS: u32 = 10 * DAY_IN_LEDGERS; +pub(crate) const HIGH_USER_DATA_BUMP_LEDGERS: u32 = 20 * DAY_IN_LEDGERS; -pub(crate) const LOW_INSTANCE_BUMP_LEDGERS: u32 = DAY_IN_LEDGERS; // 1 day -pub(crate) const HIGH_INSTANCE_BUMP_LEDGERS: u32 = 7 * DAY_IN_LEDGERS; // 7 days +pub(crate) const LOW_INSTANCE_BUMP_LEDGERS: u32 = DAY_IN_LEDGERS; +pub(crate) const HIGH_INSTANCE_BUMP_LEDGERS: u32 = 7 * DAY_IN_LEDGERS; #[derive(Clone)] #[contracttype] pub enum DataKey { Admin, - BaseAsset, Reserves, ReserveAssetKey(Address), - ReserveTimestampWindow, - Treasury, - IRParams, UserConfig(Address), PriceFeed(Address), Pause, - FlashLoanFee, - STokenUnderlyingBalance(Address), TokenSupply(Address), TokenBalance(Address, Address), - InitialHealth, + PoolConfig, + ProtocolFeeVault(Address), } pub fn has_admin(env: &Env) -> bool { - env.storage() - .instance() - .extend_ttl(LOW_INSTANCE_BUMP_LEDGERS, HIGH_INSTANCE_BUMP_LEDGERS); + bump_instance(env); env.storage().instance().has(&DataKey::Admin) } pub fn write_admin(env: &Env, admin: &Address) { - env.storage() - .instance() - .extend_ttl(LOW_INSTANCE_BUMP_LEDGERS, HIGH_INSTANCE_BUMP_LEDGERS); + bump_instance(env); env.storage().instance().set(&DataKey::Admin, admin); } pub fn read_admin(env: &Env) -> Result { - env.storage() - .instance() - .extend_ttl(LOW_INSTANCE_BUMP_LEDGERS, HIGH_INSTANCE_BUMP_LEDGERS); + bump_instance(env); env.storage() .instance() @@ -62,59 +51,24 @@ pub fn read_admin(env: &Env) -> Result { .ok_or(Error::Uninitialized) } -pub fn write_ir_params(env: &Env, ir_params: &IRParams) { - env.storage() - .instance() - .extend_ttl(LOW_INSTANCE_BUMP_LEDGERS, HIGH_INSTANCE_BUMP_LEDGERS); - - env.storage().instance().set(&DataKey::IRParams, ir_params); -} - -pub fn read_ir_params(env: &Env) -> Result { - env.storage() - .instance() - .extend_ttl(LOW_INSTANCE_BUMP_LEDGERS, HIGH_INSTANCE_BUMP_LEDGERS); - - env.storage() - .instance() - .get(&DataKey::IRParams) - .ok_or(Error::Uninitialized) -} - pub fn read_reserve(env: &Env, asset: &Address) -> Result { - env.storage() - .instance() - .extend_ttl(LOW_INSTANCE_BUMP_LEDGERS, HIGH_INSTANCE_BUMP_LEDGERS); + bump_instance(env); env.storage() .instance() .get(&DataKey::ReserveAssetKey(asset.clone())) - .ok_or(Error::NoReserveExistForAsset) + .ok_or(Error::Uninitialized) } pub fn write_reserve(env: &Env, asset: &Address, reserve_data: &ReserveData) { - env.storage() - .instance() - .extend_ttl(LOW_INSTANCE_BUMP_LEDGERS, HIGH_INSTANCE_BUMP_LEDGERS); + bump_instance(env); let asset_key: DataKey = DataKey::ReserveAssetKey(asset.clone()); env.storage().instance().set(&asset_key, reserve_data); } -pub fn has_reserve(env: &Env, asset: &Address) -> bool { - env.storage() - .instance() - .extend_ttl(LOW_INSTANCE_BUMP_LEDGERS, HIGH_INSTANCE_BUMP_LEDGERS); - - env.storage() - .instance() - .has(&DataKey::ReserveAssetKey(asset.clone())) -} - pub fn read_reserves(env: &Env) -> Vec
{ - env.storage() - .instance() - .extend_ttl(LOW_INSTANCE_BUMP_LEDGERS, HIGH_INSTANCE_BUMP_LEDGERS); + bump_instance(env); env.storage() .instance() @@ -122,31 +76,8 @@ pub fn read_reserves(env: &Env) -> Vec
{ .unwrap_or(vec![env]) } -pub fn read_reserve_timestamp_window(env: &Env) -> u64 { - env.storage() - .instance() - .extend_ttl(LOW_INSTANCE_BUMP_LEDGERS, HIGH_INSTANCE_BUMP_LEDGERS); - - env.storage() - .instance() - .get(&DataKey::ReserveTimestampWindow) - .unwrap_or(20) -} - -pub fn write_reserve_timestamp_window(env: &Env, window: u64) { - env.storage() - .instance() - .extend_ttl(LOW_INSTANCE_BUMP_LEDGERS, HIGH_INSTANCE_BUMP_LEDGERS); - - env.storage() - .instance() - .set(&DataKey::ReserveTimestampWindow, &window); -} - pub fn write_reserves(env: &Env, reserves: &Vec
) { - env.storage() - .instance() - .extend_ttl(LOW_INSTANCE_BUMP_LEDGERS, HIGH_INSTANCE_BUMP_LEDGERS); + bump_instance(env); env.storage().instance().set(&DataKey::Reserves, reserves); } @@ -163,7 +94,7 @@ pub fn read_user_config(env: &Env, user: &Address) -> Result Result { - env.storage() - .instance() - .extend_ttl(LOW_INSTANCE_BUMP_LEDGERS, HIGH_INSTANCE_BUMP_LEDGERS); + bump_instance(env); let data_key = DataKey::PriceFeed(asset.clone()); env.storage() .instance() .get(&data_key) - .ok_or(Error::NoPriceFeed) + .ok_or(Error::Uninitialized) } pub fn write_price_feeds(env: &Env, inputs: &Vec) { - env.storage() - .instance() - .extend_ttl(LOW_INSTANCE_BUMP_LEDGERS, HIGH_INSTANCE_BUMP_LEDGERS); + bump_instance(env); for input in inputs.iter() { let data_key = DataKey::PriceFeed(input.asset.clone()); - let config = PriceFeedConfig { - asset_decimals: input.asset_decimals, - feeds: input.feeds, - }; - - env.storage().instance().set(&data_key, &config); + env.storage().instance().set( + &data_key, + &PriceFeedConfig { + asset_decimals: input.asset_decimals, + min_sanity_price_in_base: input.min_sanity_price_in_base, + max_sanity_price_in_base: input.max_sanity_price_in_base, + feeds: input.feeds, + }, + ); } } -pub fn write_base_asset(env: &Env, config: &BaseAssetConfig) { - env.storage() - .instance() - .extend_ttl(LOW_INSTANCE_BUMP_LEDGERS, HIGH_INSTANCE_BUMP_LEDGERS); - - let data_key = DataKey::BaseAsset; - - env.storage().instance().set(&data_key, config); -} - -pub fn read_base_asset(env: &Env) -> Result { - env.storage() - .instance() - .extend_ttl(LOW_INSTANCE_BUMP_LEDGERS, HIGH_INSTANCE_BUMP_LEDGERS); - - let data_key = DataKey::BaseAsset; - - env.storage() - .instance() - .get(&data_key) - .ok_or(Error::BaseAssetNotInitialized) -} - -pub fn write_initial_health(env: &Env, value: u32) { - env.storage() - .instance() - .extend_ttl(LOW_INSTANCE_BUMP_LEDGERS, HIGH_INSTANCE_BUMP_LEDGERS); - - let data_key = DataKey::InitialHealth; - - env.storage().instance().set(&data_key, &value); -} - -pub fn read_initial_health(env: &Env) -> Result { - env.storage() - .instance() - .extend_ttl(LOW_INSTANCE_BUMP_LEDGERS, HIGH_INSTANCE_BUMP_LEDGERS); - - let data_key = DataKey::InitialHealth; - - env.storage() - .instance() - .get(&data_key) - .ok_or(Error::InitialHealthNotInitialized) -} - -pub fn paused(env: &Env) -> bool { - env.storage() - .instance() - .extend_ttl(LOW_INSTANCE_BUMP_LEDGERS, HIGH_INSTANCE_BUMP_LEDGERS); +pub fn read_pause_info(env: &Env) -> PauseInfo { + bump_instance(env); env.storage() .instance() .get(&DataKey::Pause) - .unwrap_or(false) + .unwrap_or(PauseInfo { + paused: false, + grace_period_secs: 0, + unpaused_at: 0, + }) } -pub fn write_pause(env: &Env, value: bool) { - env.storage() - .instance() - .extend_ttl(LOW_INSTANCE_BUMP_LEDGERS, HIGH_INSTANCE_BUMP_LEDGERS); +pub fn write_pause_info(env: &Env, value: PauseInfo) { + bump_instance(env); env.storage().instance().set(&DataKey::Pause, &value); } -pub fn write_treasury(env: &Env, treasury: &Address) { - env.storage() - .instance() - .extend_ttl(LOW_INSTANCE_BUMP_LEDGERS, HIGH_INSTANCE_BUMP_LEDGERS); - - env.storage().instance().set(&DataKey::Treasury, treasury); -} - -pub fn read_treasury(env: &Env) -> Address { - env.storage() - .instance() - .extend_ttl(LOW_INSTANCE_BUMP_LEDGERS, HIGH_INSTANCE_BUMP_LEDGERS); - - env.storage().instance().get(&DataKey::Treasury).unwrap() -} - -pub fn write_flash_loan_fee(env: &Env, fee: u32) { - env.storage() - .instance() - .extend_ttl(LOW_INSTANCE_BUMP_LEDGERS, HIGH_INSTANCE_BUMP_LEDGERS); - - env.storage().instance().set(&DataKey::FlashLoanFee, &fee); -} - -pub fn read_flash_loan_fee(env: &Env) -> u32 { - env.storage() - .instance() - .extend_ttl(LOW_INSTANCE_BUMP_LEDGERS, HIGH_INSTANCE_BUMP_LEDGERS); - - env.storage() - .instance() - .get(&DataKey::FlashLoanFee) - .unwrap() -} - -pub fn write_stoken_underlying_balance( - env: &Env, - s_token_address: &Address, - total_supply: i128, -) -> Result<(), Error> { - env.storage() - .instance() - .extend_ttl(LOW_INSTANCE_BUMP_LEDGERS, HIGH_INSTANCE_BUMP_LEDGERS); - - assert_with_error!(env, !total_supply.is_negative(), Error::MustBePositive); - - let data_key = DataKey::STokenUnderlyingBalance(s_token_address.clone()); - env.storage().instance().set(&data_key, &total_supply); - - Ok(()) -} - -pub fn read_stoken_underlying_balance(env: &Env, s_token_address: &Address) -> i128 { - env.storage() - .instance() - .extend_ttl(LOW_INSTANCE_BUMP_LEDGERS, HIGH_INSTANCE_BUMP_LEDGERS); - - let data_key = DataKey::STokenUnderlyingBalance(s_token_address.clone()); - env.storage().instance().get(&data_key).unwrap_or(0i128) -} - -pub fn add_stoken_underlying_balance( +pub fn add_token_balance( env: &Env, - s_token_address: &Address, + token: &Address, + account: &Address, amount: i128, ) -> Result { - let mut total_supply = read_stoken_underlying_balance(env, s_token_address); + let mut balance = read_token_balance(env, token, account); - total_supply = total_supply + balance = balance .checked_add(amount) .ok_or(Error::MathOverflowError)?; - write_stoken_underlying_balance(env, s_token_address, total_supply)?; + write_token_balance(env, token, account, balance)?; - Ok(total_supply) + Ok(balance) } pub fn read_token_total_supply(env: &Env, token: &Address) -> i128 { - env.storage() - .instance() - .extend_ttl(LOW_INSTANCE_BUMP_LEDGERS, HIGH_INSTANCE_BUMP_LEDGERS); + bump_instance(env); let key = DataKey::TokenSupply(token.clone()); env.storage().instance().get(&key).unwrap_or(0i128) @@ -362,11 +184,9 @@ pub fn write_token_total_supply( token: &Address, total_supply: i128, ) -> Result<(), Error> { - env.storage() - .instance() - .extend_ttl(LOW_INSTANCE_BUMP_LEDGERS, HIGH_INSTANCE_BUMP_LEDGERS); + bump_instance(env); - assert_with_error!(env, !total_supply.is_negative(), Error::MustBePositive); + assert_with_error!(env, !total_supply.is_negative(), Error::MustBeNonNegative); let data_key = DataKey::TokenSupply(token.clone()); env.storage().instance().set(&data_key, &total_supply); @@ -395,7 +215,7 @@ pub fn write_token_balance( account: &Address, balance: i128, ) -> Result<(), Error> { - assert_with_error!(env, !balance.is_negative(), Error::MustBePositive); + assert_with_error!(env, !balance.is_negative(), Error::MustBeNonNegative); let key = DataKey::TokenBalance(token.clone(), account.clone()); env.storage().persistent().set(&key, &balance); @@ -407,3 +227,54 @@ pub fn write_token_balance( Ok(()) } + +pub fn read_protocol_fee_vault(env: &Env, asset: &Address) -> i128 { + bump_instance(env); + + let key = DataKey::ProtocolFeeVault(asset.clone()); + let value = env.storage().instance().get(&key); + + value.unwrap_or(0) +} + +pub fn write_protocol_fee_vault(env: &Env, asset: &Address, balance: i128) { + assert_with_error!(env, !balance.is_negative(), Error::MustBeNonNegative); + let key = DataKey::ProtocolFeeVault(asset.clone()); + + env.storage().instance().set(&key, &balance); + bump_instance(env); +} + +pub fn add_protocol_fee_vault(env: &Env, asset: &Address, amount: i128) -> Result<(), Error> { + let mut balance = read_protocol_fee_vault(env, asset); + balance = balance + .checked_add(amount) + .ok_or(Error::MathOverflowError)?; + + write_protocol_fee_vault(env, asset, balance); + + Ok(()) +} + +pub fn write_pool_config(env: &Env, config: &PoolConfig) { + bump_instance(env); + + env.storage() + .instance() + .set(&DataKey::PoolConfig, &config.clone()); +} + +pub fn read_pool_config(env: &Env) -> Result { + bump_instance(env); + + env.storage() + .instance() + .get(&DataKey::PoolConfig) + .ok_or(Error::Uninitialized) +} + +fn bump_instance(env: &Env) { + env.storage() + .instance() + .extend_ttl(LOW_INSTANCE_BUMP_LEDGERS, HIGH_INSTANCE_BUMP_LEDGERS); +} diff --git a/contracts/pool/src/tests/account_position.rs b/contracts/pool/src/tests/account_position.rs index 4e18eeea..d6cd2583 100644 --- a/contracts/pool/src/tests/account_position.rs +++ b/contracts/pool/src/tests/account_position.rs @@ -7,7 +7,7 @@ use soroban_sdk::testutils::Address as _; use soroban_sdk::vec; #[test] -#[should_panic(expected = "HostError: Error(Contract, #202)")] +#[should_panic(expected = "HostError: Error(Contract, #1)")] fn should_fail_when_user_config_not_exist() { let env = Env::default(); env.mock_all_auths(); @@ -24,7 +24,22 @@ fn should_update_when_deposit_borrow_withdraw_liquidate_price_change() { env.mock_all_auths(); let sut = init_pool(&env, false); - sut.pool.set_initial_health(&2_500); + sut.pool.set_pool_configuration(&PoolConfig { + base_asset_address: sut.reserves[0].token.address.clone(), + base_asset_decimals: sut.reserves[0].token.decimals(), + flash_loan_fee: 5, + initial_health: 2_500, + grace_period: 1, + timestamp_window: 20, + user_assets_limit: 4, + min_collat_amount: 0, + min_debt_amount: 0, + liquidation_protocol_fee: 0, + ir_alpha: 143, + ir_initial_rate: 200, + ir_max_rate: 50_000, + ir_scaling_coeff: 9_000, + }); let debt_token = sut.reserves[1].token.address.clone(); let deposit_token = sut.reserves[0].token.address.clone(); @@ -69,7 +84,7 @@ fn should_update_when_deposit_borrow_withdraw_liquidate_price_change() { let position_after_change_price = sut.pool.account_position(&borrower); - sut.pool.liquidate(&lender, &borrower, &false); + sut.pool.liquidate(&lender, &borrower); let position_after_liquidate = sut.pool.account_position(&borrower); assert_eq!(position_after_deposit.discounted_collateral, 600_000); @@ -89,6 +104,6 @@ fn should_update_when_deposit_borrow_withdraw_liquidate_price_change() { assert_eq!(position_after_change_price.npv, -20_000); assert_eq!(position_after_liquidate.discounted_collateral, 358_700); - assert_eq!(position_after_liquidate.debt, 269_026); - assert_eq!(position_after_liquidate.npv, 89_674); + assert_eq!(position_after_liquidate.debt, 269_025); + assert_eq!(position_after_liquidate.npv, 89_675); } diff --git a/contracts/pool/src/tests/borrow.rs b/contracts/pool/src/tests/borrow.rs index cf9ee9f6..530a8cca 100644 --- a/contracts/pool/src/tests/borrow.rs +++ b/contracts/pool/src/tests/borrow.rs @@ -1,6 +1,7 @@ use super::sut::DAY; -use crate::tests::sut::{fill_pool, init_pool}; -use soroban_sdk::testutils::{Address as _, AuthorizedFunction, Events, Ledger}; +use crate::tests::sut::{fill_pool, init_pool, set_time}; +use pool_interface::types::pool_config::PoolConfig; +use soroban_sdk::testutils::{Address as _, AuthorizedFunction, Events}; use soroban_sdk::{symbol_short, vec, Address, Env, IntoVal, Symbol}; #[test] @@ -25,7 +26,7 @@ fn should_require_authorized_caller() { } #[test] -#[should_panic(expected = "HostError: Error(Contract, #3)")] +#[should_panic(expected = "HostError: Error(Contract, #2)")] fn should_fail_when_pool_paused() { let env = Env::default(); env.mock_all_auths(); @@ -39,7 +40,7 @@ fn should_fail_when_pool_paused() { } #[test] -#[should_panic(expected = "HostError: Error(Contract, #304)")] +#[should_panic(expected = "HostError: Error(Contract, #302)")] fn should_fail_when_invalid_amount() { let env = Env::default(); env.mock_all_auths(); @@ -52,7 +53,7 @@ fn should_fail_when_invalid_amount() { } #[test] -#[should_panic(expected = "HostError: Error(Contract, #101)")] +#[should_panic(expected = "HostError: Error(Contract, #100)")] fn should_fail_when_reserve_deactivated() { let env = Env::default(); env.mock_all_auths(); @@ -80,7 +81,7 @@ fn should_fail_when_borrowing_disabled() { } #[test] -#[should_panic(expected = "HostError: Error(Contract, #310)")] +#[should_panic(expected = "HostError: Error(Contract, #306)")] fn should_fail_when_borrowing_collat_asset() { let env = Env::default(); env.mock_all_auths(); @@ -94,7 +95,7 @@ fn should_fail_when_borrowing_collat_asset() { } #[test] -#[should_panic(expected = "HostError: Error(Contract, #311)")] +#[should_panic(expected = "HostError: Error(Contract, #4)")] fn should_fail_when_util_cap_exceeded() { let env = Env::default(); env.mock_all_auths(); @@ -110,7 +111,7 @@ fn should_fail_when_util_cap_exceeded() { } #[test] -#[should_panic(expected = "HostError: Error(Contract, #301)")] +#[should_panic(expected = "HostError: Error(Contract, #3)")] fn should_fail_when_collat_not_covers_amount() { let env = Env::default(); env.mock_all_auths(); @@ -123,7 +124,7 @@ fn should_fail_when_collat_not_covers_amount() { } #[test] -#[should_panic(expected = "HostError: Error(Contract, #202)")] +#[should_panic(expected = "HostError: Error(Contract, #1)")] fn should_fail_when_user_config_not_exist() { let env = Env::default(); env.mock_all_auths(); @@ -135,7 +136,7 @@ fn should_fail_when_user_config_not_exist() { } #[test] -#[should_panic(expected = "HostError: Error(Contract, #301)")] +#[should_panic(expected = "HostError: Error(Contract, #3)")] fn should_fail_when_lt_initial_health() { let env = Env::default(); env.mock_all_auths(); @@ -144,7 +145,22 @@ fn should_fail_when_lt_initial_health() { let (_, borrower, debt_config) = fill_pool(&env, &sut, false); let token_address = debt_config.token.address.clone(); - sut.pool.set_initial_health(&2_500); + sut.pool.set_pool_configuration(&PoolConfig { + base_asset_address: sut.reserves[0].token.address.clone(), + base_asset_decimals: sut.reserves[0].token.decimals(), + flash_loan_fee: 5, + initial_health: 2_500, + timestamp_window: 20, + grace_period: 1, + user_assets_limit: 4, + min_collat_amount: 0, + min_debt_amount: 0, + liquidation_protocol_fee: 0, + ir_alpha: 143, + ir_initial_rate: 200, + ir_max_rate: 50_000, + ir_scaling_coeff: 9_000, + }); sut.pool.borrow(&borrower, &token_address, &50_000_000); } @@ -165,6 +181,7 @@ fn should_change_user_config() { let is_borrowing_any_before = user_config.is_borrowing_any(); let is_borrowing_token_1_before = user_config.is_borrowing(&env, reserve_1.get_id()); let is_borrowing_token_2_before = user_config.is_borrowing(&env, reserve_2.get_id()); + let user_total_assets_before = user_config.total_assets(); sut.pool.borrow(&borrower, &token_1_address, &10_000_000); sut.pool.borrow(&borrower, &token_2_address, &10_000_000); @@ -173,6 +190,7 @@ fn should_change_user_config() { let is_borrowing_any_after_borrow = user_config.is_borrowing_any(); let is_borrowing_token_1_after_borrow = user_config.is_borrowing(&env, reserve_1.get_id()); let is_borrowing_token_2_after_borrow = user_config.is_borrowing(&env, reserve_2.get_id()); + let user_total_assets_after_borrow = user_config.total_assets(); sut.pool.repay(&borrower, &token_1_address, &i128::MAX); @@ -180,6 +198,7 @@ fn should_change_user_config() { let is_borrowing_any_after_repay_1 = user_config.is_borrowing_any(); let is_borrowing_token_1_after_repay_1 = user_config.is_borrowing(&env, reserve_1.get_id()); let is_borrowing_token_2_after_repay_1 = user_config.is_borrowing(&env, reserve_2.get_id()); + let user_total_assets_after_repay_1 = user_config.total_assets(); sut.pool.repay(&borrower, &token_2_address, &i128::MAX); @@ -187,22 +206,27 @@ fn should_change_user_config() { let is_borrowing_any_after_repay_2 = user_config.is_borrowing_any(); let is_borrowing_token_1_after_repay_2 = user_config.is_borrowing(&env, reserve_1.get_id()); let is_borrowing_token_2_after_repay_2 = user_config.is_borrowing(&env, reserve_2.get_id()); + let user_total_assets_after_repay_2 = user_config.total_assets(); assert_eq!(is_borrowing_any_before, false); assert_eq!(is_borrowing_token_1_before, false); assert_eq!(is_borrowing_token_2_before, false); + assert_eq!(user_total_assets_before, 1); assert_eq!(is_borrowing_any_after_borrow, true); assert_eq!(is_borrowing_token_1_after_borrow, true); assert_eq!(is_borrowing_token_2_after_borrow, true); + assert_eq!(user_total_assets_after_borrow, 3); assert_eq!(is_borrowing_any_after_repay_1, true); assert_eq!(is_borrowing_token_1_after_repay_1, false); assert_eq!(is_borrowing_token_2_after_repay_1, true); + assert_eq!(user_total_assets_after_repay_1, 2); assert_eq!(is_borrowing_any_after_repay_2, false); assert_eq!(is_borrowing_token_1_after_repay_2, false); assert_eq!(is_borrowing_token_2_after_repay_2, false); + assert_eq!(user_total_assets_after_repay_2, 1); } #[test] @@ -214,14 +238,14 @@ fn should_affect_coeffs() { let (_, borrower, debt_config) = fill_pool(&env, &sut, false); let token_address = debt_config.token.address.clone(); - env.ledger().with_mut(|li| li.timestamp = 2 * DAY); + set_time(&env, &sut, 2 * DAY, false); let collat_coeff_prev = sut.pool.collat_coeff(&token_address); let debt_coeff_prev = sut.pool.debt_coeff(&token_address); sut.pool.borrow(&borrower, &token_address, &20_000_000); - env.ledger().with_mut(|li| li.timestamp = 3 * DAY); + set_time(&env, &sut, 3 * DAY, false); let collat_coeff = sut.pool.collat_coeff(&token_address); let debt_coeff = sut.pool.debt_coeff(&token_address); @@ -271,39 +295,39 @@ fn should_change_balances_when_borrow_and_repay() { let sut = init_pool(&env, false); let (_, borrower, debt_config) = fill_pool(&env, &sut, false); let token_address = debt_config.token.address.clone(); - let treasury = sut.pool.treasury(); + // let treasury = sut.pool.treasury(); - env.ledger().with_mut(|li| li.timestamp = 2 * DAY); + set_time(&env, &sut, 2 * DAY, true); - let treasury_before = debt_config.token.balance(&treasury); + let treasury_before = sut.pool.protocol_fee(&debt_config.token.address); let debt_balance_before = debt_config.debt_token().balance(&borrower); let debt_total_before = debt_config.debt_token().total_supply(); let borrower_balance_before = debt_config.token.balance(&borrower); let underlying_supply_before = sut .pool - .stoken_underlying_balance(&debt_config.s_token().address); + .token_balance(&debt_config.token.address, &debt_config.s_token().address); sut.pool.borrow(&borrower, &token_address, &20_000_000); - let treasury_after_borrow = debt_config.token.balance(&treasury); + let treasury_after_borrow = sut.pool.protocol_fee(&debt_config.token.address); let debt_balance_after_borrow = debt_config.debt_token().balance(&borrower); let debt_total_after_borrow = debt_config.debt_token().total_supply(); let borrower_balance_after_borrow = debt_config.token.balance(&borrower); let underlying_supply_after_borrow = sut .pool - .stoken_underlying_balance(&debt_config.s_token().address); + .token_balance(&debt_config.token.address, &debt_config.s_token().address); - env.ledger().with_mut(|li| li.timestamp = 30 * DAY); + set_time(&env, &sut, 30 * DAY, false); sut.pool.repay(&borrower, &token_address, &i128::MAX); - let treasury_after_repay = debt_config.token.balance(&treasury); + let treasury_after_repay = sut.pool.protocol_fee(&debt_config.token.address); let debt_balance_after_repay = debt_config.debt_token().balance(&borrower); let debt_total_after_repay = debt_config.debt_token().total_supply(); let borrower_balance_after_repay = debt_config.token.balance(&borrower); let underlying_supply_after_repay = sut .pool - .stoken_underlying_balance(&debt_config.s_token().address); + .token_balance(&debt_config.token.address, &debt_config.s_token().address); assert_eq!(treasury_before, 0); assert_eq!(debt_balance_before, 0); @@ -317,10 +341,10 @@ fn should_change_balances_when_borrow_and_repay() { assert_eq!(borrower_balance_after_borrow, 1_020_000_000); assert_eq!(underlying_supply_after_borrow, 80_000_000); - assert_eq!(treasury_after_repay, 37_156); + assert_eq!(treasury_after_repay, 37_158); assert_eq!(debt_balance_after_repay, 0); assert_eq!(debt_total_after_repay, 0); - assert_eq!(borrower_balance_after_repay, 999_954_789); + assert_eq!(borrower_balance_after_repay, 999_954_787); assert_eq!(underlying_supply_after_repay, 100_008_055); } @@ -363,3 +387,120 @@ fn should_fail_when_borrow_rwa() { sut.pool.borrow(&borrower, &rwa_address, &10_000_000); } + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #4)")] +fn rwa_fail_when_exceed_assets_limit() { + let env = Env::default(); + env.mock_all_auths(); + let sut = init_pool(&env, false); + let (_, borrower, _) = fill_pool(&env, &sut, true); + + sut.pool.set_pool_configuration(&PoolConfig { + base_asset_address: sut.reserves[0].token.address.clone(), + base_asset_decimals: sut.reserves[0].token.decimals(), + flash_loan_fee: 5, + initial_health: 0, + timestamp_window: 20, + grace_period: 1, + user_assets_limit: 2, + min_collat_amount: 0, + min_debt_amount: 0, + liquidation_protocol_fee: 0, + ir_alpha: 143, + ir_initial_rate: 200, + ir_max_rate: 50_000, + ir_scaling_coeff: 9_000, + }); + + sut.pool + .borrow(&borrower, &sut.reserves[2].token.address, &1_000); +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #3)")] +fn should_fail_when_collat_lt_min_position_amount() { + let env = Env::default(); + env.mock_all_auths(); + + let sut = init_pool(&env, false); + + sut.pool.set_pool_configuration(&PoolConfig { + base_asset_address: sut.reserves[0].token.address.clone(), + base_asset_decimals: sut.reserves[0].token.decimals(), + flash_loan_fee: 5, + initial_health: 2_500, + timestamp_window: 20, + grace_period: 1, + user_assets_limit: 4, + min_collat_amount: 0, + min_debt_amount: 60_000_000, + liquidation_protocol_fee: 0, + ir_alpha: 143, + ir_initial_rate: 200, + ir_max_rate: 50_000, + ir_scaling_coeff: 9_000, + }); + + let lender = Address::generate(&env); + let borrower = Address::generate(&env); + sut.reserves[0].token_admin.mint(&lender, &1_000_000_000); + sut.reserves[1] + .token_admin + .mint(&borrower, &100_000_000_000); + + sut.pool + .deposit(&lender, &sut.reserves[0].token.address, &500_000_000); + sut.pool + .deposit(&borrower, &sut.reserves[1].token.address, &20_000_000_000); + + sut.pool + .borrow(&borrower, &sut.reserves[0].token.address, &50_000_000); +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #5)")] +fn should_fail_in_grace_period() { + let env = Env::default(); + env.mock_all_auths(); + + let sut = init_pool(&env, false); + let (_, borrower, debt_reserve) = fill_pool(&env, &sut, true); + sut.pool.borrow(&borrower, &debt_reserve.token.address, &1); + + sut.pool.set_pause(&true); + sut.pool.set_pause(&false); + sut.pool.borrow(&borrower, &debt_reserve.token.address, &1); +} + +#[test] +fn should_not_fail_after_grace_period() { + let env = Env::default(); + env.mock_all_auths(); + + let sut = init_pool(&env, false); + let (_, borrower, debt_reserve) = fill_pool(&env, &sut, true); + let pause_info = sut.pool.pause_info(); + let start = env.ledger().timestamp(); + let gap = 500; + + let debt_token_before = debt_reserve.debt_token().balance(&borrower); + sut.pool.borrow(&borrower, &debt_reserve.token.address, &1); + let debt_token_after = debt_reserve.debt_token().balance(&borrower); + assert!(debt_token_after > debt_token_before); + + sut.pool.set_pause(&true); + set_time(&env, &sut, start + gap, false); + sut.pool.set_pause(&false); + set_time( + &env, + &sut, + start + gap + pause_info.grace_period_secs, + false, + ); + + let debt_token_before = debt_reserve.debt_token().balance(&borrower); + sut.pool.borrow(&borrower, &debt_reserve.token.address, &1); + let debt_token_after = debt_reserve.debt_token().balance(&borrower); + assert!(debt_token_after > debt_token_before); +} diff --git a/contracts/pool/src/tests/budget.rs b/contracts/pool/src/tests/budget.rs index 1cbae2da..e3c9ab68 100644 --- a/contracts/pool/src/tests/budget.rs +++ b/contracts/pool/src/tests/budget.rs @@ -3,26 +3,23 @@ extern crate std; use pool_interface::types::collateral_params_input::CollateralParamsInput; use pool_interface::types::flash_loan_asset::FlashLoanAsset; -use pool_interface::types::ir_params::IRParams; use pool_interface::types::oracle_asset::OracleAsset; +use pool_interface::types::pool_config::PoolConfig; use pool_interface::types::price_feed::PriceFeed; use pool_interface::types::price_feed_config_input::PriceFeedConfigInput; use pool_interface::types::reserve_type::ReserveType; use pool_interface::types::timestamp_precision::TimestampPrecision; -use pool_interface::LendingPoolClient; use price_feed_interface::types::asset::Asset; use price_feed_interface::types::price_data::PriceData; use price_feed_interface::PriceFeedClient; -use soroban_sdk::testutils::{Address as _, Ledger}; +use soroban_sdk::testutils::Address as _; use soroban_sdk::{vec, Address, Bytes, Env, IntoVal, Symbol, Val, Vec}; use std::fs::OpenOptions; use std::io::prelude::*; -use crate::LendingPool; - use super::sut::{ create_pool_contract, create_price_feed_contract, create_s_token_contract, - create_token_contract, fill_pool, fill_pool_four, init_pool, DAY, + create_token_contract, fill_pool, fill_pool_four, init_pool, set_time, DAY, }; use super::upgrade::{debt_token_v2, pool_v2, s_token_v2}; @@ -165,7 +162,7 @@ fn init_reserve() { let (underlying_token, _) = create_token_contract(&env, &token_admin); let (debt_token, _) = create_token_contract(&env, &token_admin); - let pool = create_pool_contract(&env, &admin, false); + let pool = create_pool_contract(&env, &admin, false, &underlying_token.address); let s_token = create_s_token_contract(&env, &pool.address, &underlying_token.address); assert!(pool.get_reserve(&underlying_token.address).is_none()); @@ -180,58 +177,6 @@ fn init_reserve() { }); } -#[test] -fn ir_params() { - let env = Env::default(); - env.mock_all_auths(); - - let sut = init_pool(&env, true); - let (_, _, _) = fill_pool_four(&env, &sut); - - measure_budget(&env, function_name!(), || { - sut.pool.ir_params(); - }); -} - -#[test] -fn liquidate_receive_stoken_when_borrower_has_two_debts() { - let env = Env::default(); - env.mock_all_auths(); - - let sut = init_pool(&env, true); - let (_, _, borrower) = fill_pool_four(&env, &sut); - - env.ledger().with_mut(|li| li.timestamp = 4 * DAY); - - let liquidator = Address::generate(&env); - - for reserve in &sut.reserves { - reserve.token_admin.mint(&liquidator, &100_000_000_000); - } - - sut.pool - .deposit(&liquidator, &sut.reserves[0].token.address, &100_000_000); - sut.pool - .borrow(&liquidator, &sut.reserves[1].token.address, &1_000_000_000); - - env.ledger().with_mut(|l| l.timestamp = 5 * DAY); - - sut.price_feed.init( - &Asset::Stellar(sut.reserves[0].token.address.clone()), - &vec![ - &env, - PriceData { - price: 110_000_000_000_000, - timestamp: 0, - }, - ], - ); - - measure_budget(&env, function_name!(), || { - sut.pool.liquidate(&liquidator, &borrower, &true); - }); -} - #[test] fn liquidate_receive_underlying_when_borrower_has_one_debt() { let env = Env::default(); @@ -239,12 +184,27 @@ fn liquidate_receive_underlying_when_borrower_has_one_debt() { let sut = init_pool(&env, true); let (_, borrower, _) = fill_pool_four(&env, &sut); - sut.pool.set_initial_health(&100); + sut.pool.set_pool_configuration(&PoolConfig { + base_asset_address: sut.reserves[0].token.address.clone(), + base_asset_decimals: sut.reserves[0].token.decimals(), + flash_loan_fee: 5, + initial_health: 100, + timestamp_window: 20, + grace_period: 1, + user_assets_limit: 4, + min_collat_amount: 0, + min_debt_amount: 0, + liquidation_protocol_fee: 0, + ir_alpha: 143, + ir_initial_rate: 200, + ir_max_rate: 50_000, + ir_scaling_coeff: 9_000, + }); sut.pool .borrow(&borrower, &sut.reserves[2].token.address, &4_990_400_000); - env.ledger().with_mut(|li| li.timestamp = 4 * DAY); + set_time(&env, &sut, 4 * DAY, false); let liquidator = Address::generate(&env); @@ -263,7 +223,7 @@ fn liquidate_receive_underlying_when_borrower_has_one_debt() { sut.pool .borrow(&liquidator, &sut.reserves[1].token.address, &1_000_000_000); - env.ledger().with_mut(|l| l.timestamp = 5 * DAY); + set_time(&env, &sut, 5 * DAY, false); sut.price_feed.init( &Asset::Stellar(sut.reserves[2].token.address.clone()), @@ -271,13 +231,13 @@ fn liquidate_receive_underlying_when_borrower_has_one_debt() { &env, PriceData { price: 12_000_000_000_000_000, - timestamp: 0, + timestamp: 5 * DAY, }, ], ); measure_budget(&env, function_name!(), || { - sut.pool.liquidate(&liquidator, &borrower, &false); + sut.pool.liquidate(&liquidator, &borrower); }); } @@ -289,7 +249,7 @@ fn liquidate_receive_underlying_when_borrower_has_two_debts() { let sut = init_pool(&env, true); let (_, _, borrower) = fill_pool_four(&env, &sut); - env.ledger().with_mut(|li| li.timestamp = 4 * DAY); + set_time(&env, &sut, 4 * DAY, false); let liquidator = Address::generate(&env); @@ -308,7 +268,7 @@ fn liquidate_receive_underlying_when_borrower_has_two_debts() { sut.pool .borrow(&liquidator, &sut.reserves[1].token.address, &1_000_000_000); - env.ledger().with_mut(|l| l.timestamp = 5 * DAY); + set_time(&env, &sut, 5 * DAY, false); sut.price_feed.init( &Asset::Stellar(sut.reserves[0].token.address.clone()), @@ -316,25 +276,25 @@ fn liquidate_receive_underlying_when_borrower_has_two_debts() { &env, PriceData { price: 100_100_000_000_000, - timestamp: 0, + timestamp: 5 * DAY, }, ], ); measure_budget(&env, function_name!(), || { - sut.pool.liquidate(&liquidator, &borrower, &false); + sut.pool.liquidate(&liquidator, &borrower); }); } #[test] -fn paused() { +fn pause_info() { let env = Env::default(); env.mock_all_auths(); let sut = init_pool(&env, true); measure_budget(&env, function_name!(), || { - sut.pool.paused(); + sut.pool.pause_info(); }); } @@ -395,38 +355,6 @@ fn set_as_collateral() { }); } -#[test] -fn set_base_asset() { - let env = Env::default(); - env.mock_all_auths(); - - let asset = Address::generate(&env); - let sut = init_pool(&env, true); - - measure_budget(&env, function_name!(), || { - sut.pool.set_base_asset(&asset, &7); - }); -} - -#[test] -fn set_ir_params() { - let env = Env::default(); - env.mock_all_auths(); - - let sut = init_pool(&env, true); - - let ir_params_input = IRParams { - alpha: 144, - initial_rate: 201, - max_rate: 50_001, - scaling_coeff: 9_001, - }; - - measure_budget(&env, function_name!(), || { - sut.pool.set_ir_params(&ir_params_input); - }); -} - #[test] fn set_pause() { let env = Env::default(); @@ -449,7 +377,7 @@ fn set_price_feed() { let asset_2 = Address::generate(&env); let asset_3 = Address::generate(&env); - let pool = create_pool_contract(&env, &admin, false); + let pool = create_pool_contract(&env, &admin, false, &asset_1); let price_feed: PriceFeedClient<'_> = create_price_feed_contract(&env); let feed_inputs = Vec::from_array( @@ -458,6 +386,8 @@ fn set_price_feed() { PriceFeedConfigInput { asset: asset_1.clone(), asset_decimals: 7, + min_sanity_price_in_base: 5_000_000, + max_sanity_price_in_base: 50_000_000_000, feeds: vec![ &env, PriceFeed { @@ -465,6 +395,7 @@ fn set_price_feed() { feed_asset: OracleAsset::Stellar(asset_1), feed_decimals: 14, twap_records: 10, + min_timestamp_delta: 100, timestamp_precision: TimestampPrecision::Sec, }, ], @@ -472,6 +403,8 @@ fn set_price_feed() { PriceFeedConfigInput { asset: asset_2.clone(), asset_decimals: 9, + min_sanity_price_in_base: 5_000_000, + max_sanity_price_in_base: 50_000_000_000, feeds: vec![ &env, PriceFeed { @@ -479,6 +412,7 @@ fn set_price_feed() { feed_asset: OracleAsset::Stellar(asset_2), feed_decimals: 16, twap_records: 10, + min_timestamp_delta: 100, timestamp_precision: TimestampPrecision::Sec, }, ], @@ -486,6 +420,8 @@ fn set_price_feed() { PriceFeedConfigInput { asset: asset_3.clone(), asset_decimals: 9, + min_sanity_price_in_base: 5_000_000, + max_sanity_price_in_base: 50_000_000_000, feeds: vec![ &env, PriceFeed { @@ -493,6 +429,7 @@ fn set_price_feed() { feed_asset: OracleAsset::Stellar(asset_3), feed_decimals: 16, twap_records: 10, + min_timestamp_delta: 100, timestamp_precision: TimestampPrecision::Sec, }, ], @@ -531,34 +468,10 @@ fn stoken_underlying_balance() { .deposit(&lender, &sut.reserves[0].token.address, &10_000_000); measure_budget(&env, function_name!(), || { - sut.pool - .stoken_underlying_balance(&sut.reserves[0].s_token().address); - }); -} - -#[test] -fn treasury() { - let env = Env::default(); - env.mock_all_auths(); - - let pool = LendingPoolClient::new(&env, &env.register_contract(None, LendingPool)); - let flash_loan_fee = 5; - - pool.initialize( - &Address::generate(&env), - &Address::generate(&env), - &flash_loan_fee, - &2_500, - &IRParams { - alpha: 143, - initial_rate: 200, - max_rate: 50_000, - scaling_coeff: 9_000, - }, - ); - - measure_budget(&env, function_name!(), || { - pool.treasury(); + sut.pool.token_balance( + &sut.reserves[0].token.address, + &sut.reserves[0].s_token().address, + ); }); } @@ -609,26 +522,29 @@ fn withdraw_partial() { } #[test] -fn flash_loan_fee() { - let env = Env::default(); - env.mock_all_auths(); - - let sut = init_pool(&env, true); - - measure_budget(&env, function_name!(), || { - sut.pool.flash_loan_fee(); - }); -} - -#[test] -fn set_flash_loan_fee() { +fn set_pool_configuration() { let env = Env::default(); env.mock_all_auths(); let sut = init_pool(&env, true); measure_budget(&env, function_name!(), || { - sut.pool.set_flash_loan_fee(&15); + sut.pool.set_pool_configuration(&PoolConfig { + base_asset_address: sut.reserves[0].token.address.clone(), + base_asset_decimals: sut.reserves[0].token.decimals(), + flash_loan_fee: 5, + initial_health: 2_500, + timestamp_window: 20, + grace_period: 1, + user_assets_limit: 4, + min_collat_amount: 0, + min_debt_amount: 0, + liquidation_protocol_fee: 0, + ir_alpha: 143, + ir_initial_rate: 200, + ir_max_rate: 50_000, + ir_scaling_coeff: 9_000, + }); }); } @@ -746,7 +662,7 @@ fn upgrade_s_token() { let s_token_v2_wasm = env.deployer().upload_contract_wasm(s_token_v2::WASM); measure_budget(&env, function_name!(), || { - sut.pool.upgrade_s_token(&asset, &s_token_v2_wasm); + sut.pool.upgrade_token(&asset, &true, &s_token_v2_wasm); }); } @@ -760,7 +676,7 @@ fn upgrade_debt_token() { let asset = sut.reserves[0].token.address.clone(); measure_budget(&env, function_name!(), || { - sut.pool.upgrade_debt_token(&asset, &debt_token_v2_wasm); + sut.pool.upgrade_token(&asset, &false, &debt_token_v2_wasm); }); } diff --git a/contracts/pool/src/tests/collat_coeff.rs b/contracts/pool/src/tests/collat_coeff.rs index 42e1dae0..ab391606 100644 --- a/contracts/pool/src/tests/collat_coeff.rs +++ b/contracts/pool/src/tests/collat_coeff.rs @@ -2,8 +2,9 @@ use crate::tests::sut::{fill_pool_three, init_pool, DAY}; use crate::*; use price_feed_interface::types::asset::Asset; use price_feed_interface::types::price_data::PriceData; -use soroban_sdk::testutils::{Address as _, Ledger}; +use soroban_sdk::testutils::Address as _; use soroban_sdk::vec; +use tests::sut::set_time; #[test] fn should_update_when_deposit_borrow_withdraw_liquidate() { @@ -11,7 +12,22 @@ fn should_update_when_deposit_borrow_withdraw_liquidate() { env.mock_all_auths(); let sut = init_pool(&env, false); - sut.pool.set_initial_health(&2_500); + sut.pool.set_pool_configuration(&PoolConfig { + base_asset_address: sut.reserves[0].token.address.clone(), + base_asset_decimals: sut.reserves[0].token.decimals(), + flash_loan_fee: 5, + initial_health: 2_500, + timestamp_window: 20, + grace_period: 1, + user_assets_limit: 4, + min_collat_amount: 0, + min_debt_amount: 0, + liquidation_protocol_fee: 0, + ir_alpha: 143, + ir_initial_rate: 200, + ir_max_rate: 50_000, + ir_scaling_coeff: 9_000, + }); let debt_token = sut.reserves[1].token.address.clone(); let deposit_token = sut.reserves[0].token.address.clone(); @@ -26,7 +42,7 @@ fn should_update_when_deposit_borrow_withdraw_liquidate() { sut.reserves[i].token_admin.mint(&borrower, &amount); } - env.ledger().with_mut(|l| l.timestamp = DAY); + set_time(&env, &sut, DAY, false); for i in 0..3 { let amount = (i == 0).then(|| 1_000_000).unwrap_or(100_000_000); @@ -38,19 +54,19 @@ fn should_update_when_deposit_borrow_withdraw_liquidate() { sut.pool.deposit(&borrower, &deposit_token, &1_000_000); sut.pool.borrow(&borrower, &debt_token, &40_000_000); - env.ledger().with_mut(|l| l.timestamp = 2 * DAY); + set_time(&env, &sut, 2 * DAY, false); let collat_coeff_initial = sut.pool.collat_coeff(&debt_token); sut.pool .withdraw(&borrower, &deposit_token, &100_000, &lender); - env.ledger().with_mut(|l| l.timestamp = 3 * DAY); + set_time(&env, &sut, 3 * DAY, false); let collat_coeff_after_withdraw = sut.pool.collat_coeff(&debt_token); sut.pool.borrow(&borrower, &debt_token, &400_000); - env.ledger().with_mut(|l| l.timestamp = 4 * DAY); + set_time(&env, &sut, 4 * DAY, false); let collat_coeff_after_borrow = sut.pool.collat_coeff(&debt_token); sut.price_feed.init( @@ -59,17 +75,17 @@ fn should_update_when_deposit_borrow_withdraw_liquidate() { &env, PriceData { price: 14_000_000_000_000_000, - timestamp: 0, + timestamp: 5 * DAY, }, ], ); - env.ledger().with_mut(|l| l.timestamp = 5 * DAY); + set_time(&env, &sut, 5 * DAY, false); let collat_coeff_after_price_change = sut.pool.collat_coeff(&debt_token); - sut.pool.liquidate(&lender, &borrower, &false); + sut.pool.liquidate(&lender, &borrower); - env.ledger().with_mut(|l| l.timestamp = 6 * DAY); + set_time(&env, &sut, 6 * DAY, false); let collat_coeff_after_liquidate = sut.pool.collat_coeff(&debt_token); assert_eq!(collat_coeff_initial, 1_000_000_010); @@ -90,15 +106,16 @@ fn should_change_over_time() { let collat_coeff_1 = sut.pool.collat_coeff(&debt_token); - env.ledger().with_mut(|l| l.timestamp = 4 * DAY); + set_time(&env, &sut, 4 * DAY, false); + let collat_coeff_2 = sut.pool.collat_coeff(&debt_token); - env.ledger().with_mut(|l| l.timestamp = 5 * DAY); + set_time(&env, &sut, 5 * DAY, false); let collat_coeff_3 = sut.pool.collat_coeff(&debt_token); - assert_eq!(collat_coeff_1, 1_000_328_900); - assert_eq!(collat_coeff_2, 1_000_438_560); - assert_eq!(collat_coeff_3, 1_000_548_220); + assert_eq!(collat_coeff_1, 1_000_328_880); + assert_eq!(collat_coeff_2, 1_000_438_540); + assert_eq!(collat_coeff_3, 1_000_548_200); } #[test] @@ -107,30 +124,45 @@ fn should_change_when_elapsed_time_gte_window() { env.mock_all_auths(); let sut = init_pool(&env, false); - sut.pool.set_reserve_timestamp_window(&20); + sut.pool.set_pool_configuration(&PoolConfig { + base_asset_address: sut.reserves[0].token.address.clone(), + base_asset_decimals: sut.reserves[0].token.decimals(), + flash_loan_fee: 5, + initial_health: 0, + timestamp_window: 20, + grace_period: 1, + user_assets_limit: 4, + min_collat_amount: 0, + min_debt_amount: 0, + liquidation_protocol_fee: 0, + ir_alpha: 143, + ir_initial_rate: 200, + ir_max_rate: 50_000, + ir_scaling_coeff: 9_000, + }); let (_, _, _, debt_config) = fill_pool_three(&env, &sut); let debt_token = debt_config.token.address.clone(); let collat_coeff_1 = sut.pool.collat_coeff(&debt_token); - env.ledger().with_mut(|li| li.timestamp = 3 * DAY + 19); + set_time(&env, &sut, 3 * DAY + 19, false); let collat_coeff_2 = sut.pool.collat_coeff(&debt_token); - env.ledger().with_mut(|li| li.timestamp = 3 * DAY + 26); + set_time(&env, &sut, 3 * DAY + 26, false); let collat_coeff_3 = sut.pool.collat_coeff(&debt_token); - env.ledger().with_mut(|li| li.timestamp = 3 * DAY + 51); + set_time(&env, &sut, 3 * DAY + 51, false); let collat_coeff_4 = sut.pool.collat_coeff(&debt_token); - env.ledger().with_mut(|li| li.timestamp = 3 * DAY + 55); + set_time(&env, &sut, 3 * DAY + 55, false); let collat_coeff_5 = sut.pool.collat_coeff(&debt_token); - env.ledger().with_mut(|li| li.timestamp = 3 * DAY + 61); + set_time(&env, &sut, 3 * DAY + 61, false); let collat_coeff_6 = sut.pool.collat_coeff(&debt_token); diff --git a/contracts/pool/src/tests/configure_as_collateral.rs b/contracts/pool/src/tests/configure_as_collateral.rs index 3ec6e500..ca597371 100644 --- a/contracts/pool/src/tests/configure_as_collateral.rs +++ b/contracts/pool/src/tests/configure_as_collateral.rs @@ -82,7 +82,7 @@ fn should_fail_when_invalid_util_cap() { } #[test] -#[should_panic(expected = "HostError: Error(Contract, #404)")] +#[should_panic(expected = "HostError: Error(Contract, #3)")] fn should_fail_when_invalid_liquidity_cap() { let env = Env::default(); env.mock_all_auths(); diff --git a/contracts/pool/src/tests/debt_coeff.rs b/contracts/pool/src/tests/debt_coeff.rs index 6f378a5b..e65ea974 100644 --- a/contracts/pool/src/tests/debt_coeff.rs +++ b/contracts/pool/src/tests/debt_coeff.rs @@ -4,8 +4,9 @@ use crate::*; use common::FixedI128; use price_feed_interface::types::asset::Asset; use price_feed_interface::types::price_data::PriceData; -use soroban_sdk::testutils::{Address as _, Ledger}; +use soroban_sdk::testutils::Address as _; use soroban_sdk::vec; +use tests::sut::set_time; #[test] fn should_update_when_deposit_borrow_withdraw_liquidate() { @@ -13,7 +14,22 @@ fn should_update_when_deposit_borrow_withdraw_liquidate() { env.mock_all_auths(); let sut = init_pool(&env, false); - sut.pool.set_initial_health(&2_500); + sut.pool.set_pool_configuration(&PoolConfig { + base_asset_address: sut.reserves[0].token.address.clone(), + base_asset_decimals: sut.reserves[0].token.decimals(), + flash_loan_fee: 5, + initial_health: 2_500, + timestamp_window: 20, + grace_period: 1, + user_assets_limit: 4, + min_collat_amount: 0, + min_debt_amount: 0, + liquidation_protocol_fee: 0, + ir_alpha: 143, + ir_initial_rate: 200, + ir_max_rate: 50_000, + ir_scaling_coeff: 9_000, + }); let debt_token = sut.reserves[1].token.address.clone(); let deposit_token = sut.reserves[0].token.address.clone(); @@ -28,7 +44,7 @@ fn should_update_when_deposit_borrow_withdraw_liquidate() { sut.reserves[i].token_admin.mint(&borrower, &amount); } - env.ledger().with_mut(|l| l.timestamp = DAY); + set_time(&env, &sut, DAY, false); for i in 0..3 { let amount = (i == 0).then(|| 1_000_000).unwrap_or(100_000_000); @@ -40,19 +56,19 @@ fn should_update_when_deposit_borrow_withdraw_liquidate() { sut.pool.deposit(&borrower, &deposit_token, &1_000_000); sut.pool.borrow(&borrower, &debt_token, &40_000_000); - env.ledger().with_mut(|l| l.timestamp = 2 * DAY); + set_time(&env, &sut, 2 * DAY, false); let debt_coeff_initial = sut.pool.debt_coeff(&debt_token); sut.pool .withdraw(&borrower, &deposit_token, &100_000, &lender); - env.ledger().with_mut(|l| l.timestamp = 3 * DAY); + set_time(&env, &sut, 3 * DAY, false); let debt_coeff_after_withdraw = sut.pool.debt_coeff(&debt_token); sut.pool.borrow(&borrower, &debt_token, &400_000); - env.ledger().with_mut(|l| l.timestamp = 4 * DAY); + set_time(&env, &sut, 4 * DAY, false); let debt_coeff_after_borrow = sut.pool.debt_coeff(&debt_token); sut.price_feed.init( @@ -61,17 +77,17 @@ fn should_update_when_deposit_borrow_withdraw_liquidate() { &env, PriceData { price: 14_000_000_000_000_000, - timestamp: 0, + timestamp: 5 * DAY, }, ], ); - env.ledger().with_mut(|l| l.timestamp = 5 * DAY); + set_time(&env, &sut, 5 * DAY, false); let debt_coeff_after_price_change = sut.pool.debt_coeff(&debt_token); - sut.pool.liquidate(&lender, &borrower, &false); + sut.pool.liquidate(&lender, &borrower); - env.ledger().with_mut(|l| l.timestamp = 6 * DAY); + set_time(&env, &sut, 6 * DAY, false); let debt_coeff_after_liquidate = sut.pool.debt_coeff(&debt_token); assert_eq!(debt_coeff_initial, 1_000_000_000); @@ -92,15 +108,15 @@ fn should_change_over_time() { let debt_coeff_1 = sut.pool.debt_coeff(&debt_token); - env.ledger().with_mut(|l| l.timestamp = 4 * DAY); + set_time(&env, &sut, 4 * DAY, false); let debt_coeff_2 = sut.pool.debt_coeff(&debt_token); - env.ledger().with_mut(|l| l.timestamp = 5 * DAY); + set_time(&env, &sut, 5 * DAY, false); let debt_coeff_3 = sut.pool.debt_coeff(&debt_token); - assert_eq!(debt_coeff_1, 1_000_609_079); - assert_eq!(debt_coeff_2, 1_000_812_160); - assert_eq!(debt_coeff_3, 1_001_015_242); + assert_eq!(debt_coeff_1, 1_000_609_032); + assert_eq!(debt_coeff_2, 1_000_812_113); + assert_eq!(debt_coeff_3, 1_001_015_195); } #[test] @@ -112,7 +128,7 @@ fn should_be_correctly_calculated() { let (_, borrower, debt_config) = fill_pool(&env, &sut, true); - env.ledger().with_mut(|l| l.timestamp = 2 * DAY); + set_time(&env, &sut, 2 * DAY, false); sut.pool .borrow(&borrower, &debt_config.token.address, &50_000); @@ -134,7 +150,7 @@ fn should_be_correctly_calculated() { assert_eq!(collat_coeff, expected_collat_coeff); // shift time to 8 days - env.ledger().with_mut(|l| l.timestamp = 10 * DAY); + set_time(&env, &sut, 10 * DAY, false); let elapsed_time = 8 * DAY; let collat_ar = calc_next_accrued_rate( @@ -160,30 +176,45 @@ fn should_change_when_elapsed_time_gte_window() { env.mock_all_auths(); let sut = init_pool(&env, false); - sut.pool.set_reserve_timestamp_window(&20); + sut.pool.set_pool_configuration(&PoolConfig { + base_asset_address: sut.reserves[0].token.address.clone(), + base_asset_decimals: sut.reserves[0].token.decimals(), + flash_loan_fee: 5, + initial_health: 0, + timestamp_window: 20, + grace_period: 1, + user_assets_limit: 4, + min_collat_amount: 0, + min_debt_amount: 0, + liquidation_protocol_fee: 0, + ir_alpha: 143, + ir_initial_rate: 200, + ir_max_rate: 50_000, + ir_scaling_coeff: 9_000, + }); let (_, _, _, debt_config) = fill_pool_three(&env, &sut); let debt_token = debt_config.token.address.clone(); let debt_coeff_1 = sut.pool.debt_coeff(&debt_token); - env.ledger().with_mut(|li| li.timestamp = 3 * DAY + 19); + set_time(&env, &sut, 3 * DAY + 19, false); let debt_coeff_2 = sut.pool.debt_coeff(&debt_token); - env.ledger().with_mut(|li| li.timestamp = 3 * DAY + 26); + set_time(&env, &sut, 3 * DAY + 26, false); let debt_coeff_3 = sut.pool.debt_coeff(&debt_token); - env.ledger().with_mut(|li| li.timestamp = 3 * DAY + 51); + set_time(&env, &sut, 3 * DAY + 51, false); let debt_coeff_4 = sut.pool.debt_coeff(&debt_token); - env.ledger().with_mut(|li| li.timestamp = 3 * DAY + 55); + set_time(&env, &sut, 3 * DAY + 55, false); let debt_coeff_5 = sut.pool.debt_coeff(&debt_token); - env.ledger().with_mut(|li| li.timestamp = 3 * DAY + 61); + set_time(&env, &sut, 3 * DAY + 61, false); let debt_coeff_6 = sut.pool.debt_coeff(&debt_token); diff --git a/contracts/pool/src/tests/deposit.rs b/contracts/pool/src/tests/deposit.rs index f3125cc3..15db3f4a 100644 --- a/contracts/pool/src/tests/deposit.rs +++ b/contracts/pool/src/tests/deposit.rs @@ -1,7 +1,8 @@ use crate::tests::sut::{fill_pool, init_pool, DAY}; use crate::*; -use soroban_sdk::testutils::{Address as _, AuthorizedFunction, Events, Ledger}; +use soroban_sdk::testutils::{Address as _, AuthorizedFunction, Events}; use soroban_sdk::{symbol_short, vec, IntoVal, Symbol}; +use tests::sut::set_time; #[test] fn should_require_authorized_caller() { @@ -26,7 +27,7 @@ fn should_require_authorized_caller() { } #[test] -#[should_panic(expected = "HostError: Error(Contract, #3)")] +#[should_panic(expected = "HostError: Error(Contract, #2)")] fn should_fail_when_pool_paused() { let env = Env::default(); env.mock_all_auths(); @@ -40,7 +41,7 @@ fn should_fail_when_pool_paused() { } #[test] -#[should_panic(expected = "HostError: Error(Contract, #304)")] +#[should_panic(expected = "HostError: Error(Contract, #302)")] fn should_fail_when_invalid_amount() { let env = Env::default(); env.mock_all_auths(); @@ -53,7 +54,7 @@ fn should_fail_when_invalid_amount() { } #[test] -#[should_panic(expected = "HostError: Error(Contract, #101)")] +#[should_panic(expected = "HostError: Error(Contract, #100)")] fn should_fail_when_reserve_deactivated() { let env = Env::default(); env.mock_all_auths(); @@ -67,7 +68,7 @@ fn should_fail_when_reserve_deactivated() { } #[test] -#[should_panic(expected = "HostError: Error(Contract, #312)")] +#[should_panic(expected = "HostError: Error(Contract, #4)")] fn should_fail_when_liquidity_cap_exceeded() { let env = Env::default(); env.mock_all_auths(); @@ -107,6 +108,7 @@ fn should_change_user_config() { user_config.is_using_as_collateral(&env, reserve.get_id()), true ); + assert_eq!(user_config.total_assets(), 1); } #[test] @@ -119,11 +121,13 @@ fn should_change_balances() { let token_address = sut.token().address.clone(); sut.token_admin().mint(&user, &10_000_000_000); - env.ledger().with_mut(|li| li.timestamp = 2 * DAY); + set_time(&env, &sut, 2 * DAY, false); sut.pool.deposit(&user, &token_address, &3_000_000_000); - let stoken_underlying_balance = sut.pool.stoken_underlying_balance(&sut.s_token().address); + let stoken_underlying_balance = sut + .pool + .token_balance(&sut.token().address, &sut.s_token().address); let user_balance = sut.token().balance(&user); let user_stoken_balance = sut.s_token().balance(&user); @@ -141,7 +145,7 @@ fn should_affect_coeffs() { let (lender, _, debt_config) = fill_pool(&env, &sut, true); let debt_token = &debt_config.token.address; - env.ledger().with_mut(|li| li.timestamp = 2 * DAY); + set_time(&env, &sut, 2 * DAY, false); let collat_coeff_prev = sut.pool.collat_coeff(&debt_token); let debt_coeff_prev = sut.pool.debt_coeff(&debt_token); @@ -149,7 +153,7 @@ fn should_affect_coeffs() { sut.pool .deposit(&lender, &sut.reserves[1].token.address, &100_000_000); - env.ledger().with_mut(|li| li.timestamp = 3 * DAY); + set_time(&env, &sut, 3 * DAY, false); let collat_coeff = sut.pool.collat_coeff(&debt_token); let debt_coeff = sut.pool.debt_coeff(&debt_token); @@ -279,7 +283,7 @@ fn rwa_should_not_affect_coeffs() { rwa_reserve_config.token_admin.mint(&lender, &1_000_000_000); let debt_token = &debt_config.token.address; - env.ledger().with_mut(|li| li.timestamp = 2 * DAY); + set_time(&env, &sut, 2 * DAY, false); let collat_coeff_prev = sut.pool.collat_coeff(&debt_token); let debt_coeff_prev = sut.pool.debt_coeff(&debt_token); @@ -317,3 +321,62 @@ fn rwa_should_affect_account_data() { assert!(borrower_position_prev.debt == borrower_position.debt); assert!(borrower_position_prev.npv < borrower_position.npv); } + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #4)")] +fn rwa_fail_when_exceed_assets_limit() { + let env = Env::default(); + env.mock_all_auths(); + let sut = init_pool(&env, false); + let (_, borrower, _) = fill_pool(&env, &sut, true); + + sut.pool.set_pool_configuration(&PoolConfig { + base_asset_address: sut.reserves[0].token.address.clone(), + base_asset_decimals: sut.reserves[0].token.decimals(), + flash_loan_fee: 5, + initial_health: 0, + timestamp_window: 20, + grace_period: 1, + user_assets_limit: 2, + min_collat_amount: 0, + min_debt_amount: 0, + liquidation_protocol_fee: 0, + ir_alpha: 143, + ir_initial_rate: 200, + ir_max_rate: 50_000, + ir_scaling_coeff: 9_000, + }); + + sut.pool + .deposit(&borrower, &sut.reserves[2].token.address, &1_000_000_000); +} + +#[test] +fn should_not_fail_in_grace_period() { + let env = Env::default(); + env.mock_all_auths(); + + let user = Address::generate(&env); + let sut = init_pool(&env, false); + let token_address = sut.token().address.clone(); + + sut.token_admin().mint(&user, &10_000_000_000); + set_time(&env, &sut, 2 * DAY, false); + + sut.pool.deposit(&user, &token_address, &3_000_000_000); + + sut.pool.set_pause(&true); + sut.pool.set_pause(&false); + + sut.pool.deposit(&user, &token_address, &3_000_000_000); + + let stoken_underlying_balance = sut + .pool + .token_balance(&sut.token().address, &sut.s_token().address); + let user_balance = sut.token().balance(&user); + let user_stoken_balance = sut.s_token().balance(&user); + + assert_eq!(stoken_underlying_balance, 6_000_000_000); + assert_eq!(user_balance, 4_000_000_000); + assert_eq!(user_stoken_balance, 6_000_000_000); +} diff --git a/contracts/pool/src/tests/enable_borrowing_on_reserve.rs b/contracts/pool/src/tests/enable_borrowing_on_reserve.rs index 996ba873..09354a5e 100644 --- a/contracts/pool/src/tests/enable_borrowing_on_reserve.rs +++ b/contracts/pool/src/tests/enable_borrowing_on_reserve.rs @@ -108,7 +108,7 @@ fn should_emit_events() { } #[test] -#[should_panic(expected = "HostError: Error(Contract, #110)")] +#[should_panic(expected = "HostError: Error(Contract, #105)")] fn should_fail_when_enable_rwa() { let env = Env::default(); env.mock_all_auths(); diff --git a/contracts/pool/src/tests/finalize_transfer.rs b/contracts/pool/src/tests/finalize_transfer.rs new file mode 100644 index 00000000..60d44fa8 --- /dev/null +++ b/contracts/pool/src/tests/finalize_transfer.rs @@ -0,0 +1,417 @@ +use crate::tests::sut::{fill_pool, init_pool, set_time}; +use pool_interface::types::pool_config::PoolConfig; +use soroban_sdk::Env; + +use super::sut::{create_s_token_contract, create_token_contract}; + +#[test] +fn finalize_transfer_should_change_in_pool_balance() { + let env = Env::default(); + env.mock_all_auths(); + let sut = init_pool(&env, false); + let (lender, borrower, _debt_token_reserve) = fill_pool(&env, &sut, true); + let token_client = &sut.reserves[0].token; + let s_token_client = sut.reserves[0].s_token(); + + let lender_balance_before = s_token_client.balance(&lender); + let borrower_balance_before = s_token_client.balance(&borrower); + let lender_in_pool_before = sut.pool.token_balance(&s_token_client.address, &lender); + let borrower_in_pool_before = sut.pool.token_balance(&s_token_client.address, &borrower); + let s_token_supply = s_token_client.total_supply(); + sut.pool.finalize_transfer( + &token_client.address, + &lender, + &borrower, + &1, + &lender_balance_before, + &borrower_balance_before, + &s_token_supply, + ); + + let lender_in_pool_after = sut.pool.token_balance(&s_token_client.address, &lender); + let borrower_in_pool_after = sut.pool.token_balance(&s_token_client.address, &borrower); + + assert_eq!(lender_in_pool_before - lender_in_pool_after, 1); + assert_eq!(borrower_in_pool_after - borrower_in_pool_before, 1); +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #2)")] +fn finalize_transfer_should_fail_if_paused() { + let env = Env::default(); + env.mock_all_auths(); + let sut = init_pool(&env, false); + let (lender, borrower, _debt_token_reserve) = fill_pool(&env, &sut, true); + let token_client = &sut.reserves[0].token; + let s_token_client = sut.reserves[0].s_token(); + sut.pool.set_pause(&true); + + let lender_balance_before = s_token_client.balance(&lender); + let borrower_balance_before = s_token_client.balance(&borrower); + let s_token_supply = s_token_client.total_supply(); + sut.pool.finalize_transfer( + &token_client.address, + &lender, + &borrower, + &1, + &lender_balance_before, + &borrower_balance_before, + &s_token_supply, + ); +} + +#[test] +#[should_panic(expected = "HostError: Error(Auth, InvalidAction)")] +fn should_fail_when_transfering_unknown_asset() { + let env = Env::default(); + env.mock_all_auths(); + let sut = init_pool(&env, false); + let (lender, borrower, _debt_token_reserve) = fill_pool(&env, &sut, true); + let token_client = &sut.reserves[0].token; + let unknown_s_token = create_s_token_contract(&env, &sut.pool.address, &token_client.address); + unknown_s_token.mint(&lender, &100); + unknown_s_token.transfer(&lender, &borrower, &1); +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #1)")] +fn should_fail_when_on_no_reserve() { + let env = Env::default(); + env.mock_all_auths(); + let sut = init_pool(&env, false); + let (lender, borrower, _debt_token_reserve) = fill_pool(&env, &sut, true); + // let token_client = &sut.reserves[0].token; + let s_token_client = sut.reserves[0].s_token(); + let (unknown_token, _) = create_token_contract(&env, &sut.token_admin); + + let s_token_supply = s_token_client.total_supply(); + let lender_balance_before = s_token_client.balance(&lender); + let borrower_balance_before = s_token_client.balance(&borrower); + + sut.pool.finalize_transfer( + &unknown_token.address, + &lender, + &borrower, + &1, + &lender_balance_before, + &borrower_balance_before, + &s_token_supply, + ); +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #100)")] +fn finalize_transfer_should_fail_if_reserve_is_not_active() { + let env = Env::default(); + env.mock_all_auths(); + let sut = init_pool(&env, false); + let (lender, borrower, _debt_token_reserve) = fill_pool(&env, &sut, true); + let token_client = &sut.reserves[0].token; + let s_token_client = sut.reserves[0].s_token(); + + sut.pool.set_reserve_status(&token_client.address, &false); + + let lender_balance_before = s_token_client.balance(&lender); + let borrower_balance_before = s_token_client.balance(&borrower); + let s_token_supply = s_token_client.total_supply(); + sut.pool.finalize_transfer( + &token_client.address, + &lender, + &borrower, + &1, + &lender_balance_before, + &borrower_balance_before, + &s_token_supply, + ); +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #105)")] +fn finalize_transfer_should_fail_if_calling_on_rwa() { + let env = Env::default(); + env.mock_all_auths(); + let sut = init_pool(&env, false); + let (lender, borrower, _debt_token_reserve) = fill_pool(&env, &sut, true); + let token_client = &sut.reserves[3].token; + let s_token_client = sut.reserves[0].s_token(); + + let lender_balance_before = s_token_client.balance(&lender); + let borrower_balance_before = s_token_client.balance(&borrower); + let s_token_supply = s_token_client.total_supply(); + sut.pool.finalize_transfer( + &token_client.address, + &lender, + &borrower, + &1, + &lender_balance_before, + &borrower_balance_before, + &s_token_supply, + ); +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #201)")] +fn finalize_transfer_should_fail_if_receiver_has_debt_in_same_asset() { + let env = Env::default(); + env.mock_all_auths(); + let sut = init_pool(&env, false); + let (lender, borrower, debt_token_reserve) = fill_pool(&env, &sut, true); + let s_token_client = sut.reserves[0].s_token(); + + let lender_balance_before = s_token_client.balance(&lender); + let borrower_balance_before = s_token_client.balance(&borrower); + let s_token_supply = s_token_client.total_supply(); + sut.pool.finalize_transfer( + &debt_token_reserve.token.address, + &lender, + &borrower, + &1, + &lender_balance_before, + &borrower_balance_before, + &s_token_supply, + ); +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #302)")] +fn finalize_transfer_should_fail_if_transfers_with_underflow() { + let env = Env::default(); + env.mock_all_auths(); + let sut = init_pool(&env, false); + let (lender, borrower, _debt_token_reserve) = fill_pool(&env, &sut, true); + let s_token_client = sut.reserves[0].s_token(); + let token_client = &sut.reserves[0].token; + + let lender_balance_before = s_token_client.balance(&lender); + let borrower_balance_before = s_token_client.balance(&borrower); + let s_token_supply = s_token_client.total_supply(); + sut.pool.finalize_transfer( + &token_client.address, + &lender, + &borrower, + &(i128::MAX), + &lender_balance_before, + &borrower_balance_before, + &s_token_supply, + ); +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #3)")] +fn finalize_transfer_should_fail_if_npv_fail_bellow_initial_health() { + let env = Env::default(); + env.mock_all_auths(); + let sut = init_pool(&env, false); + let (lender, borrower, _debt_token_reserve) = fill_pool(&env, &sut, true); + let s_token_client = sut.reserves[0].s_token(); + let token_client = &sut.reserves[0].token; + + let lender_balance_before = s_token_client.balance(&lender); + let borrower_balance_before = s_token_client.balance(&borrower); + let s_token_supply = s_token_client.total_supply(); + sut.pool.finalize_transfer( + &token_client.address, + &borrower, + &lender, + &(borrower_balance_before - 1), + &lender_balance_before, + &borrower_balance_before, + &s_token_supply, + ); +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #4)")] +fn rwa_fail_when_exceed_assets_limit() { + let env = Env::default(); + env.mock_all_auths(); + let sut = init_pool(&env, false); + sut.pool.set_pool_configuration(&PoolConfig { + base_asset_address: sut.reserves[0].token.address.clone(), + base_asset_decimals: sut.reserves[0].token.decimals(), + flash_loan_fee: 5, + initial_health: 0, + timestamp_window: 20, + grace_period: 1, + user_assets_limit: 2, + min_collat_amount: 0, + min_debt_amount: 0, + liquidation_protocol_fee: 0, + ir_alpha: 143, + ir_initial_rate: 200, + ir_max_rate: 50_000, + ir_scaling_coeff: 9_000, + }); + + let (lender, borrower, _debt_token_reserve) = fill_pool(&env, &sut, true); + let token_client = &sut.reserves[2].token; + let s_token_client = sut.reserves[2].s_token(); + + let lender_balance_before = s_token_client.balance(&lender); + let borrower_balance_before = s_token_client.balance(&borrower); + let s_token_supply = s_token_client.total_supply(); + sut.pool.finalize_transfer( + &token_client.address, + &lender, + &borrower, + &1, + &lender_balance_before, + &borrower_balance_before, + &s_token_supply, + ); +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #3)")] +fn should_fail_when_collat_lt_min_position_amount() { + let env = Env::default(); + env.mock_all_auths(); + let sut = init_pool(&env, false); + sut.pool.set_pool_configuration(&PoolConfig { + base_asset_address: sut.reserves[0].token.address.clone(), + base_asset_decimals: sut.reserves[0].token.decimals(), + flash_loan_fee: 5, + initial_health: 0, + timestamp_window: 20, + grace_period: 1, + user_assets_limit: 3, + min_collat_amount: 600_000, + min_debt_amount: 0, + liquidation_protocol_fee: 0, + ir_alpha: 143, + ir_initial_rate: 200, + ir_max_rate: 50_000, + ir_scaling_coeff: 9_000, + }); + + let (lender, borrower, _debt_token_reserve) = fill_pool(&env, &sut, true); + let token_client = &sut.reserves[0].token; + let s_token_client = sut.reserves[0].s_token(); + + let lender_balance_before = s_token_client.balance(&lender); + let borrower_balance_before = s_token_client.balance(&borrower); + let s_token_supply = s_token_client.total_supply(); + sut.pool.finalize_transfer( + &token_client.address, + &borrower, + &lender, + &500_000, + &borrower_balance_before, + &lender_balance_before, + &s_token_supply, + ); +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #5)")] +fn should_fail_in_grace_period() { + let env = Env::default(); + env.mock_all_auths(); + + let sut = init_pool(&env, false); + let (lender, borrower, _) = fill_pool(&env, &sut, false); + + let token_client = &sut.reserves[0].token; + let s_token_client = sut.reserves[0].s_token(); + + let lender_balance_before = s_token_client.balance(&lender); + let borrower_balance_before = s_token_client.balance(&borrower); + let lender_in_pool_before = sut.pool.token_balance(&s_token_client.address, &lender); + let borrower_in_pool_before = sut.pool.token_balance(&s_token_client.address, &borrower); + let s_token_supply = s_token_client.total_supply(); + sut.pool.finalize_transfer( + &token_client.address, + &lender, + &borrower, + &1, + &lender_balance_before, + &borrower_balance_before, + &s_token_supply, + ); + + let lender_in_pool_after = sut.pool.token_balance(&s_token_client.address, &lender); + let borrower_in_pool_after = sut.pool.token_balance(&s_token_client.address, &borrower); + + assert_eq!(lender_in_pool_before - lender_in_pool_after, 1); + assert_eq!(borrower_in_pool_after - borrower_in_pool_before, 1); + + sut.pool.set_pause(&true); + sut.pool.set_pause(&false); + sut.pool.finalize_transfer( + &token_client.address, + &lender, + &borrower, + &1, + &lender_balance_before, + &borrower_balance_before, + &s_token_supply, + ); +} + +#[test] +fn should_not_fail_after_grace_period() { + let env = Env::default(); + env.mock_all_auths(); + + let sut = init_pool(&env, false); + let (lender, borrower, _) = fill_pool(&env, &sut, false); + + let pause_info = sut.pool.pause_info(); + let start = env.ledger().timestamp(); + let gap = 500; + + let token_client = &sut.reserves[0].token; + let s_token_client = sut.reserves[0].s_token(); + + let lender_balance_before = s_token_client.balance(&lender); + let borrower_balance_before = s_token_client.balance(&borrower); + let lender_in_pool_before = sut.pool.token_balance(&s_token_client.address, &lender); + let borrower_in_pool_before = sut.pool.token_balance(&s_token_client.address, &borrower); + let s_token_supply = s_token_client.total_supply(); + sut.pool.finalize_transfer( + &token_client.address, + &lender, + &borrower, + &1, + &lender_balance_before, + &borrower_balance_before, + &s_token_supply, + ); + + let lender_in_pool_after = sut.pool.token_balance(&s_token_client.address, &lender); + let borrower_in_pool_after = sut.pool.token_balance(&s_token_client.address, &borrower); + + assert_eq!(lender_in_pool_before - lender_in_pool_after, 1); + assert_eq!(borrower_in_pool_after - borrower_in_pool_before, 1); + + sut.pool.set_pause(&true); + set_time(&env, &sut, start + gap, false); + sut.pool.set_pause(&false); + set_time( + &env, + &sut, + start + gap + pause_info.grace_period_secs, + false, + ); + + let lender_balance_before = lender_balance_before - 1; + let borrower_balance_before = borrower_balance_before + 1; + let lender_in_pool_before = sut.pool.token_balance(&s_token_client.address, &lender); + let borrower_in_pool_before = sut.pool.token_balance(&s_token_client.address, &borrower); + let s_token_supply = s_token_client.total_supply(); + sut.pool.finalize_transfer( + &token_client.address, + &lender, + &borrower, + &1, + &lender_balance_before, + &borrower_balance_before, + &s_token_supply, + ); + + let lender_in_pool_after = sut.pool.token_balance(&s_token_client.address, &lender); + let borrower_in_pool_after = sut.pool.token_balance(&s_token_client.address, &borrower); + + assert_eq!(lender_in_pool_before - lender_in_pool_after, 1); + assert_eq!(borrower_in_pool_after - borrower_in_pool_before, 1); +} diff --git a/contracts/pool/src/tests/flash_loan.rs b/contracts/pool/src/tests/flash_loan.rs index cb47eaeb..1574abc0 100644 --- a/contracts/pool/src/tests/flash_loan.rs +++ b/contracts/pool/src/tests/flash_loan.rs @@ -1,10 +1,12 @@ use crate::tests::sut::{fill_pool, init_pool}; +use common::FixedI128; use pool_interface::types::flash_loan_asset::FlashLoanAsset; +use pool_interface::types::pool_config::PoolConfig; use soroban_sdk::testutils::Events; use soroban_sdk::{vec, Bytes, Env, IntoVal, Symbol, Val, Vec}; #[test] -#[should_panic(expected = "HostError: Error(Contract, #313)")] +#[should_panic(expected = "HostError: Error(Contract, #307)")] fn should_fail_when_receiver_receive_returns_false() { let env = Env::default(); env.mock_all_auths(); @@ -70,9 +72,9 @@ fn should_require_borrower_to_pay_fee() { ], ); - let treasury_asset_0_before = sut.reserves[0].token.balance(&sut.pool.treasury()); - let treasury_asset_1_before = sut.reserves[1].token.balance(&sut.pool.treasury()); - let treasury_asset_2_before = sut.reserves[2].token.balance(&sut.pool.treasury()); + let treasury_asset_0_before = sut.pool.protocol_fee(&sut.reserves[0].token.address); + let treasury_asset_1_before = sut.pool.protocol_fee(&sut.reserves[1].token.address); + let treasury_asset_2_before = sut.pool.protocol_fee(&sut.reserves[2].token.address); let s_token_undetlying_asset_0_before = sut.reserves[0] .token @@ -91,9 +93,9 @@ fn should_require_borrower_to_pay_fee() { &Bytes::new(&env), ); - let treasury_asset_0_after = sut.reserves[0].token.balance(&sut.pool.treasury()); - let treasury_asset_1_after = sut.reserves[1].token.balance(&sut.pool.treasury()); - let treasury_asset_2_after = sut.reserves[2].token.balance(&sut.pool.treasury()); + let treasury_asset_0_after = sut.pool.protocol_fee(&sut.reserves[0].token.address); + let treasury_asset_1_after = sut.pool.protocol_fee(&sut.reserves[1].token.address); + let treasury_asset_2_after = sut.pool.protocol_fee(&sut.reserves[2].token.address); let s_token_undetlying_asset_0_after = sut.reserves[0] .token @@ -215,8 +217,287 @@ fn should_emit_events() { &sut.reserves[0].token.address ) .into_val(&env), - (1000000i128, 500i128).into_val(&env) + (1000000i128, 500i128, false).into_val(&env) ), ] ); } + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #4)")] +fn rwa_fail_when_exceed_assets_limit() { + let env = Env::default(); + env.mock_all_auths(); + + let sut = init_pool(&env, false); + let (_, borrower, _) = fill_pool(&env, &sut, false); + + sut.pool.set_pool_configuration(&PoolConfig { + base_asset_address: sut.reserves[0].token.address.clone(), + base_asset_decimals: sut.reserves[0].token.decimals(), + flash_loan_fee: 5, + initial_health: 0, + timestamp_window: 20, + grace_period: 1, + user_assets_limit: 1, + min_collat_amount: 0, + min_debt_amount: 0, + liquidation_protocol_fee: 0, + ir_alpha: 143, + ir_initial_rate: 200, + ir_max_rate: 50_000, + ir_scaling_coeff: 9_000, + }); + + let _: Val = env.invoke_contract( + &sut.flash_loan_receiver.address, + &Symbol::new(&env, "initialize"), + vec![&env, sut.pool.address.into_val(&env), false.into_val(&env)], + ); + + let loan_assets = Vec::from_array( + &env, + [ + FlashLoanAsset { + asset: sut.reserves[0].token.address.clone(), + amount: 1000000, + borrow: false, + }, + FlashLoanAsset { + asset: sut.reserves[1].token.address.clone(), + amount: 2000000, + borrow: false, + }, + FlashLoanAsset { + asset: sut.reserves[2].token.address.clone(), + amount: 3000000, + borrow: true, + }, + ], + ); + + sut.pool.flash_loan( + &borrower, + &sut.flash_loan_receiver.address, + &loan_assets, + &Bytes::new(&env), + ); +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #3)")] +fn should_fail_when_debt_lt_min_position_amount() { + let env = Env::default(); + env.mock_all_auths(); + + let sut = init_pool(&env, false); + let (_, borrower, _) = fill_pool(&env, &sut, false); + + sut.pool.set_pool_configuration(&PoolConfig { + base_asset_address: sut.reserves[0].token.address.clone(), + base_asset_decimals: sut.reserves[0].token.decimals(), + flash_loan_fee: 5, + initial_health: 0, + timestamp_window: 20, + grace_period: 1, + user_assets_limit: 2, + min_collat_amount: 0, + min_debt_amount: 4_000_000, + liquidation_protocol_fee: 0, + ir_alpha: 143, + ir_initial_rate: 200, + ir_max_rate: 50_000, + ir_scaling_coeff: 9_000, + }); + + let _: Val = env.invoke_contract( + &sut.flash_loan_receiver.address, + &Symbol::new(&env, "initialize"), + vec![&env, sut.pool.address.into_val(&env), false.into_val(&env)], + ); + + let loan_assets = Vec::from_array( + &env, + [ + FlashLoanAsset { + asset: sut.reserves[0].token.address.clone(), + amount: 1000000, + borrow: false, + }, + FlashLoanAsset { + asset: sut.reserves[1].token.address.clone(), + amount: 2000000, + borrow: false, + }, + FlashLoanAsset { + asset: sut.reserves[2].token.address.clone(), + amount: 3000000, + borrow: true, + }, + ], + ); + + sut.pool.flash_loan( + &borrower, + &sut.flash_loan_receiver.address, + &loan_assets, + &Bytes::new(&env), + ); +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #5)")] +fn should_fail_if_borrow_in_grace_period() { + let env = Env::default(); + env.mock_all_auths(); + + let sut = init_pool(&env, false); + let (_, borrower, _) = fill_pool(&env, &sut, false); + + let _: Val = env.invoke_contract( + &sut.flash_loan_receiver.address, + &Symbol::new(&env, "initialize"), + vec![&env, sut.pool.address.into_val(&env), false.into_val(&env)], + ); + + let loan_assets = Vec::from_array( + &env, + [ + FlashLoanAsset { + asset: sut.reserves[0].token.address.clone(), + amount: 1000000, + borrow: false, + }, + FlashLoanAsset { + asset: sut.reserves[1].token.address.clone(), + amount: 2000000, + borrow: false, + }, + FlashLoanAsset { + asset: sut.reserves[2].token.address.clone(), + amount: 3000000, + borrow: true, + }, + ], + ); + + sut.pool.set_pause(&true); + sut.pool.set_pause(&false); + + sut.pool.flash_loan( + &borrower, + &sut.flash_loan_receiver.address, + &loan_assets, + &Bytes::new(&env), + ); +} + +#[test] +fn should_not_fail_in_grace_period() { + let env = Env::default(); + env.mock_all_auths(); + + let sut = init_pool(&env, false); + let (_, borrower, _) = fill_pool(&env, &sut, false); + + let _: Val = env.invoke_contract( + &sut.flash_loan_receiver.address, + &Symbol::new(&env, "initialize"), + vec![&env, sut.pool.address.into_val(&env), false.into_val(&env)], + ); + + let loan_assets = Vec::from_array( + &env, + [ + FlashLoanAsset { + asset: sut.reserves[0].token.address.clone(), + amount: 1000000, + borrow: false, + }, + FlashLoanAsset { + asset: sut.reserves[1].token.address.clone(), + amount: 2000000, + borrow: false, + }, + FlashLoanAsset { + asset: sut.reserves[2].token.address.clone(), + amount: 3000000, + borrow: false, + }, + ], + ); + + sut.pool.set_pause(&true); + sut.pool.set_pause(&false); + + sut.pool.flash_loan( + &borrower, + &sut.flash_loan_receiver.address, + &loan_assets, + &Bytes::new(&env), + ); +} + +#[test] +fn flashloan_should_pay_protocol_fee_if_not_borrow() { + let env = Env::default(); + env.mock_all_auths(); + + let sut = init_pool(&env, false); + let (_, borrower, _) = fill_pool(&env, &sut, false); + + let _: Val = env.invoke_contract( + &sut.flash_loan_receiver.address, + &Symbol::new(&env, "initialize"), + vec![&env, sut.pool.address.into_val(&env), false.into_val(&env)], + ); + + let flash_loan_fee = + FixedI128::from_percentage(sut.pool.pool_configuration().flash_loan_fee).unwrap(); + + let protocol_fee_0_before = sut.pool.protocol_fee(&sut.reserves[0].token.address); + let protocol_fee_1_before = sut.pool.protocol_fee(&sut.reserves[1].token.address); + + let loan_assets = Vec::from_array( + &env, + [ + FlashLoanAsset { + asset: sut.reserves[0].token.address.clone(), + amount: 1000000, + borrow: false, + }, + FlashLoanAsset { + asset: sut.reserves[1].token.address.clone(), + amount: 2000000, + borrow: false, + }, + FlashLoanAsset { + asset: sut.reserves[2].token.address.clone(), + amount: 3000000, + borrow: true, + }, + ], + ); + + sut.pool.flash_loan( + &borrower, + &sut.flash_loan_receiver.address, + &loan_assets, + &Bytes::new(&env), + ); + + let protocol_fee_0_after = sut.pool.protocol_fee(&sut.reserves[0].token.address); + let protocol_fee_1_after = sut.pool.protocol_fee(&sut.reserves[1].token.address); + + assert!(protocol_fee_0_after > protocol_fee_0_before); + assert!(protocol_fee_1_after > protocol_fee_1_before); + + assert_eq!( + protocol_fee_0_after - protocol_fee_0_before, + flash_loan_fee.mul_int(1000000).unwrap() + ); + assert_eq!( + protocol_fee_1_after - protocol_fee_1_before, + flash_loan_fee.mul_int(2000000).unwrap() + ); +} diff --git a/contracts/pool/src/tests/flash_loan_fee.rs b/contracts/pool/src/tests/flash_loan_fee.rs deleted file mode 100644 index ee250bd3..00000000 --- a/contracts/pool/src/tests/flash_loan_fee.rs +++ /dev/null @@ -1,11 +0,0 @@ -use crate::{tests::sut::init_pool, *}; - -#[test] -fn should_return_flash_loan_fee() { - let env = Env::default(); - env.mock_all_auths(); - - let sut = init_pool(&env, false); - - assert_eq!(sut.pool.flash_loan_fee(), 5); -} diff --git a/contracts/pool/src/tests/get_reserve.rs b/contracts/pool/src/tests/get_reserve.rs index 4e5a09c7..10574c0b 100644 --- a/contracts/pool/src/tests/get_reserve.rs +++ b/contracts/pool/src/tests/get_reserve.rs @@ -28,7 +28,8 @@ fn should_return_reserve() { let (underlying_token, _) = create_token_contract(&env, &token_admin); let (debt_token, _) = create_token_contract(&env, &token_admin); - let pool: LendingPoolClient<'_> = create_pool_contract(&env, &admin, false); + let pool: LendingPoolClient<'_> = + create_pool_contract(&env, &admin, false, &underlying_token.address); let s_token = create_s_token_contract(&env, &pool.address, &underlying_token.address); assert!(pool.get_reserve(&underlying_token.address).is_none()); diff --git a/contracts/pool/src/tests/init_reserve.rs b/contracts/pool/src/tests/init_reserve.rs index bcbe9280..5f62ee75 100644 --- a/contracts/pool/src/tests/init_reserve.rs +++ b/contracts/pool/src/tests/init_reserve.rs @@ -20,7 +20,8 @@ fn should_require_admin() { let (underlying_token, _) = create_token_contract(&env, &token_admin); let (debt_token, _) = create_token_contract(&env, &token_admin); - let pool: LendingPoolClient<'_> = create_pool_contract(&env, &admin, false); + let pool: LendingPoolClient<'_> = + create_pool_contract(&env, &admin, false, &underlying_token.address); let s_token = create_s_token_contract(&env, &pool.address, &underlying_token.address); assert!(pool.get_reserve(&underlying_token.address).is_none()); @@ -54,7 +55,7 @@ fn should_require_admin() { } #[test] -#[should_panic(expected = "HostError: Error(Contract, #105)")] +#[should_panic(expected = "HostError: Error(Contract, #0)")] fn should_fail_when_calling_second_time() { let env = Env::default(); env.mock_all_auths(); @@ -103,7 +104,8 @@ fn should_set_underlying_asset_s_token_and_debt_token_addresses() { let (underlying_token, _) = create_token_contract(&env, &token_admin); let (debt_token, _) = create_token_contract(&env, &token_admin); - let pool: LendingPoolClient<'_> = create_pool_contract(&env, &admin, false); + let pool: LendingPoolClient<'_> = + create_pool_contract(&env, &admin, false, &underlying_token.address); let s_token = create_s_token_contract(&env, &pool.address, &underlying_token.address); assert!(pool.get_reserve(&underlying_token.address).is_none()); diff --git a/contracts/pool/src/tests/ir_params.rs b/contracts/pool/src/tests/ir_params.rs deleted file mode 100644 index e604502c..00000000 --- a/contracts/pool/src/tests/ir_params.rs +++ /dev/null @@ -1,26 +0,0 @@ -use crate::tests::sut::init_pool; -use crate::*; - -#[test] -fn should_return_ir_params() { - let env = Env::default(); - env.mock_all_auths(); - - let sut = init_pool(&env, false); - - let ir_params_input = IRParams { - alpha: 144, - initial_rate: 201, - max_rate: 50_001, - scaling_coeff: 9_001, - }; - - sut.pool.set_ir_params(&ir_params_input); - - let ir_params = sut.pool.ir_params().unwrap(); - - assert_eq!(ir_params_input.alpha, ir_params.alpha); - assert_eq!(ir_params_input.initial_rate, ir_params.initial_rate); - assert_eq!(ir_params_input.max_rate, ir_params.max_rate); - assert_eq!(ir_params_input.scaling_coeff, ir_params.scaling_coeff); -} diff --git a/contracts/pool/src/tests/liquidate.rs b/contracts/pool/src/tests/liquidate.rs index b00320fb..f2482e20 100644 --- a/contracts/pool/src/tests/liquidate.rs +++ b/contracts/pool/src/tests/liquidate.rs @@ -1,9 +1,11 @@ use crate::tests::sut::{fill_pool, fill_pool_three, init_pool, DAY}; use crate::*; +use common::FixedI128; use price_feed_interface::types::asset::Asset; use price_feed_interface::types::price_data::PriceData; -use soroban_sdk::testutils::{Address as _, AuthorizedFunction, Events, Ledger}; +use soroban_sdk::testutils::{Address as _, AuthorizedFunction, Events}; use soroban_sdk::{symbol_short, vec, IntoVal, Symbol}; +use tests::sut::set_time; use super::sut::fill_pool_six; @@ -14,22 +16,37 @@ fn should_require_authorized_caller() { let sut = init_pool(&env, false); let (_, borrower, liquidator, _) = fill_pool_three(&env, &sut); - sut.pool.set_initial_health(&2_500); - - sut.pool.liquidate(&liquidator, &borrower, &false); + sut.pool.set_pool_configuration(&PoolConfig { + base_asset_address: sut.reserves[0].token.address.clone(), + base_asset_decimals: sut.reserves[0].token.decimals(), + flash_loan_fee: 5, + initial_health: 2_500, + timestamp_window: 20, + grace_period: 1, + user_assets_limit: 4, + min_collat_amount: 0, + min_debt_amount: 0, + liquidation_protocol_fee: 0, + ir_alpha: 143, + ir_initial_rate: 200, + ir_max_rate: 50_000, + ir_scaling_coeff: 9_000, + }); + + sut.pool.liquidate(&liquidator, &borrower); assert_eq!( env.auths().pop().map(|f| f.1.function).unwrap(), AuthorizedFunction::Contract(( sut.pool.address.clone(), symbol_short!("liquidate"), - (liquidator.clone(), borrower.clone(), false).into_val(&env) + (liquidator.clone(), borrower.clone()).into_val(&env) )), ); } #[test] -#[should_panic(expected = "HostError: Error(Contract, #3)")] +#[should_panic(expected = "HostError: Error(Contract, #2)")] fn should_fail_when_pool_paused() { let env = Env::default(); env.mock_all_auths(); @@ -38,11 +55,11 @@ fn should_fail_when_pool_paused() { let (_, borrower, liquidator, _) = fill_pool_three(&env, &sut); sut.pool.set_pause(&true); - sut.pool.liquidate(&liquidator, &borrower, &false); + sut.pool.liquidate(&liquidator, &borrower); } #[test] -#[should_panic(expected = "HostError: Error(Contract, #101)")] +#[should_panic(expected = "HostError: Error(Contract, #100)")] fn should_fail_when_reserve_deactivated() { let env = Env::default(); env.mock_all_auths(); @@ -52,11 +69,11 @@ fn should_fail_when_reserve_deactivated() { let collat_reserve = sut.reserves[0].token.address.clone(); sut.pool.set_reserve_status(&collat_reserve, &false); - sut.pool.liquidate(&liquidator, &borrower, &false); + sut.pool.liquidate(&liquidator, &borrower); } #[test] -#[should_panic(expected = "HostError: Error(Contract, #303)")] +#[should_panic(expected = "HostError: Error(Contract, #301)")] fn should_fail_when_good_position() { let env = Env::default(); env.mock_all_auths(); @@ -68,24 +85,7 @@ fn should_fail_when_good_position() { let position = sut.pool.account_position(&borrower); assert!(position.npv > 0, "test configuration"); - sut.pool.liquidate(&liquidator, &borrower, &false); -} - -#[test] -#[should_panic(expected = "HostError: Error(Contract, #204)")] -fn should_fail_when_have_debt_in_receiving_s_token() { - let env = Env::default(); - env.mock_all_auths(); - - let sut = init_pool(&env, false); - let (_, borrower, liquidator, debt_config) = fill_pool_three(&env, &sut); - - sut.pool - .deposit(&liquidator, &debt_config.token.address, &500_000_000); - sut.pool - .borrow(&liquidator, &sut.reserves[0].token.address, &1_000_000); - - sut.pool.liquidate(&liquidator, &borrower, &true); + sut.pool.liquidate(&liquidator, &borrower); } #[test] @@ -99,7 +99,7 @@ fn should_fail_when_liquidator_has_not_enough_underlying_asset() { let token_address = debt_config.token.address.clone(); sut.pool.deposit(&liquidator, &token_address, &999_990_000); - sut.pool.liquidate(&liquidator, &borrower, &false); + sut.pool.liquidate(&liquidator, &borrower); } #[test] @@ -113,9 +113,24 @@ fn should_liquidate_reducing_position_to_healthy() { let collat_2_token = sut.reserves[2].token.address.clone(); let debt_token = sut.reserves[1].token.address.clone(); - sut.pool.set_initial_health(&2_500); - - env.ledger().with_mut(|li| li.timestamp = 10_000); + sut.pool.set_pool_configuration(&PoolConfig { + base_asset_address: sut.reserves[0].token.address.clone(), + base_asset_decimals: sut.reserves[0].token.decimals(), + flash_loan_fee: 5, + initial_health: 2_500, + timestamp_window: 20, + grace_period: 1, + user_assets_limit: 4, + min_collat_amount: 0, + min_debt_amount: 0, + liquidation_protocol_fee: 0, + ir_alpha: 143, + ir_initial_rate: 200, + ir_max_rate: 50_000, + ir_scaling_coeff: 9_000, + }); + + set_time(&env, &sut, 10_000, true); sut.pool .deposit(&borrower, &collat_1_token, &10_000_000_000); @@ -150,12 +165,12 @@ fn should_liquidate_reducing_position_to_healthy() { &env, PriceData { price: (18 * 10i128.pow(15)), - timestamp: 0, + timestamp: 10_000, }, ], ); - sut.pool.liquidate(&liquidator, &borrower, &false); + sut.pool.liquidate(&liquidator, &borrower); let borrower_token_0_after = sut.reserves[0].token.balance(&borrower); let borrower_token_1_after = sut.reserves[1].token.balance(&borrower); @@ -209,20 +224,20 @@ fn should_liquidate_reducing_position_to_healthy() { assert_eq!(borrower_token_2_after, 0); assert_eq!(borrower_stoken_0_after, 0); assert_eq!(borrower_stoken_1_after, 0); - assert_eq!(borrower_stoken_2_after, 456_547_338_651); + assert_eq!(borrower_stoken_2_after, 456_547_338_939); assert_eq!(borrower_dtoken_0_after, 0); - assert_eq!(borrower_dtoken_1_after, 114_129_609_027); + assert_eq!(borrower_dtoken_1_after, 114_129_609_050); assert_eq!(borrower_dtoken_2_after, 0); - assert_eq!(borrower_account_position_after.npv, 684_821_005); + assert_eq!(borrower_account_position_after.npv, 684_821_007); assert_eq!( borrower_account_position_after.discounted_collateral, - 2_739_284_031 + 2_739_284_033 ); assert_eq!(borrower_account_position_after.debt, 2_054_463_026); assert_eq!(liquidator_token_0_after, 20_000_000_000); - assert_eq!(liquidator_token_1_after, 314_086_185_200); - assert_eq!(liquidator_token_2_after, 1_543_452_661_349); + assert_eq!(liquidator_token_1_after, 314_086_185_223); + assert_eq!(liquidator_token_2_after, 1_543_452_661_061); assert_eq!(liquidator_stoken_0_after, 0); assert_eq!(liquidator_stoken_1_after, 0); assert_eq!(liquidator_stoken_2_after, 0); @@ -232,7 +247,7 @@ fn should_liquidate_reducing_position_to_healthy() { } #[test] -fn should_liquidate_receiving_stokens_when_requested() { +fn should_fully_liquidate_when_gte_max_penalty() { let env = Env::default(); env.mock_all_auths(); @@ -242,9 +257,24 @@ fn should_liquidate_receiving_stokens_when_requested() { let collat_2_token = sut.reserves[2].token.address.clone(); let debt_token = sut.reserves[1].token.address.clone(); - sut.pool.set_initial_health(&2_500); - - env.ledger().with_mut(|li| li.timestamp = 10_000); + sut.pool.set_pool_configuration(&PoolConfig { + base_asset_address: sut.reserves[0].token.address.clone(), + base_asset_decimals: sut.reserves[0].token.decimals(), + flash_loan_fee: 5, + initial_health: 2_500, + timestamp_window: 20, + grace_period: 1, + user_assets_limit: 4, + min_collat_amount: 0, + min_debt_amount: 0, + liquidation_protocol_fee: 0, + ir_alpha: 143, + ir_initial_rate: 200, + ir_max_rate: 50_000, + ir_scaling_coeff: 9_000, + }); + + set_time(&env, &sut, 10_000, true); sut.pool .deposit(&borrower, &collat_1_token, &10_000_000_000); @@ -278,13 +308,13 @@ fn should_liquidate_receiving_stokens_when_requested() { &vec![ &env, PriceData { - price: (18 * 10i128.pow(15)), - timestamp: 0, + price: (2 * 10i128.pow(16)), + timestamp: 10_000, }, ], ); - sut.pool.liquidate(&liquidator, &borrower, &true); + sut.pool.liquidate(&liquidator, &borrower); let borrower_token_0_after = sut.reserves[0].token.balance(&borrower); let borrower_token_1_after = sut.reserves[1].token.balance(&borrower); @@ -338,23 +368,20 @@ fn should_liquidate_receiving_stokens_when_requested() { assert_eq!(borrower_token_2_after, 0); assert_eq!(borrower_stoken_0_after, 0); assert_eq!(borrower_stoken_1_after, 0); - assert_eq!(borrower_stoken_2_after, 456_547_338_651); + assert_eq!(borrower_stoken_2_after, 0); assert_eq!(borrower_dtoken_0_after, 0); - assert_eq!(borrower_dtoken_1_after, 114_129_609_027); + assert_eq!(borrower_dtoken_1_after, 0); assert_eq!(borrower_dtoken_2_after, 0); - assert_eq!(borrower_account_position_after.npv, 684_821_005); - assert_eq!( - borrower_account_position_after.discounted_collateral, - 2_739_284_031 - ); - assert_eq!(borrower_account_position_after.debt, 2_054_463_026); + assert_eq!(borrower_account_position_after.npv, 0); + assert_eq!(borrower_account_position_after.discounted_collateral, 0); + assert_eq!(borrower_account_position_after.debt, 0); - assert_eq!(liquidator_token_0_after, 10_000_000_000); - assert_eq!(liquidator_token_1_after, 314_086_185_200); - assert_eq!(liquidator_token_2_after, 1_000_000_000_000); - assert_eq!(liquidator_stoken_0_after, 10_000_000_000); + assert_eq!(liquidator_token_0_after, 20_000_000_000); + assert_eq!(liquidator_token_1_after, 199_949_350_400); + assert_eq!(liquidator_token_2_after, 2_000_000_000_000); + assert_eq!(liquidator_stoken_0_after, 0); assert_eq!(liquidator_stoken_1_after, 0); - assert_eq!(liquidator_stoken_2_after, 543_452_661_349); + assert_eq!(liquidator_stoken_2_after, 0); assert_eq!(liquidator_dtoken_0_after, 0); assert_eq!(liquidator_dtoken_1_after, 0); assert_eq!(liquidator_dtoken_2_after, 0); @@ -370,6 +397,7 @@ fn should_change_user_config() { let collat_1_token = sut.reserves[0].token.address.clone(); let collat_2_token = sut.reserves[2].token.address.clone(); let debt_token = sut.reserves[1].token.address.clone(); + sut.pool.deposit(&liquidator, &collat_1_token, &1); let reserve_0 = sut .pool @@ -384,9 +412,24 @@ fn should_change_user_config() { .get_reserve(&sut.reserves[2].token.address) .unwrap(); - sut.pool.set_initial_health(&2_500); - - env.ledger().with_mut(|li| li.timestamp = 10_000); + sut.pool.set_pool_configuration(&PoolConfig { + base_asset_address: sut.reserves[0].token.address.clone(), + base_asset_decimals: sut.reserves[0].token.decimals(), + flash_loan_fee: 5, + initial_health: 2_500, + timestamp_window: 20, + grace_period: 1, + user_assets_limit: 4, + min_collat_amount: 0, + min_debt_amount: 0, + liquidation_protocol_fee: 0, + ir_alpha: 143, + ir_initial_rate: 200, + ir_max_rate: 50_000, + ir_scaling_coeff: 9_000, + }); + + set_time(&env, &sut, 10_000, false); sut.pool .deposit(&borrower, &collat_1_token, &10_000_000_000); @@ -400,11 +443,12 @@ fn should_change_user_config() { &env, PriceData { price: (18 * 10i128.pow(15)), - timestamp: 0, + timestamp: 10_000, }, ], ); + let liquidator_user_config = sut.pool.user_configuration(&liquidator); let borrower_user_config = sut.pool.user_configuration(&borrower); let is_borrower_borrowed_token_0_before = @@ -419,10 +463,25 @@ fn should_change_user_config() { borrower_user_config.is_using_as_collateral(&env, reserve_1.get_id()); let is_borrower_deposited_token_2_before = borrower_user_config.is_using_as_collateral(&env, reserve_2.get_id()); + let borrower_total_assets_before = borrower_user_config.total_assets(); - env.ledger().with_mut(|li| li.timestamp = 2 * DAY); + let is_liquidator_borrowed_token_0_before = + liquidator_user_config.is_borrowing(&env, reserve_0.get_id()); + let is_liquidator_borrowed_token_1_before = + liquidator_user_config.is_borrowing(&env, reserve_1.get_id()); + let is_liquidator_borrowed_token_2_before = + liquidator_user_config.is_borrowing(&env, reserve_2.get_id()); + let is_liquidator_deposited_token_0_before = + liquidator_user_config.is_using_as_collateral(&env, reserve_0.get_id()); + let is_liquidator_deposited_token_1_before = + liquidator_user_config.is_using_as_collateral(&env, reserve_1.get_id()); + let is_liquidator_deposited_token_2_before = + liquidator_user_config.is_using_as_collateral(&env, reserve_2.get_id()); + let liquidator_total_assets_before = liquidator_user_config.total_assets(); - sut.pool.liquidate(&liquidator, &borrower, &true); + set_time(&env, &sut, 2 * DAY, false); + + sut.pool.liquidate(&liquidator, &borrower); let liquidator_user_config = sut.pool.user_configuration(&liquidator); let borrower_user_config = sut.pool.user_configuration(&borrower); @@ -439,6 +498,7 @@ fn should_change_user_config() { liquidator_user_config.is_using_as_collateral(&env, reserve_1.get_id()); let is_liquidator_deposited_token_2_after = liquidator_user_config.is_using_as_collateral(&env, reserve_2.get_id()); + let liquidator_total_assets_after = liquidator_user_config.total_assets(); let is_borrower_borrowed_token_0_after = borrower_user_config.is_borrowing(&env, reserve_0.get_id()); @@ -452,6 +512,7 @@ fn should_change_user_config() { borrower_user_config.is_using_as_collateral(&env, reserve_1.get_id()); let is_borrower_deposited_token_2_after = borrower_user_config.is_using_as_collateral(&env, reserve_2.get_id()); + let borrower_total_assets_after = borrower_user_config.total_assets(); assert_eq!(is_borrower_borrowed_token_0_before, false); assert_eq!(is_borrower_borrowed_token_1_before, true); @@ -460,24 +521,49 @@ fn should_change_user_config() { assert_eq!(is_borrower_deposited_token_0_before, true); assert_eq!(is_borrower_deposited_token_1_before, false); assert_eq!(is_borrower_deposited_token_2_before, true); + assert_eq!(borrower_total_assets_before, 3); assert_eq!(is_borrower_borrowed_token_0_after, false); assert_eq!(is_borrower_borrowed_token_1_after, true); assert_eq!(is_borrower_borrowed_token_2_after, false); - assert_eq!(is_liquidator_borrowed_token_0_after, false); - assert_eq!(is_liquidator_borrowed_token_1_after, false); - assert_eq!(is_liquidator_borrowed_token_2_after, false); + assert_eq!(borrower_total_assets_after, 2); + + assert_eq!( + is_liquidator_borrowed_token_0_before, + is_liquidator_borrowed_token_0_after + ); + assert_eq!( + is_liquidator_borrowed_token_1_before, + is_liquidator_borrowed_token_1_after + ); + assert_eq!( + is_liquidator_borrowed_token_2_before, + is_liquidator_borrowed_token_2_after + ); assert_eq!(is_borrower_deposited_token_0_after, false); assert_eq!(is_borrower_deposited_token_1_after, false); assert_eq!(is_borrower_deposited_token_2_after, true); - assert_eq!(is_liquidator_deposited_token_0_after, true); - assert_eq!(is_liquidator_deposited_token_1_after, false); - assert_eq!(is_liquidator_deposited_token_2_after, true); + assert_eq!( + is_liquidator_deposited_token_0_before, + is_liquidator_deposited_token_0_after + ); + assert_eq!( + is_liquidator_deposited_token_1_before, + is_liquidator_deposited_token_1_after + ); + assert_eq!( + is_liquidator_deposited_token_2_before, + is_liquidator_deposited_token_2_after + ); + assert_eq!( + liquidator_total_assets_before, + liquidator_total_assets_after + ); } #[test] -fn should_affect_account_data() { +fn should_affect_borrower_account_data() { let env = Env::default(); env.mock_all_auths(); @@ -487,9 +573,28 @@ fn should_affect_account_data() { let collat_2_token = sut.reserves[2].token.address.clone(); let debt_token = sut.reserves[1].token.address.clone(); - sut.pool.set_initial_health(&2_500); - - env.ledger().with_mut(|li| li.timestamp = 10_000); + sut.pool.set_pool_configuration(&PoolConfig { + base_asset_address: sut.reserves[0].token.address.clone(), + base_asset_decimals: sut.reserves[0].token.decimals(), + flash_loan_fee: 5, + initial_health: 2_500, + timestamp_window: 20, + grace_period: 1, + user_assets_limit: 4, + min_collat_amount: 0, + min_debt_amount: 0, + liquidation_protocol_fee: 0, + ir_alpha: 143, + ir_initial_rate: 200, + ir_max_rate: 50_000, + ir_scaling_coeff: 9_000, + }); + + sut.pool.deposit(&liquidator, &collat_1_token, &1); + + sut.pool.deposit(&liquidator, &collat_1_token, &1); + + set_time(&env, &sut, 10_000, true); sut.pool .deposit(&borrower, &collat_1_token, &10_000_000_000); @@ -503,16 +608,17 @@ fn should_affect_account_data() { &env, PriceData { price: (18 * 10i128.pow(15)), - timestamp: 0, + timestamp: 10_000, }, ], ); + let liquidator_account_position_before = sut.pool.account_position(&liquidator); let borrower_account_position_before = sut.pool.account_position(&borrower); - env.ledger().with_mut(|li| li.timestamp = 2 * DAY); + set_time(&env, &sut, 2 * DAY + 1, false); // initial timestamp = grace period = 1 - sut.pool.liquidate(&liquidator, &borrower, &true); + sut.pool.liquidate(&liquidator, &borrower); let liquidator_account_position_after = sut.pool.account_position(&liquidator); let borrower_account_position_after = sut.pool.account_position(&borrower); @@ -525,18 +631,24 @@ fn should_affect_account_data() { assert_eq!(borrower_account_position_before.npv, -2_400_911_692); assert_eq!( - liquidator_account_position_after.discounted_collateral, - 9_319_109_725 + liquidator_account_position_before.discounted_collateral, + liquidator_account_position_after.discounted_collateral + ); + assert_eq!( + liquidator_account_position_before.debt, + liquidator_account_position_after.debt + ); + assert_eq!( + liquidator_account_position_before.npv, + liquidator_account_position_after.npv ); - assert_eq!(liquidator_account_position_after.debt, 0); - assert_eq!(liquidator_account_position_after.npv, 9_319_109_725); assert_eq!( borrower_account_position_after.discounted_collateral, - 2_680_890_273 + 2_680_890_275 ); assert_eq!(borrower_account_position_after.debt, 2_008_842_827); - assert_eq!(borrower_account_position_after.npv, 672_047_446); + assert_eq!(borrower_account_position_after.npv, 672_047_448); } #[test] @@ -550,9 +662,24 @@ fn should_affect_coeffs() { let collat_2_token = sut.reserves[2].token.address.clone(); let debt_token = sut.reserves[1].token.address.clone(); - sut.pool.set_initial_health(&2_500); - - env.ledger().with_mut(|li| li.timestamp = 10_000); + sut.pool.set_pool_configuration(&PoolConfig { + base_asset_address: sut.reserves[0].token.address.clone(), + base_asset_decimals: sut.reserves[0].token.decimals(), + flash_loan_fee: 5, + initial_health: 2_500, + timestamp_window: 20, + grace_period: 1, + user_assets_limit: 4, + min_collat_amount: 0, + min_debt_amount: 0, + liquidation_protocol_fee: 0, + ir_alpha: 143, + ir_initial_rate: 200, + ir_max_rate: 50_000, + ir_scaling_coeff: 9_000, + }); + + set_time(&env, &sut, 10_000, false); sut.pool .deposit(&borrower, &collat_1_token, &10_000_000_000); @@ -566,12 +693,12 @@ fn should_affect_coeffs() { &env, PriceData { price: (18 * 10i128.pow(15)), - timestamp: 0, + timestamp: 10_000, }, ], ); - env.ledger().with_mut(|li| li.timestamp = 4 * DAY); + set_time(&env, &sut, 4 * DAY, false); let asset_1 = sut.reserves[0].token.address.clone(); let asset_2 = sut.reserves[1].token.address.clone(); @@ -584,11 +711,11 @@ fn should_affect_coeffs() { let asset_3_collat_coeff_before = sut.pool.collat_coeff(&asset_3); let asset_3_debt_coeff_before = sut.pool.debt_coeff(&asset_3); - env.ledger().with_mut(|li| li.timestamp = 5 * DAY); + set_time(&env, &sut, 5 * DAY, false); - sut.pool.liquidate(&liquidator, &borrower, &false); + sut.pool.liquidate(&liquidator, &borrower); - env.ledger().with_mut(|li| li.timestamp = 6 * DAY); + set_time(&env, &sut, 6 * DAY, false); let asset_1_collat_coeff_after = sut.pool.collat_coeff(&asset_1); let asset_1_debt_coeff_after = sut.pool.debt_coeff(&asset_1); @@ -616,9 +743,24 @@ fn should_emit_events() { let collat_2_token = sut.reserves[2].token.address.clone(); let debt_token = sut.reserves[1].token.address.clone(); - sut.pool.set_initial_health(&2_500); - - env.ledger().with_mut(|li| li.timestamp = 10_000); + sut.pool.set_pool_configuration(&PoolConfig { + base_asset_address: sut.reserves[0].token.address.clone(), + base_asset_decimals: sut.reserves[0].token.decimals(), + flash_loan_fee: 5, + initial_health: 2_500, + timestamp_window: 20, + grace_period: 1, + user_assets_limit: 4, + min_collat_amount: 0, + min_debt_amount: 0, + liquidation_protocol_fee: 0, + ir_alpha: 143, + ir_initial_rate: 200, + ir_max_rate: 50_000, + ir_scaling_coeff: 9_000, + }); + + set_time(&env, &sut, 10_000, false); sut.pool .deposit(&borrower, &collat_1_token, &10_000_000_000); @@ -632,12 +774,12 @@ fn should_emit_events() { &env, PriceData { price: (18 * 10i128.pow(15)), - timestamp: 0, + timestamp: 10_000, }, ], ); - sut.pool.liquidate(&liquidator, &borrower, &false); + sut.pool.liquidate(&liquidator, &borrower); let mut events = env.events().all(); let event = events.pop_back_unchecked(); @@ -649,7 +791,7 @@ fn should_emit_events() { ( sut.pool.address.clone(), (Symbol::new(&env, "liquidation"), borrower.clone()).into_val(&env), - (12_346_448_668i128, 15_434_526_613i128).into_val(&env) + (12_346_441_522i128, 15_434_514_766i128).into_val(&env) ), ] ); @@ -670,9 +812,24 @@ fn should_liquidate_rwa_collateral() { .token_admin .mint(&borrower, &100_000_000_000); - sut.pool.set_initial_health(&2_500); - - env.ledger().with_mut(|li| li.timestamp = 10_000); + sut.pool.set_pool_configuration(&PoolConfig { + base_asset_address: sut.reserves[0].token.address.clone(), + base_asset_decimals: sut.reserves[0].token.decimals(), + flash_loan_fee: 5, + initial_health: 2_500, + timestamp_window: 20, + grace_period: 1, + user_assets_limit: 4, + min_collat_amount: 0, + min_debt_amount: 0, + liquidation_protocol_fee: 0, + ir_alpha: 143, + ir_initial_rate: 200, + ir_max_rate: 50_000, + ir_scaling_coeff: 9_000, + }); + + set_time(&env, &sut, 10_000, false); sut.pool .deposit(&borrower, &collat_1_token, &10_000_000_000); @@ -689,12 +846,12 @@ fn should_liquidate_rwa_collateral() { &env, PriceData { price: (18 * 10i128.pow(15)), - timestamp: 0, + timestamp: 10_000, }, ], ); - sut.pool.liquidate(&liquidator, &borrower, &false); + sut.pool.liquidate(&liquidator, &borrower); let borrower_rwa_after = sut.rwa_config().token.balance(&borrower); let liquidator_rwa_after = sut.rwa_config().token.balance(&liquidator); @@ -708,3 +865,289 @@ fn should_liquidate_rwa_collateral() { assert!(liquidator_rwa_after > liquidator_rwa_before); assert!(pool_rwa_after < pool_rwa_before); } + +#[test] +fn should_not_panic_on_zero_collateral_transfer() { + let env = Env::default(); + env.mock_all_auths(); + + let sut = init_pool(&env, false); + let (liquidator, borrower) = fill_pool_six(&env, &sut); + let high_priority_collat = &sut.reserves[0].token.address; + let low_priority_collat = &sut.reserves[1].token.address; + let debt_token = &sut.reserves[2].token.address; + + // deposit collat with high priority with price ~1 and amount 1e-9 + sut.pool.deposit(&borrower, high_priority_collat, &1); + // deposit another collat + sut.pool + .deposit(&borrower, low_priority_collat, &1_000_000_000); + sut.pool.borrow(&borrower, debt_token, &500_000_000); + sut.price_feed.init( + &Asset::Stellar(debt_token.clone()), + &vec![ + &env, + PriceData { + price: 12_000_000_000_000_000, + timestamp: 0, + }, + ], + ); + let pos_before = sut.pool.account_position(&borrower); + sut.pool.liquidate(&liquidator, &borrower); + let pos_after = sut.pool.account_position(&borrower); + + assert!(pos_before.npv < pos_after.npv); +} + +#[test] +fn should_round_debt_correctly() { + let env = Env::default(); + env.mock_all_auths(); + + let sut = init_pool(&env, false); + sut.pool.set_pool_configuration(&PoolConfig { + base_asset_address: sut.reserves[0].token.address.clone(), + base_asset_decimals: sut.reserves[0].token.decimals(), + flash_loan_fee: 5, + initial_health: 2_500, + timestamp_window: 20, + grace_period: 1, + user_assets_limit: 4, + min_collat_amount: 0, + min_debt_amount: 0, + liquidation_protocol_fee: 0, + ir_alpha: 143, + ir_initial_rate: 200, + ir_max_rate: 50_000, + ir_scaling_coeff: 9_000, + }); + let (liquidator, borrower) = fill_pool_six(&env, &sut); + let high_priority_collat = &sut.reserves[0].token.address; + let low_priority_collat = &sut.reserves[1].token.address; + let debt_token = &sut.reserves[2].token.address; + + // deposit collat with high priority with price ~1 and amount 1e-9 + sut.pool.deposit(&borrower, high_priority_collat, &1); + // deposit another collat + sut.pool + .deposit(&borrower, low_priority_collat, &1_000_000_000); + sut.pool.borrow(&borrower, debt_token, &400_000_000); + sut.price_feed.init( + &Asset::Stellar(debt_token.clone()), + &vec![ + &env, + PriceData { + price: 16_000_000_000_000_000, + timestamp: 0, + }, + ], + ); + let pos_before = sut.pool.account_position(&borrower); + sut.pool.liquidate(&liquidator, &borrower); + let pos_after = sut.pool.account_position(&borrower); + + assert!(pos_before.npv < pos_after.npv); +} + +#[test] +fn should_round_correctly_with_low_collateral() { + let env = Env::default(); + env.mock_all_auths(); + + let sut = init_pool(&env, false); + sut.pool.set_pool_configuration(&PoolConfig { + base_asset_address: sut.reserves[0].token.address.clone(), + base_asset_decimals: sut.reserves[0].token.decimals(), + flash_loan_fee: 5, + initial_health: 2_500, + timestamp_window: 20, + grace_period: 1, + user_assets_limit: 4, + min_collat_amount: 0, + min_debt_amount: 0, + liquidation_protocol_fee: 0, + ir_alpha: 143, + ir_initial_rate: 200, + ir_max_rate: 50_000, + ir_scaling_coeff: 9_000, + }); + let (liquidator, borrower) = fill_pool_six(&env, &sut); + let high_priority_collat = &sut.reserves[0].token.address; + let low_priority_collat = &sut.reserves[1].token.address; + let debt_token = &sut.reserves[2].token.address; + + // deposit collat with high priority with price ~1 and amount 1e-9 + sut.pool.deposit(&borrower, high_priority_collat, &1); + // deposit another collat + sut.pool + .deposit(&borrower, low_priority_collat, &1_000_000_000); + sut.pool.borrow(&borrower, debt_token, &400_000_000); + sut.price_feed.init( + &Asset::Stellar(debt_token.clone()), + &vec![ + &env, + PriceData { + price: 20_000_000_000_000_000, + timestamp: 0, + }, + ], + ); + let pos_before = sut.pool.account_position(&borrower); + sut.pool.liquidate(&liquidator, &borrower); + let pos_after = sut.pool.account_position(&borrower); + + assert!(pos_before.npv < pos_after.npv); +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #5)")] +fn should_fail_in_grace_period() { + let env = Env::default(); + env.mock_all_auths(); + + let sut = init_pool(&env, false); + let (_, borrower, liquidator, _) = fill_pool_three(&env, &sut); + + sut.pool.set_pause(&true); + sut.pool.set_pause(&false); + sut.pool.liquidate(&liquidator, &borrower); +} + +#[test] +fn should_not_fail_after_grace_period() { + let env = Env::default(); + env.mock_all_auths(); + + let sut = init_pool(&env, false); + let pause_info = sut.pool.pause_info(); + let start = env.ledger().timestamp(); + let gap = 500; + let (_, borrower, liquidator, _) = fill_pool_three(&env, &sut); + let borrower_pos_before = sut.pool.account_position(&borrower); + + sut.pool.set_pause(&true); + set_time(&env, &sut, start + gap, false); + sut.pool.set_pause(&false); + set_time( + &env, + &sut, + start + gap + pause_info.grace_period_secs, + false, + ); + + sut.pool.liquidate(&liquidator, &borrower); + + let borrower_npv_after = sut.pool.account_position(&borrower); + + assert!(borrower_npv_after.npv > borrower_pos_before.npv); +} + +#[test] +fn should_pay_protocol_fee() { + let env = Env::default(); + env.mock_all_auths(); + + struct LiquidatorProtocolBonus { + liquidator: (i128, i128), + protocol: (i128, i128), + } + + let liquidate = |liquidation_protocol_fee| { + let sut = init_pool(&env, false); + let (liquidator, borrower) = fill_pool_six(&env, &sut); + let collat_1_token = sut.reserves[0].token.address.clone(); + let collat_2_token = sut.reserves[2].token.address.clone(); + let debt_token = sut.reserves[1].token.address.clone(); + + sut.pool.set_pool_configuration(&PoolConfig { + base_asset_address: sut.reserves[0].token.address.clone(), + base_asset_decimals: sut.reserves[0].token.decimals(), + flash_loan_fee: 5, + initial_health: 2_500, + timestamp_window: 20, + grace_period: 1, + user_assets_limit: 4, + min_collat_amount: 0, + min_debt_amount: 0, + liquidation_protocol_fee, + ir_alpha: 143, + ir_initial_rate: 200, + ir_max_rate: 50_000, + ir_scaling_coeff: 9_000, + }); + + let liquidator_balance_before_1 = sut.reserves[0].token.balance(&liquidator); + let liquidator_balance_before_2 = sut.reserves[2].token.balance(&liquidator); + let protocol_fee_before_1 = sut.pool.protocol_fee(&sut.reserves[0].token.address); + let protocol_fee_before_2 = sut.pool.protocol_fee(&sut.reserves[2].token.address); + + let timestamp = env.ledger().timestamp() + 10_000; + set_time(&env, &sut, timestamp, false); + + sut.pool + .deposit(&borrower, &collat_1_token, &10_000_000_000); + sut.pool + .deposit(&borrower, &collat_2_token, &1_000_000_000_000); + sut.pool.borrow(&borrower, &debt_token, &800_000_000_000); + + sut.price_feed.init( + &Asset::Stellar(debt_token), + &vec![ + &env, + PriceData { + price: (18 * 10i128.pow(15)), + timestamp: timestamp, + }, + ], + ); + + sut.pool.liquidate(&liquidator, &borrower); + + let liquidator_balance_after_1 = sut.reserves[0].token.balance(&liquidator); + let liquidator_balance_after_2 = sut.reserves[2].token.balance(&liquidator); + + let liquidator_bonus_1 = liquidator_balance_after_1 - liquidator_balance_before_1; + let liquidator_bonus_2 = liquidator_balance_after_2 - liquidator_balance_before_2; + + let protocol_fee_after_1 = sut.pool.protocol_fee(&sut.reserves[0].token.address); + let protocol_fee_after_2 = sut.pool.protocol_fee(&sut.reserves[2].token.address); + + let protocol_bonus_1 = protocol_fee_after_1 - protocol_fee_before_1; + let protocol_bonus_2 = protocol_fee_after_2 - protocol_fee_before_2; + + LiquidatorProtocolBonus { + liquidator: (liquidator_bonus_1, liquidator_bonus_2), + protocol: (protocol_bonus_1, protocol_bonus_2), + } + }; + + let bonus_without_protocol_fee = liquidate(0); + + for fee in [1, 10, 100, 1000, 5000, 9000, 10000] { + let bonus_with_protocol_fee = liquidate(fee); + + let expected_protocol_fee_1 = + bonus_without_protocol_fee.liquidator.0 - bonus_with_protocol_fee.liquidator.0; + let expected_protocol_fee_2 = + bonus_without_protocol_fee.liquidator.1 - bonus_with_protocol_fee.liquidator.1; + + assert_eq!( + bonus_without_protocol_fee.liquidator.0 - bonus_with_protocol_fee.liquidator.0, + FixedI128::from_percentage(fee) + .unwrap() + .mul_int(bonus_without_protocol_fee.liquidator.0) + .unwrap() + ); + assert_eq!( + bonus_without_protocol_fee.liquidator.1 - bonus_with_protocol_fee.liquidator.1, + FixedI128::from_percentage(fee) + .unwrap() + .mul_int(bonus_without_protocol_fee.liquidator.1) + .unwrap() + ); + + assert_eq!(expected_protocol_fee_1, bonus_with_protocol_fee.protocol.0); + assert_eq!(expected_protocol_fee_2, bonus_with_protocol_fee.protocol.1); + } +} diff --git a/contracts/pool/src/tests/mod.rs b/contracts/pool/src/tests/mod.rs index 2706bb5a..dce41bd2 100644 --- a/contracts/pool/src/tests/mod.rs +++ b/contracts/pool/src/tests/mod.rs @@ -7,24 +7,21 @@ pub mod configure_as_collateral; pub mod debt_coeff; pub mod deposit; pub mod enable_borrowing_on_reserve; +pub mod finalize_transfer; pub mod flash_loan; -pub mod flash_loan_fee; pub mod get_reserve; pub mod init_reserve; -pub mod ir_params; pub mod liquidate; pub mod paused; +pub mod protocol_fee; pub mod rates; pub mod repay; pub mod rounding; pub mod set_as_collateral; -pub mod set_base_asset; -pub mod set_flash_loan_fee; -pub mod set_ir_params; pub mod set_pause; +pub mod set_pool_configuration; pub mod set_price_feeds; pub mod set_reserve_status; -pub mod set_reserve_timestamp_window; pub mod soroban_map; pub mod stoken_underlying_balance; mod sut; diff --git a/contracts/pool/src/tests/paused.rs b/contracts/pool/src/tests/paused.rs index 0dd652b3..38354336 100644 --- a/contracts/pool/src/tests/paused.rs +++ b/contracts/pool/src/tests/paused.rs @@ -1,15 +1,28 @@ use crate::{tests::sut::init_pool, *}; #[test] -fn should_return_paused_flag() { +fn should_return_pause_info() { let env = Env::default(); env.mock_all_auths(); let sut = init_pool(&env, false); + let prev_pause_info = sut.pool.pause_info(); sut.pool.set_pause(&false); - assert!(!sut.pool.paused()); + let next_pause_info = sut.pool.pause_info(); + assert!(!next_pause_info.paused); + assert_eq!( + prev_pause_info.grace_period_secs, + next_pause_info.grace_period_secs + ); + assert_eq!(prev_pause_info.unpaused_at, next_pause_info.unpaused_at); sut.pool.set_pause(&true); - assert!(sut.pool.paused()); + let next_pause_info = sut.pool.pause_info(); + assert!(next_pause_info.paused); + assert_eq!( + prev_pause_info.grace_period_secs, + next_pause_info.grace_period_secs + ); + assert_eq!(prev_pause_info.unpaused_at, next_pause_info.unpaused_at); } diff --git a/contracts/pool/src/tests/protocol_fee.rs b/contracts/pool/src/tests/protocol_fee.rs new file mode 100644 index 00000000..7e4f2fe2 --- /dev/null +++ b/contracts/pool/src/tests/protocol_fee.rs @@ -0,0 +1,211 @@ +#![cfg(test)] +extern crate std; + +use pool_interface::types::pool_config::PoolConfig; +use price_feed_interface::types::{asset::Asset, price_data::PriceData}; +use soroban_sdk::{ + testutils::{Address as _, AuthorizedFunction, AuthorizedInvocation}, + vec, Address, Env, IntoVal, Symbol, +}; + +use crate::tests::sut::{fill_pool_six, set_time}; + +use super::sut::{create_token_contract, fill_pool, init_pool, Sut, DAY}; + +fn generate_protocol_fee(env: &Env, sut: &Sut, debt_token: &Address, borrower: &Address) -> i128 { + set_time(env, sut, 2 * DAY, false); + + let protocol_fee_before = sut.pool.protocol_fee(debt_token); + + sut.pool.repay(&borrower, debt_token, &i128::MAX); + + let protocol_fee_after = sut.pool.protocol_fee(debt_token); + + protocol_fee_after - protocol_fee_before +} + +#[test] +fn should_read_protocol_fee() { + let env = Env::default(); + env.mock_all_auths(); + let sut = init_pool(&env, false); + let (_, borrower, debt_config) = fill_pool(&env, &sut, true); + let expected_fee = generate_protocol_fee(&env, &sut, &debt_config.token.address, &borrower); + let actual_fee = sut.pool.protocol_fee(&debt_config.token.address); + + assert_eq!(expected_fee, actual_fee); +} + +#[test] +fn should_require_admin() { + let env = Env::default(); + env.mock_all_auths(); + + let sut = init_pool(&env, false); + let (_, borrower, debt_config) = fill_pool(&env, &sut, true); + let _ = generate_protocol_fee(&env, &sut, &debt_config.token.address, &borrower); + let recipient = Address::generate(&env); + + sut.pool + .claim_protocol_fee(&debt_config.token.address, &recipient); + + assert_eq!( + env.auths(), + [( + sut.pool_admin.clone(), + AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + sut.pool.address.clone(), + Symbol::new(&env, "claim_protocol_fee"), + vec![ + &env, + debt_config.token.address.into_val(&env), + recipient.into_val(&env) + ] + )), + sub_invocations: std::vec![] + } + )] + ); +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #1)")] +fn should_fail_if_reserve_not_exists() { + let env = Env::default(); + env.mock_all_auths(); + + let sut = init_pool(&env, false); + let (_, borrower, debt_config) = fill_pool(&env, &sut, true); + let _ = generate_protocol_fee(&env, &sut, &debt_config.token.address, &borrower); + let recipient = Address::generate(&env); + let (token, _) = create_token_contract(&env, &sut.pool_admin); + + sut.pool.claim_protocol_fee(&token.address, &recipient); +} + +#[test] +fn should_claim_fee() { + let env = Env::default(); + env.mock_all_auths(); + + let sut = init_pool(&env, false); + let (_, borrower, debt_config) = fill_pool(&env, &sut, true); + let expected_fee = generate_protocol_fee(&env, &sut, &debt_config.token.address, &borrower); + let recipient = Address::generate(&env); + + let recipient_balance_before = debt_config.token.balance(&recipient); + let recipient_stoken_balance_before = debt_config.s_token().balance(&recipient); + let s_token_balance_before = debt_config.token.balance(&debt_config.s_token().address); + let s_token_balance_internal_before = sut + .pool + .token_balance(&debt_config.token.address, &debt_config.s_token().address); + let recipient_internal_balance_before = sut + .pool + .token_balance(&recipient, &debt_config.token.address); + let fee_before = sut.pool.protocol_fee(&debt_config.token.address); + + sut.pool + .claim_protocol_fee(&debt_config.token.address, &recipient); + + let recipient_balance_after = debt_config.token.balance(&recipient); + let recipient_stoken_balance_after = debt_config.s_token().balance(&recipient); + let s_token_balance_after = debt_config.token.balance(&debt_config.s_token().address); + let s_token_balance_internal_after = sut + .pool + .token_balance(&debt_config.token.address, &debt_config.s_token().address); + let recipient_internal_balance_after = sut + .pool + .token_balance(&recipient, &debt_config.token.address); + let fee_after = sut.pool.protocol_fee(&debt_config.token.address); + + assert_eq!( + recipient_balance_after - recipient_balance_before, + expected_fee + ); + assert_eq!(s_token_balance_before - s_token_balance_after, expected_fee); + assert_eq!( + recipient_stoken_balance_before, + recipient_stoken_balance_after + ); + + assert_eq!( + s_token_balance_internal_before, + s_token_balance_internal_after + ); + assert_eq!( + recipient_internal_balance_before, + recipient_internal_balance_after + ); + + assert_eq!(fee_before - fee_after, expected_fee); + assert_eq!(fee_after, 0); +} + +#[test] +fn should_claim_fee_rwa() { + let env = Env::default(); + env.mock_all_auths(); + + let sut = init_pool(&env, false); + let (liquidator, borrower) = fill_pool_six(&env, &sut); + let recipient = Address::generate(&env); + let collat_1_token = sut.reserves[0].token.address.clone(); + let rwa_token = sut.rwa_config().token.address.clone(); + let debt_token = sut.reserves[1].token.address.clone(); + + sut.rwa_config() + .token_admin + .mint(&borrower, &100_000_000_000); + + sut.pool.set_pool_configuration(&PoolConfig { + base_asset_address: sut.reserves[0].token.address.clone(), + base_asset_decimals: sut.reserves[0].token.decimals(), + flash_loan_fee: 5, + initial_health: 2_500, + timestamp_window: 20, + grace_period: 1, + user_assets_limit: 4, + min_collat_amount: 0, + min_debt_amount: 0, + liquidation_protocol_fee: 100, + ir_alpha: 143, + ir_initial_rate: 200, + ir_max_rate: 50_000, + ir_scaling_coeff: 9_000, + }); + + set_time(&env, &sut, 10_000, false); + + sut.pool + .deposit(&borrower, &collat_1_token, &10_000_000_000); + sut.pool.deposit(&borrower, &rwa_token, &100_000_000_000); + sut.pool.borrow(&borrower, &debt_token, &800_000_000_000); + + sut.price_feed.init( + &Asset::Stellar(debt_token), + &vec![ + &env, + PriceData { + price: (18 * 10i128.pow(15)), + timestamp: 10_000, + }, + ], + ); + + sut.pool.liquidate(&liquidator, &borrower); + + let recipient_rwa_before = sut.rwa_config().token.balance(&recipient); + let pool_rwa_before = sut.rwa_config().token.balance(&sut.pool.address); + let fee_before = sut.pool.protocol_fee(&rwa_token); + + sut.pool.claim_protocol_fee(&rwa_token, &recipient); + + let recipient_rwa_after = sut.rwa_config().token.balance(&recipient); + let pool_rwa_after = sut.rwa_config().token.balance(&sut.pool.address); + let fee_after = sut.pool.protocol_fee(&rwa_token); + + assert_eq!(recipient_rwa_after - recipient_rwa_before, fee_before); + assert_eq!(pool_rwa_before - pool_rwa_after, fee_before); + assert_eq!(fee_after, 0); +} diff --git a/contracts/pool/src/tests/rates.rs b/contracts/pool/src/tests/rates.rs index 679899e5..f453f272 100644 --- a/contracts/pool/src/tests/rates.rs +++ b/contracts/pool/src/tests/rates.rs @@ -1,28 +1,39 @@ use crate::methods::utils::rate::{calc_accrued_rates, calc_interest_rate, calc_next_accrued_rate}; -use crate::tests::sut::{init_pool, DAY}; +use crate::tests::sut::{init_pool, set_time, DAY}; use common::FixedI128; -use pool_interface::types::{ - ir_params::IRParams, reserve_data::ReserveData, reserve_type::ReserveType, -}; -use soroban_sdk::testutils::{Address as _, Ledger}; +use pool_interface::types::pool_config::PoolConfig; +use pool_interface::types::reserve_data::ReserveData; +use pool_interface::types::reserve_type::ReserveType; +use soroban_sdk::testutils::Address as _; use soroban_sdk::{Address, Env}; -pub fn get_default_ir_params() -> IRParams { - IRParams { - alpha: 143, //1.43 - initial_rate: 200, //2% - max_rate: 50000, //500% - scaling_coeff: 9000, //90% +pub fn get_default_ir_params(env: &Env) -> PoolConfig { + PoolConfig { + base_asset_address: Address::generate(env), + base_asset_decimals: 7, + flash_loan_fee: 5, + initial_health: 2_500, + grace_period: 1, + timestamp_window: 20, + user_assets_limit: 4, + min_collat_amount: 0, + min_debt_amount: 0, + liquidation_protocol_fee: 0, + ir_alpha: 143, //1.43 + ir_initial_rate: 200, //2% + ir_max_rate: 50000, //500% + ir_scaling_coeff: 9000, //90% } } #[test] fn should_return_zero_when_utilization_is_zero() { + let env = Env::default(); let total_collateral = 1000; let total_debt = 0; - let ir_params = get_default_ir_params(); + let pool_config = get_default_ir_params(&env); - let ir = calc_interest_rate(total_collateral, total_debt, &ir_params).unwrap(); + let ir = calc_interest_rate(total_collateral, total_debt, &pool_config).unwrap(); assert_eq!(ir, FixedI128::ZERO); } @@ -31,44 +42,47 @@ fn should_return_zero_when_utilization_is_zero() { fn should_return_max_rate_when_utilization_is_gte_one() { let total_collateral = 1; let total_debt = 1; - let ir_params = get_default_ir_params(); + let env = Env::default(); + let pool_config = get_default_ir_params(&env); - let ir = calc_interest_rate(total_collateral, total_debt, &ir_params); + let ir = calc_interest_rate(total_collateral, total_debt, &pool_config); - assert_eq!(ir, FixedI128::from_percentage(ir_params.max_rate)); + assert_eq!(ir, FixedI128::from_percentage(pool_config.ir_max_rate)); } #[test] fn should_return_none_when_collateral_or_debt_is_negative() { let total_collateral = -1; let total_debt = 1; - let ir_params = get_default_ir_params(); + let env = Env::default(); + let pool_config = get_default_ir_params(&env); - let ir = calc_interest_rate(total_collateral, total_debt, &ir_params); + let ir = calc_interest_rate(total_collateral, total_debt, &pool_config); assert!(ir.is_none()); let total_collateral = 1; let total_debt = -1; - let ir = calc_interest_rate(total_collateral, total_debt, &ir_params); + let ir = calc_interest_rate(total_collateral, total_debt, &pool_config); assert!(ir.is_none()); let total_collateral = -1; let total_debt = -1; - let ir = calc_interest_rate(total_collateral, total_debt, &ir_params); + let ir = calc_interest_rate(total_collateral, total_debt, &pool_config); assert!(ir.is_none()); } #[test] fn should_calc_interest_rate() { - let ir_params = get_default_ir_params(); + let env = Env::default(); + let pool_config = get_default_ir_params(&env); //utilization = 0.2, ir ~ 0.027517810, ir = 0.02/(1-0.2)^1.43 = 0,0275176482 let total_debt = 20; let total_collateral: i128 = 100; - let ir = calc_interest_rate(total_collateral, total_debt, &ir_params).unwrap(); + let ir = calc_interest_rate(total_collateral, total_debt, &pool_config).unwrap(); assert_eq!( ir, FixedI128::from_rational(27517810, 1_000_000_000).unwrap() @@ -77,7 +91,7 @@ fn should_calc_interest_rate() { //utilization = 0.5, ir ~ 0.053966913, ir = 0.02/(1 - 0.5)^1.43 = 0,0538893431 let total_debt = 50; let total_collateral: i128 = 100; - let ir = calc_interest_rate(total_collateral, total_debt, &ir_params).unwrap(); + let ir = calc_interest_rate(total_collateral, total_debt, &pool_config).unwrap(); assert_eq!( ir, FixedI128::from_rational(53966913, 1_000_000_000).unwrap() @@ -86,7 +100,7 @@ fn should_calc_interest_rate() { //utilization = 0.75, ir ~ 0.145205089, ir = 0.02/(1-0.75)^1.43 = 0,1452030649 let total_debt = 75; let total_collateral: i128 = 100; - let ir = calc_interest_rate(total_collateral, total_debt, &ir_params).unwrap(); + let ir = calc_interest_rate(total_collateral, total_debt, &pool_config).unwrap(); assert_eq!( ir, FixedI128::from_rational(145205089, 1_000_000_000).unwrap() @@ -95,7 +109,7 @@ fn should_calc_interest_rate() { // utlization = 0.8, ir ~ 0.199799636, ir = 0.02/(1-0.8)^1.43 = 0,1997823429 let total_debt: i128 = 80; let total_collateral: i128 = 100; - let ir = calc_interest_rate(total_collateral, total_debt, &ir_params).unwrap(); + let ir = calc_interest_rate(total_collateral, total_debt, &pool_config).unwrap(); assert_eq!( ir, FixedI128::from_rational(199799636, 1_000_000_000).unwrap() @@ -104,7 +118,7 @@ fn should_calc_interest_rate() { // utilization = 0.9, ir ~ 540574625, ir = 0.02/(1-0.9)^1.43 = 0,5383069608 let total_debt: i128 = 90; let total_collateral: i128 = 100; - let ir = calc_interest_rate(total_collateral, total_debt, &ir_params).unwrap(); + let ir = calc_interest_rate(total_collateral, total_debt, &pool_config).unwrap(); assert_eq!( ir, FixedI128::from_rational(540574625, 1_000_000_000).unwrap() @@ -113,7 +127,7 @@ fn should_calc_interest_rate() { //utilization = 0.95, ir - 1.524769809, ir = 0.02/(1-0.9)^1.43 = 1,117567356 let total_debt: i128 = 95; let total_collateral: i128 = 100; - let ir = calc_interest_rate(total_collateral, total_debt, &ir_params).unwrap(); + let ir = calc_interest_rate(total_collateral, total_debt, &pool_config).unwrap(); assert_eq!( ir, FixedI128::from_rational(1524769809, 1_000_000_000).unwrap() @@ -122,7 +136,7 @@ fn should_calc_interest_rate() { //utilization = 0.99, ir - 5.0, ir = 0.02/(1-0.9)^1.43 = 14.4887192 let total_debt: i128 = 99; let total_collateral: i128 = 100; - let ir = calc_interest_rate(total_collateral, total_debt, &ir_params).unwrap(); + let ir = calc_interest_rate(total_collateral, total_debt, &pool_config).unwrap(); assert_eq!( ir, FixedI128::from_rational(5_000_000_000u64, 1_000_000_000).unwrap() @@ -149,10 +163,17 @@ fn should_calc_borrower_and_lender_rates() { let input = ReserveType::Fungible(Address::generate(env), Address::generate(env)); let reserve_data = ReserveData::new(env, input); - let ir_params = get_default_ir_params(); + let env = Env::default(); + let pool_config = get_default_ir_params(&env); - let accrued_rates = - calc_accrued_rates(total_collateral, total_debt, DAY, ir_params, &reserve_data).unwrap(); + let accrued_rates = calc_accrued_rates( + total_collateral, + total_debt, + DAY, + &pool_config, + &reserve_data, + ) + .unwrap(); //debt_ir = 0,027517810 assert_eq!(accrued_rates.borrower_ir.into_inner(), 27517810); @@ -173,10 +194,16 @@ fn should_fail_when_collateral_is_zero() { let input = ReserveType::Fungible(Address::generate(env), Address::generate(env)); let reserve_data = ReserveData::new(env, input); - let ir_params = get_default_ir_params(); - - let mb_accrued_rates = - calc_accrued_rates(total_collateral, total_debt, DAY, ir_params, &reserve_data); + let env = Env::default(); + let pool_config = get_default_ir_params(&env); + + let mb_accrued_rates = calc_accrued_rates( + total_collateral, + total_debt, + DAY, + &pool_config, + &reserve_data, + ); assert!(mb_accrued_rates.is_none()); } @@ -219,20 +246,19 @@ fn should_update_rates_over_time() { } // shift time to - env.ledger().with_mut(|li| li.timestamp = DAY); + let elapsed_time = DAY; + set_time(&env, &sut, elapsed_time, true); //second deposit by lender of debt asset sut.pool.deposit(&lender, &debt_asset_1, &100_000_000); let updated = sut.pool.get_reserve(&debt_asset_1).unwrap(); - let ir_params = sut.pool.ir_params().unwrap(); - let debt_ir = calc_interest_rate(200_000_000, 40_000_000, &ir_params).unwrap(); + let pool_config = sut.pool.pool_configuration(); + let debt_ir = calc_interest_rate(200_000_000, 40_000_000, &pool_config).unwrap(); let lender_ir = debt_ir - .checked_mul(FixedI128::from_percentage(ir_params.scaling_coeff).unwrap()) + .checked_mul(FixedI128::from_percentage(pool_config.ir_scaling_coeff).unwrap()) .unwrap(); - let elapsed_time = env.ledger().timestamp(); - let coll_ar = calc_next_accrued_rate(FixedI128::ONE, lender_ir, elapsed_time) .unwrap() .into_inner(); diff --git a/contracts/pool/src/tests/repay.rs b/contracts/pool/src/tests/repay.rs index 144eb64d..8e39a605 100644 --- a/contracts/pool/src/tests/repay.rs +++ b/contracts/pool/src/tests/repay.rs @@ -1,7 +1,8 @@ use crate::tests::sut::{fill_pool, init_pool, DAY}; use crate::*; -use soroban_sdk::testutils::{Events, Ledger}; +use soroban_sdk::testutils::Events; use soroban_sdk::{vec, IntoVal, Symbol}; +use tests::sut::set_time; #[test] fn should_partially_repay() { @@ -13,12 +14,13 @@ fn should_partially_repay() { let debt_token = &debt_config.token.address; let stoken_token = &debt_config.s_token().address; - env.ledger().with_mut(|li| li.timestamp = 2 * DAY); - let treasury_address = sut.pool.treasury().clone(); + set_time(&env, &sut, 2 * DAY, false); - let stoken_underlying_balance = sut.pool.stoken_underlying_balance(&stoken_token); + let stoken_underlying_balance = sut + .pool + .token_balance(&debt_config.token.address, &stoken_token); let user_balance = debt_config.token.balance(&borrower); - let treasury_balance = debt_config.token.balance(&treasury_address); + let treasury_balance = sut.pool.protocol_fee(&debt_config.token.address); let user_debt_balance = debt_config.debt_token().balance(&borrower); assert_eq!(stoken_underlying_balance, 60_000_000); @@ -28,14 +30,16 @@ fn should_partially_repay() { sut.pool.repay(&borrower, &debt_token, &20_000_000i128); - let stoken_underlying_balance = sut.pool.stoken_underlying_balance(&stoken_token); + let stoken_underlying_balance = sut + .pool + .token_balance(&debt_config.token.address, &stoken_token); let user_balance = debt_config.token.balance(&borrower); - let treasury_balance = debt_config.token.balance(&treasury_address); + let treasury_balance = sut.pool.protocol_fee(&debt_config.token.address); let user_debt_balance = debt_config.debt_token().balance(&borrower); - assert_eq!(stoken_underlying_balance, 79_997_089); + assert_eq!(stoken_underlying_balance, 79_997_090); assert_eq!(user_balance, 1_020_000_000); - assert_eq!(treasury_balance, 2_911); + assert_eq!(treasury_balance, 2_910); assert_eq!(user_debt_balance, 20_004_549); } @@ -49,12 +53,13 @@ fn should_fully_repay() { let debt_token = &debt_config.token.address; let stoken_token = &debt_config.s_token().address; - env.ledger().with_mut(|li| li.timestamp = 2 * DAY); - let treasury_address = sut.pool.treasury().clone(); + set_time(&env, &sut, 2 * DAY, false); - let stoken_underlying_balance = sut.pool.stoken_underlying_balance(&stoken_token); + let stoken_underlying_balance = sut + .pool + .token_balance(&debt_config.token.address, &stoken_token); let user_balance = debt_config.token.balance(&borrower); - let treasury_balance = debt_config.token.balance(&treasury_address); + let treasury_balance = sut.pool.protocol_fee(&debt_config.token.address); let user_debt_balance = debt_config.debt_token().balance(&borrower); assert_eq!(stoken_underlying_balance, 60_000_000); @@ -64,14 +69,16 @@ fn should_fully_repay() { sut.pool.repay(&borrower, &debt_token, &i128::MAX); - let stoken_underlying_balance = sut.pool.stoken_underlying_balance(&stoken_token); + let stoken_underlying_balance = sut + .pool + .token_balance(&debt_config.token.address, &stoken_token); let user_balance = debt_config.token.balance(&borrower); - let treasury_balance = debt_config.token.balance(&treasury_address); + let treasury_balance = sut.pool.protocol_fee(&debt_config.token.address); let user_debt_balance = debt_config.debt_token().balance(&borrower); assert_eq!(stoken_underlying_balance, 100_003_275); - assert_eq!(user_balance, 999_990_902); - assert_eq!(treasury_balance, 5_823); + assert_eq!(user_balance, 999_990_903); + assert_eq!(treasury_balance, 5_822); assert_eq!(user_debt_balance, 0); } @@ -90,6 +97,7 @@ fn should_change_user_config() { let reserve = sut.pool.get_reserve(&debt_config.token.address).unwrap(); assert_eq!(user_config.is_borrowing(&env, reserve.get_id()), false); + assert_eq!(user_config.total_assets(), 1); } #[test] @@ -100,7 +108,7 @@ fn should_affect_coeffs() { let sut = init_pool(&env, false); let (_, borrower, debt_config) = fill_pool(&env, &sut, true); - env.ledger().with_mut(|li| li.timestamp = 2 * DAY); + set_time(&env, &sut, 2 * DAY, false); let collat_coeff_prev = sut.pool.collat_coeff(&debt_config.token.address); let debt_coeff_prev = sut.pool.debt_coeff(&debt_config.token.address); @@ -108,7 +116,7 @@ fn should_affect_coeffs() { sut.pool .repay(&borrower, &debt_config.token.address, &20_000_000); - env.ledger().with_mut(|li| li.timestamp = 3 * DAY); + set_time(&env, &sut, 3 * DAY, false); let collat_coeff = sut.pool.collat_coeff(&debt_config.token.address); let debt_coeff = sut.pool.debt_coeff(&debt_config.token.address); @@ -127,12 +135,12 @@ fn should_affect_account_data() { let account_position_prev = sut.pool.account_position(&borrower); - env.ledger().with_mut(|li| li.timestamp = 2 * DAY); + set_time(&env, &sut, 2 * DAY, false); sut.pool .repay(&borrower, &debt_config.token.address, &10_000_000); - env.ledger().with_mut(|li| li.timestamp = 3 * DAY); + set_time(&env, &sut, 3 * DAY, false); let account_position = sut.pool.account_position(&borrower); @@ -163,7 +171,7 @@ fn should_emit_events() { let (_, borrower, debt_config) = fill_pool(&env, &sut, true); let debt_token = &debt_config.token.address; - env.ledger().with_mut(|li| li.timestamp = 2 * DAY); + set_time(&env, &sut, 2 * DAY, false); sut.pool.repay(&borrower, &debt_token.clone(), &i128::MAX); @@ -176,14 +184,14 @@ fn should_emit_events() { ( sut.pool.address.clone(), (Symbol::new(&env, "repay"), borrower.clone()).into_val(&env), - (debt_token, 40_009_098i128).into_val(&env) + (debt_token, 40_009_097i128).into_val(&env) ), ] ); } #[test] -#[should_panic(expected = "HostError: Error(Contract, #110)")] +#[should_panic(expected = "HostError: Error(Contract, #105)")] fn should_fail_when_repay_rwa() { let env = Env::default(); env.mock_all_auths(); @@ -194,3 +202,97 @@ fn should_fail_when_repay_rwa() { sut.pool.repay(&borrower, &rwa_address, &10_000_000); } + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #3)")] +fn should_fail_when_debt_lt_min_position_amount() { + let env = Env::default(); + env.mock_all_auths(); + + let sut = init_pool(&env, false); + let (_, borrower, debt_config) = fill_pool(&env, &sut, true); + let debt_token = &debt_config.token.address; + + sut.pool.set_pool_configuration(&PoolConfig { + base_asset_address: sut.reserves[0].token.address.clone(), + base_asset_decimals: sut.reserves[0].token.decimals(), + flash_loan_fee: 5, + initial_health: 0, + timestamp_window: 20, + grace_period: 1, + user_assets_limit: 2, + min_collat_amount: 0, + min_debt_amount: 300_000, + liquidation_protocol_fee: 0, + ir_alpha: 143, + ir_initial_rate: 200, + ir_max_rate: 50_000, + ir_scaling_coeff: 9_000, + }); + + set_time(&env, &sut, 2 * DAY, false); + + sut.pool.repay(&borrower, &debt_token, &20_000_000i128); +} + +#[test] +fn should_not_fail_in_grace_period() { + let env = Env::default(); + env.mock_all_auths(); + + let sut = init_pool(&env, false); + let (_, borrower, debt_config) = fill_pool(&env, &sut, true); + let debt_token = &debt_config.token.address; + let stoken_token = &debt_config.s_token().address; + + set_time(&env, &sut, 2 * DAY, false); + + let stoken_underlying_balance = sut + .pool + .token_balance(&debt_config.token.address, &stoken_token); + let user_balance = debt_config.token.balance(&borrower); + let treasury_balance = sut.pool.protocol_fee(&debt_config.token.address); + let user_debt_balance = debt_config.debt_token().balance(&borrower); + + assert_eq!(stoken_underlying_balance, 60_000_000); + assert_eq!(user_balance, 1_040_000_000); + assert_eq!(treasury_balance, 0); + assert_eq!(user_debt_balance, 40_000_001); + + sut.pool.set_pause(&true); + sut.pool.set_pause(&false); + + sut.pool.repay(&borrower, &debt_token, &i128::MAX); + + let stoken_underlying_balance = sut + .pool + .token_balance(&debt_config.token.address, &stoken_token); + let user_balance = debt_config.token.balance(&borrower); + let treasury_balance = sut.pool.protocol_fee(&debt_config.token.address); + let user_debt_balance = debt_config.debt_token().balance(&borrower); + + assert_eq!(stoken_underlying_balance, 100_003_275); + assert_eq!(user_balance, 999_990_903); + assert_eq!(treasury_balance, 5_822); + assert_eq!(user_debt_balance, 0); +} + +#[test] +fn repay_should_pay_protocol_fee() { + let env = Env::default(); + env.mock_all_auths(); + + let sut = init_pool(&env, false); + let (_, borrower, debt_config) = fill_pool(&env, &sut, true); + let debt_token = &debt_config.token.address; + + set_time(&env, &sut, 2 * DAY, false); + + let protocol_fee_before = sut.pool.protocol_fee(debt_token); + + sut.pool.repay(&borrower, debt_token, &i128::MAX); + + let protocol_fee_after = sut.pool.protocol_fee(debt_token); + + assert_eq!(protocol_fee_after - protocol_fee_before, 5822); +} diff --git a/contracts/pool/src/tests/set_as_collateral.rs b/contracts/pool/src/tests/set_as_collateral.rs index cb0ecc29..f3e9a805 100644 --- a/contracts/pool/src/tests/set_as_collateral.rs +++ b/contracts/pool/src/tests/set_as_collateral.rs @@ -65,7 +65,7 @@ fn should_disable_collateral_when_deposited() { } #[test] -#[should_panic(expected = "HostError: Error(Contract, #204)")] +#[should_panic(expected = "HostError: Error(Contract, #201)")] fn should_fail_when_has_debt() { let env = Env::default(); env.mock_all_auths(); @@ -77,13 +77,30 @@ fn should_fail_when_has_debt() { } #[test] -#[should_panic(expected = "HostError: Error(Contract, #302)")] -fn should_fail_when_bad_position() { +#[should_panic(expected = "HostError: Error(Contract, #3)")] +fn should_fail_when_npv_fails_bellow_initial_health() { let env = Env::default(); env.mock_all_auths(); let (sut, user, (collat_reserve_index, _), (collat_token, _)) = init_with_debt(&env); + sut.pool.set_pool_configuration(&PoolConfig { + base_asset_address: sut.reserves[0].token.address.clone(), + base_asset_decimals: sut.reserves[0].token.decimals(), + flash_loan_fee: 5, + initial_health: 2_500, + timestamp_window: 20, + grace_period: 1, + user_assets_limit: 4, + min_collat_amount: 0, + min_debt_amount: 0, + liquidation_protocol_fee: 0, + ir_alpha: 143, + ir_initial_rate: 200, + ir_max_rate: 50_000, + ir_scaling_coeff: 9_000, + }); + sut.pool .set_as_collateral(&user, &collat_token.clone(), &false); @@ -132,6 +149,71 @@ fn should_emit_events() { ); } +#[test] +#[should_panic(expected = "HostError: Error(Contract, #4)")] +fn rwa_fail_when_exceed_assets_limit() { + let env = Env::default(); + env.mock_all_auths(); + let (sut, user, (_, _), (_, _)) = init_with_debt(&env); + deposit(&sut.pool, &sut.reserves[0].token_admin, &user); + deposit(&sut.pool, &sut.reserves[2].token_admin, &user); + + assert_eq!( + sut.pool + .set_as_collateral(&user, &&sut.reserves[0].token.address, &false), + () + ); + + sut.pool.set_pool_configuration(&PoolConfig { + base_asset_address: sut.reserves[0].token.address.clone(), + base_asset_decimals: sut.reserves[0].token.decimals(), + flash_loan_fee: 5, + initial_health: 0, + timestamp_window: 20, + grace_period: 1, + user_assets_limit: 2, + min_collat_amount: 0, + min_debt_amount: 300_000, + liquidation_protocol_fee: 0, + ir_alpha: 143, + ir_initial_rate: 200, + ir_max_rate: 50_000, + ir_scaling_coeff: 9_000, + }); + + sut.pool + .set_as_collateral(&user, &&sut.reserves[0].token.address, &true); +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #3)")] +fn should_fail_when_collat_lt_min_position_amount() { + let env = Env::default(); + env.mock_all_auths(); + let (sut, user, (_, _), (collat_token, _)) = init_with_debt(&env); + deposit(&sut.pool, &sut.reserves[0].token_admin, &user); + deposit(&sut.pool, &sut.reserves[2].token_admin, &user); + + sut.pool.set_pool_configuration(&PoolConfig { + base_asset_address: sut.reserves[0].token.address.clone(), + base_asset_decimals: sut.reserves[0].token.decimals(), + flash_loan_fee: 5, + initial_health: 0, + timestamp_window: 20, + grace_period: 1, + user_assets_limit: 2, + min_collat_amount: 7_000_000, + min_debt_amount: 0, + liquidation_protocol_fee: 0, + ir_alpha: 143, + ir_initial_rate: 200, + ir_max_rate: 50_000, + ir_scaling_coeff: 9_000, + }); + + assert_eq!(sut.pool.set_as_collateral(&user, &collat_token, &false), ()); +} + /// Init for set_as_collateral tests. /// Returns Sut, user address, reserve index and token address fn init(env: &Env) -> (Sut, Address, u8, Address) { diff --git a/contracts/pool/src/tests/set_base_asset.rs b/contracts/pool/src/tests/set_base_asset.rs deleted file mode 100644 index 31bf2cda..00000000 --- a/contracts/pool/src/tests/set_base_asset.rs +++ /dev/null @@ -1,54 +0,0 @@ -#![cfg(test)] -extern crate std; - -use soroban_sdk::testutils::{AuthorizedFunction, AuthorizedInvocation}; -use soroban_sdk::{IntoVal, Symbol}; - -use crate::tests::sut::init_pool; -use crate::*; - -#[test] -fn should_require_admin() { - let env = Env::default(); - env.mock_all_auths(); - - let sut = init_pool(&env, false); - let underlying_token = sut.reserves[0].token.address.clone(); - sut.pool.set_base_asset(&underlying_token, &9u32); - - assert_eq!( - env.auths(), - [( - sut.pool_admin.clone(), - AuthorizedInvocation { - function: AuthorizedFunction::Contract(( - sut.pool.address.clone(), - Symbol::new(&env, "set_base_asset"), - (underlying_token, 9u32).into_val(&env) - )), - sub_invocations: std::vec![] - } - )] - ); -} - -#[test] -fn should_set_base_asset() { - let env = Env::default(); - env.mock_all_auths(); - - let sut = init_pool(&env, false); - let underlying_token_1 = sut.reserves[0].token.address.clone(); - let underlying_token_2 = sut.reserves[1].token.address.clone(); - - let base_asset_init = sut.pool.base_asset(); - - assert_eq!(base_asset_init.address, underlying_token_1); - assert_eq!(base_asset_init.decimals, 7u32); - - sut.pool.set_base_asset(&underlying_token_2, &9u32); - let base_asset_after = sut.pool.base_asset(); - - assert_eq!(base_asset_after.address, underlying_token_2); - assert_eq!(base_asset_after.decimals, 9u32); -} diff --git a/contracts/pool/src/tests/set_flash_loan_fee.rs b/contracts/pool/src/tests/set_flash_loan_fee.rs deleted file mode 100644 index 6fc6c47a..00000000 --- a/contracts/pool/src/tests/set_flash_loan_fee.rs +++ /dev/null @@ -1,44 +0,0 @@ -#![cfg(test)] -extern crate std; - -use crate::{tests::sut::init_pool, *}; -use soroban_sdk::{ - testutils::{AuthorizedFunction, AuthorizedInvocation}, - vec, IntoVal, Symbol, -}; - -#[test] -fn should_require_admin() { - let env = Env::default(); - env.mock_all_auths(); - - let sut = init_pool(&env, false); - - sut.pool.set_flash_loan_fee(&10); - - assert_eq!( - env.auths(), - [( - sut.pool_admin, - AuthorizedInvocation { - function: AuthorizedFunction::Contract(( - sut.pool.address.clone(), - Symbol::new(&env, "set_flash_loan_fee"), - vec![&env, 10u32.into_val(&env)] - )), - sub_invocations: std::vec![] - } - )] - ); -} - -#[test] -fn should_set_flash_loan_fee() { - let env = Env::default(); - env.mock_all_auths(); - - let sut = init_pool(&env, false); - - sut.pool.set_flash_loan_fee(&15); - assert_eq!(sut.pool.flash_loan_fee(), 15); -} diff --git a/contracts/pool/src/tests/set_ir_params.rs b/contracts/pool/src/tests/set_ir_params.rs deleted file mode 100644 index d5c6ec28..00000000 --- a/contracts/pool/src/tests/set_ir_params.rs +++ /dev/null @@ -1,117 +0,0 @@ -#![cfg(test)] -extern crate std; - -use crate::tests::sut::init_pool; -use crate::*; -use soroban_sdk::testutils::{AuthorizedFunction, AuthorizedInvocation}; -use soroban_sdk::{vec, IntoVal, Symbol}; - -#[test] -fn should_require_admin() { - let env = Env::default(); - env.mock_all_auths(); - - let sut = init_pool(&env, false); - - let ir_params_input = IRParams { - alpha: 144, - initial_rate: 201, - max_rate: 50_001, - scaling_coeff: 9_001, - }; - - sut.pool.set_ir_params(&ir_params_input.clone()); - - assert_eq!( - env.auths(), - [( - sut.pool_admin, - AuthorizedInvocation { - function: AuthorizedFunction::Contract(( - sut.pool.address.clone(), - Symbol::new(&env, "set_ir_params"), - vec![&env, ir_params_input.into_val(&env)] - )), - sub_invocations: std::vec![] - } - )] - ); -} - -#[test] -#[should_panic(expected = "HostError: Error(Contract, #401)")] -fn should_fail_when_invalid_initial_rate() { - let env = Env::default(); - env.mock_all_auths(); - - let sut = init_pool(&env, false); - - let ir_params_input = IRParams { - alpha: 144, - initial_rate: 10001, - max_rate: 50_001, - scaling_coeff: 9_001, - }; - - sut.pool.set_ir_params(&ir_params_input.clone()); -} - -#[test] -#[should_panic(expected = "HostError: Error(Contract, #403)")] -fn should_fail_when_invalid_max_rate() { - let env = Env::default(); - env.mock_all_auths(); - - let sut = init_pool(&env, false); - - let ir_params_input = IRParams { - alpha: 144, - initial_rate: 201, - max_rate: 10_000, - scaling_coeff: 9_001, - }; - - sut.pool.set_ir_params(&ir_params_input.clone()); -} - -#[test] -#[should_panic(expected = "HostError: Error(Contract, #402)")] -fn should_fail_when_invalid_scaling_coeff() { - let env = Env::default(); - env.mock_all_auths(); - - let sut = init_pool(&env, false); - - let ir_params_input = IRParams { - alpha: 144, - initial_rate: 201, - max_rate: 50_001, - scaling_coeff: 10_000, - }; - - sut.pool.set_ir_params(&ir_params_input.clone()); -} - -#[test] -fn should_set_ir_params() { - let env = Env::default(); - env.mock_all_auths(); - - let sut = init_pool(&env, false); - - let ir_params_input = IRParams { - alpha: 144, - initial_rate: 201, - max_rate: 50_001, - scaling_coeff: 9_001, - }; - - sut.pool.set_ir_params(&ir_params_input); - - let ir_params = sut.pool.ir_params().unwrap(); - - assert_eq!(ir_params_input.alpha, ir_params.alpha); - assert_eq!(ir_params_input.initial_rate, ir_params.initial_rate); - assert_eq!(ir_params_input.max_rate, ir_params.max_rate); - assert_eq!(ir_params_input.scaling_coeff, ir_params.scaling_coeff); -} diff --git a/contracts/pool/src/tests/set_pause.rs b/contracts/pool/src/tests/set_pause.rs index 94bc32f4..e77f674b 100644 --- a/contracts/pool/src/tests/set_pause.rs +++ b/contracts/pool/src/tests/set_pause.rs @@ -1,11 +1,11 @@ #![cfg(test)] extern crate std; -use crate::{tests::sut::init_pool, *}; -use soroban_sdk::{ - testutils::{AuthorizedFunction, AuthorizedInvocation}, - vec, IntoVal, Symbol, -}; +use crate::tests::sut::init_pool; +use crate::*; +use soroban_sdk::testutils::{AuthorizedFunction, AuthorizedInvocation}; +use soroban_sdk::{vec, IntoVal, Symbol}; +use tests::sut::{set_time, DAY}; #[test] fn should_require_admin() { @@ -38,10 +38,27 @@ fn should_set_pause() { env.mock_all_auths(); let sut = init_pool(&env, false); + let prev_pause_info = sut.pool.pause_info(); sut.pool.set_pause(&true); - assert!(sut.pool.paused()); + let pause_info = sut.pool.pause_info(); + assert!(pause_info.paused); + assert_eq!( + prev_pause_info.grace_period_secs, + pause_info.grace_period_secs + ); + assert_eq!(prev_pause_info.unpaused_at, pause_info.unpaused_at); + // change current time and check unpaused_at + set_time(&env, &sut, 2 * DAY, false); + let expected_unpaused_at = env.ledger().timestamp(); sut.pool.set_pause(&false); - assert!(!sut.pool.paused()); + let pause_info = sut.pool.pause_info(); + assert!(!pause_info.paused); + assert_eq!( + prev_pause_info.grace_period_secs, + pause_info.grace_period_secs + ); + assert_eq!(expected_unpaused_at, pause_info.unpaused_at); + assert!(prev_pause_info.unpaused_at < pause_info.unpaused_at); } diff --git a/contracts/pool/src/tests/set_pool_configuration.rs b/contracts/pool/src/tests/set_pool_configuration.rs new file mode 100644 index 00000000..6310b824 --- /dev/null +++ b/contracts/pool/src/tests/set_pool_configuration.rs @@ -0,0 +1,156 @@ +#![cfg(test)] +extern crate std; + +use soroban_sdk::testutils::{AuthorizedFunction, AuthorizedInvocation}; +use soroban_sdk::{vec, IntoVal, Symbol}; + +use crate::tests::sut::init_pool; +use crate::*; + +#[test] +fn should_require_admin() { + let env = Env::default(); + env.mock_all_auths(); + + let sut = init_pool(&env, false); + + let pool_config = PoolConfig { + base_asset_address: sut.reserves[0].token.address.clone(), + base_asset_decimals: sut.reserves[0].token.decimals(), + flash_loan_fee: 5, + initial_health: 2_500, + timestamp_window: 20, + grace_period: 1, + user_assets_limit: 4, + min_collat_amount: 0, + min_debt_amount: 0, + liquidation_protocol_fee: 0, + ir_alpha: 143, + ir_initial_rate: 200, + ir_max_rate: 50_000, + ir_scaling_coeff: 9_000, + }; + + sut.pool.set_pool_configuration(&pool_config); + + assert_eq!( + env.auths(), + [( + sut.pool_admin, + AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + sut.pool.address.clone(), + Symbol::new(&env, "set_pool_configuration"), + vec![&env, pool_config.into_val(&env)] + )), + sub_invocations: std::vec![] + } + )] + ); +} + +#[test] +fn should_set_pool_configuration() { + let env = Env::default(); + env.mock_all_auths(); + + let sut = init_pool(&env, false); + + let pool_config_before = sut.pool.pool_configuration(); + let pause_info_before = sut.pool.pause_info(); + + sut.pool.set_pool_configuration(&PoolConfig { + base_asset_address: sut.reserves[1].token.address.clone(), + base_asset_decimals: sut.reserves[1].token.decimals(), + flash_loan_fee: 12, + initial_health: 111, + timestamp_window: 11, + grace_period: 3, + user_assets_limit: 1, + min_collat_amount: 123, + min_debt_amount: 1234, + liquidation_protocol_fee: 5, + ir_alpha: 144, + ir_initial_rate: 201, + ir_max_rate: 50_001, + ir_scaling_coeff: 9_001, + }); + + let pause_info_after = sut.pool.pause_info(); + let pool_config_after = sut.pool.pool_configuration(); + + assert_eq!( + pool_config_before.base_asset_address, + sut.reserves[0].token.address + ); + assert_eq!( + pool_config_before.base_asset_decimals, + sut.reserves[0].token.decimals() + ); + assert_eq!(pool_config_before.flash_loan_fee, 5); + assert_eq!(pool_config_before.initial_health, 0); + assert_eq!(pool_config_before.timestamp_window, 20); + assert_eq!(pool_config_before.user_assets_limit, 4); + assert_eq!(pool_config_before.grace_period, 1); + assert_eq!(pool_config_before.min_collat_amount, 0); + assert_eq!(pool_config_before.min_debt_amount, 0); + assert_eq!(pool_config_before.liquidation_protocol_fee, 0); + + assert_eq!( + pool_config_after.base_asset_address, + sut.reserves[1].token.address + ); + assert_eq!( + pool_config_after.base_asset_decimals, + sut.reserves[1].token.decimals() + ); + assert_eq!(pool_config_after.flash_loan_fee, 12); + assert_eq!(pool_config_after.initial_health, 111); + assert_eq!(pool_config_after.timestamp_window, 11); + assert_eq!(pool_config_after.user_assets_limit, 1); + assert_eq!(pool_config_after.grace_period, 3); + assert_eq!(pool_config_after.min_collat_amount, 123); + assert_eq!(pool_config_after.min_debt_amount, 1234); + assert_eq!(pool_config_after.liquidation_protocol_fee, 5); + + assert_eq!( + pool_config_after.grace_period, + pause_info_after.grace_period_secs + ); + assert_eq!(pool_config_after.ir_alpha, 144); + assert_eq!(pool_config_after.ir_initial_rate, 201); + assert_eq!(pool_config_after.ir_max_rate, 50_001); + assert_eq!(pool_config_after.ir_scaling_coeff, 9_001); + + assert_eq!(pause_info_before.paused, pause_info_after.paused); + assert_eq!(pause_info_before.unpaused_at, pause_info_after.unpaused_at); +} + +// #[test] +// #[should_panic(expected = "HostError: Error(Contract, #5)")] +// fn should_require_non_zero() { +// let env = Env::default(); +// env.mock_all_auths(); + +// let sut = init_pool(&env, false); + +// let grace_period = 0; +// sut.pool.set_grace_period(&grace_period); +// } + +// #[test] +// fn should_set_grace_period() { +// let env = Env::default(); +// env.mock_all_auths(); + +// let sut = init_pool(&env, false); +// let prev_pause_info = sut.pool.pause_info(); + +// let grace_period = 1; +// sut.pool.set_grace_period(&grace_period); +// let pause_info = sut.pool.pause_info(); + +// assert_eq!(grace_period, pause_info.grace_period_secs); +// assert_eq!(prev_pause_info.paused, pause_info.paused); +// assert_eq!(prev_pause_info.unpaused_at, pause_info.unpaused_at); +// } diff --git a/contracts/pool/src/tests/set_price_feeds.rs b/contracts/pool/src/tests/set_price_feeds.rs index d1142cc1..34fd9a99 100644 --- a/contracts/pool/src/tests/set_price_feeds.rs +++ b/contracts/pool/src/tests/set_price_feeds.rs @@ -19,7 +19,7 @@ fn should_require_admin() { let asset_1 = Address::generate(&env); let asset_2 = Address::generate(&env); - let pool: LendingPoolClient<'_> = create_pool_contract(&env, &admin, false); + let pool: LendingPoolClient<'_> = create_pool_contract(&env, &admin, false, &asset_1); let price_feed: PriceFeedClient<'_> = create_price_feed_contract(&env); assert!(pool.price_feeds(&asset_1.clone()).is_none()); @@ -31,6 +31,8 @@ fn should_require_admin() { PriceFeedConfigInput { asset: asset_1.clone(), asset_decimals: 7, + min_sanity_price_in_base: 5_000_000, + max_sanity_price_in_base: 100_000_000, feeds: vec![ &env, PriceFeed { @@ -38,6 +40,7 @@ fn should_require_admin() { feed_asset: OracleAsset::Stellar(asset_1), feed_decimals: 14, twap_records: 10, + min_timestamp_delta: 100, timestamp_precision: TimestampPrecision::Sec, }, ], @@ -45,6 +48,8 @@ fn should_require_admin() { PriceFeedConfigInput { asset: asset_2.clone(), asset_decimals: 9, + min_sanity_price_in_base: 5_000_000, + max_sanity_price_in_base: 100_000_000, feeds: vec![ &env, PriceFeed { @@ -52,6 +57,7 @@ fn should_require_admin() { feed_asset: OracleAsset::Stellar(asset_2), feed_decimals: 16, twap_records: 10, + min_timestamp_delta: 100, timestamp_precision: TimestampPrecision::Sec, }, ], @@ -86,7 +92,7 @@ fn should_set_price_feed() { let asset_1 = Address::generate(&env); let asset_2 = Address::generate(&env); - let pool: LendingPoolClient<'_> = create_pool_contract(&env, &admin, false); + let pool: LendingPoolClient<'_> = create_pool_contract(&env, &admin, false, &asset_1); let price_feed_1: PriceFeedClient<'_> = create_price_feed_contract(&env); let price_feed_2: PriceFeedClient<'_> = create_price_feed_contract(&env); @@ -99,6 +105,8 @@ fn should_set_price_feed() { PriceFeedConfigInput { asset: asset_1.clone(), asset_decimals: 7, + min_sanity_price_in_base: 5_000_000, + max_sanity_price_in_base: 100_000_000, feeds: vec![ &env, PriceFeed { @@ -106,6 +114,7 @@ fn should_set_price_feed() { feed_asset: OracleAsset::Stellar(asset_1.clone()), feed_decimals: 14, twap_records: 10, + min_timestamp_delta: 100, timestamp_precision: TimestampPrecision::Sec, }, ], @@ -113,6 +122,8 @@ fn should_set_price_feed() { PriceFeedConfigInput { asset: asset_2.clone(), asset_decimals: 9, + min_sanity_price_in_base: 5_000_000, + max_sanity_price_in_base: 100_000_000, feeds: vec![ &env, PriceFeed { @@ -120,6 +131,7 @@ fn should_set_price_feed() { feed_asset: OracleAsset::Other(symbol_short!("XRP")), feed_decimals: 16, twap_records: 9, + min_timestamp_delta: 100, timestamp_precision: TimestampPrecision::Sec, }, ], diff --git a/contracts/pool/src/tests/set_reserve_status.rs b/contracts/pool/src/tests/set_reserve_status.rs index 45d99a03..f5cd05eb 100644 --- a/contracts/pool/src/tests/set_reserve_status.rs +++ b/contracts/pool/src/tests/set_reserve_status.rs @@ -81,8 +81,8 @@ fn should_emit_events() { &env, ( sut.pool.address.clone(), - (Symbol::new(&env, "reserve_activated"), &asset).into_val(&env), - ().into_val(&env) + (&asset,).into_val(&env), + (true).into_val(&env) ), ] ); @@ -97,8 +97,8 @@ fn should_emit_events() { &env, ( sut.pool.address.clone(), - (Symbol::new(&env, "reserve_deactivated"), &asset).into_val(&env), - ().into_val(&env) + (&asset,).into_val(&env), + (false).into_val(&env) ), ] ); diff --git a/contracts/pool/src/tests/set_reserve_timestamp_window.rs b/contracts/pool/src/tests/set_reserve_timestamp_window.rs deleted file mode 100644 index 22b08184..00000000 --- a/contracts/pool/src/tests/set_reserve_timestamp_window.rs +++ /dev/null @@ -1,48 +0,0 @@ -#![cfg(test)] -extern crate std; - -use soroban_sdk::testutils::{AuthorizedFunction, AuthorizedInvocation}; -use soroban_sdk::{vec, IntoVal, Symbol}; - -use crate::tests::sut::init_pool; -use crate::*; - -#[test] -fn should_require_admin() { - let env = Env::default(); - env.mock_all_auths(); - - let sut = init_pool(&env, false); - sut.pool.set_reserve_timestamp_window(&1); - - assert_eq!( - env.auths(), - [( - sut.pool_admin.clone(), - AuthorizedInvocation { - function: AuthorizedFunction::Contract(( - sut.pool.address.clone(), - Symbol::new(&env, "set_reserve_timestamp_window"), - vec![&env, 1u64.into_val(&env)] - )), - sub_invocations: std::vec![] - } - )] - ); -} - -#[test] -fn should_set_reserve_timestamp_window() { - let env = Env::default(); - env.mock_all_auths(); - - let sut = init_pool(&env, false); - - let window_initial = sut.pool.reserve_timestamp_window(); - - sut.pool.set_reserve_timestamp_window(&123); - let window_after = sut.pool.reserve_timestamp_window(); - - assert_eq!(window_initial, 20); - assert_eq!(window_after, 123); -} diff --git a/contracts/pool/src/tests/snapshots/budget_utilization.snap b/contracts/pool/src/tests/snapshots/budget_utilization.snap index 410905b9..08d532f8 100644 --- a/contracts/pool/src/tests/snapshots/budget_utilization.snap +++ b/contracts/pool/src/tests/snapshots/budget_utilization.snap @@ -1,216 +1,180 @@ ['pool::tests::budget::account_position'] = { - "cpu_cost": 33316948, - "memory_cost": 7473174, + "cpu_cost": 33834972, + "memory_cost": 7492344, "cpu_limit_exceeded": false, "memory_limit_exceeded": false, } ['pool::tests::budget::borrow'] = { - "cpu_cost": 46907124, - "memory_cost": 11511690, + "cpu_cost": 46964495, + "memory_cost": 11446188, "cpu_limit_exceeded": false, "memory_limit_exceeded": false, } ['pool::tests::budget::collat_coeff'] = { - "cpu_cost": 26758700, - "memory_cost": 4306357, + "cpu_cost": 27767249, + "memory_cost": 4416732, "cpu_limit_exceeded": false, "memory_limit_exceeded": false, } ['pool::tests::budget::configure_as_collateral'] = { - "cpu_cost": 27187808, - "memory_cost": 4379737, + "cpu_cost": 28320342, + "memory_cost": 4534180, "cpu_limit_exceeded": false, "memory_limit_exceeded": false, } ['pool::tests::budget::debt_coeff'] = { - "cpu_cost": 26375391, - "memory_cost": 4234292, + "cpu_cost": 27458080, + "memory_cost": 4362818, "cpu_limit_exceeded": false, "memory_limit_exceeded": false, } ['pool::tests::budget::deposit'] = { - "cpu_cost": 36398889, - "memory_cost": 6708009, + "cpu_cost": 36908130, + "memory_cost": 6727019, "cpu_limit_exceeded": false, "memory_limit_exceeded": false, } ['pool::tests::budget::enable_borrowing_on_reserve'] = { - "cpu_cost": 26632274, - "memory_cost": 4293689, - "cpu_limit_exceeded": false, - "memory_limit_exceeded": false, -} -['pool::tests::budget::flash_loan_fee'] = { - "cpu_cost": 26141693, - "memory_cost": 4194897, + "cpu_cost": 27745878, + "memory_cost": 4435852, "cpu_limit_exceeded": false, "memory_limit_exceeded": false, } ['pool::tests::budget::flash_loan_with_borrow'] = { - "cpu_cost": 90086661, - "memory_cost": 24994469, + "cpu_cost": 89920024, + "memory_cost": 24838743, "cpu_limit_exceeded": false, "memory_limit_exceeded": false, } ['pool::tests::budget::flash_loan_without_borrow'] = { - "cpu_cost": 55112393, - "memory_cost": 12584245, + "cpu_cost": 56416967, + "memory_cost": 12744464, "cpu_limit_exceeded": false, "memory_limit_exceeded": false, } ['pool::tests::budget::get_reserve'] = { - "cpu_cost": 26202659, - "memory_cost": 4196747, + "cpu_cost": 27285478, + "memory_cost": 4326604, "cpu_limit_exceeded": false, "memory_limit_exceeded": false, } ['pool::tests::budget::init_reserve'] = { - "cpu_cost": 178905, - "memory_cost": 30384, - "cpu_limit_exceeded": false, - "memory_limit_exceeded": false, -} -['pool::tests::budget::ir_params'] = { - "cpu_cost": 26207117, - "memory_cost": 4210312, - "cpu_limit_exceeded": false, - "memory_limit_exceeded": false, -} -['pool::tests::budget::liquidate_receive_stoken_when_borrower_has_two_debts'] = { - "cpu_cost": 49212060, - "memory_cost": 11786010, + "cpu_cost": 235448, + "memory_cost": 46240, "cpu_limit_exceeded": false, "memory_limit_exceeded": false, } ['pool::tests::budget::liquidate_receive_underlying_when_borrower_has_one_debt'] = { - "cpu_cost": 48995876, - "memory_cost": 11821311, + "cpu_cost": 48893892, + "memory_cost": 11709034, "cpu_limit_exceeded": false, "memory_limit_exceeded": false, } ['pool::tests::budget::liquidate_receive_underlying_when_borrower_has_two_debts'] = { - "cpu_cost": 49271179, - "memory_cost": 11797630, + "cpu_cost": 49171102, + "memory_cost": 11707506, "cpu_limit_exceeded": false, "memory_limit_exceeded": false, } -['pool::tests::budget::paused'] = { - "cpu_cost": 26136291, - "memory_cost": 4194687, +['pool::tests::budget::pause_info'] = { + "cpu_cost": 27232190, + "memory_cost": 4325102, "cpu_limit_exceeded": false, "memory_limit_exceeded": false, } ['pool::tests::budget::price_feed'] = { - "cpu_cost": 26149470, - "memory_cost": 4195224, + "cpu_cost": 27244494, + "memory_cost": 4325369, "cpu_limit_exceeded": false, "memory_limit_exceeded": false, } ['pool::tests::budget::repay_full'] = { - "cpu_cost": 32877332, - "memory_cost": 6360327, + "cpu_cost": 36927695, + "memory_cost": 7979472, "cpu_limit_exceeded": false, "memory_limit_exceeded": false, } ['pool::tests::budget::repay_partial'] = { - "cpu_cost": 32911277, - "memory_cost": 6350745, + "cpu_cost": 39508938, + "memory_cost": 9436893, "cpu_limit_exceeded": false, "memory_limit_exceeded": false, } ['pool::tests::budget::s_token_transfer'] = { - "cpu_cost": 41198323, - "memory_cost": 9587776, + "cpu_cost": 41895169, + "memory_cost": 9639410, "cpu_limit_exceeded": false, "memory_limit_exceeded": false, } ['pool::tests::budget::set_as_collateral'] = { - "cpu_cost": 32445679, - "memory_cost": 7335054, - "cpu_limit_exceeded": false, - "memory_limit_exceeded": false, -} -['pool::tests::budget::set_base_asset'] = { - "cpu_cost": 26457229, - "memory_cost": 4270291, - "cpu_limit_exceeded": false, - "memory_limit_exceeded": false, -} -['pool::tests::budget::set_flash_loan_fee'] = { - "cpu_cost": 26453040, - "memory_cost": 4270123, + "cpu_cost": 33392196, + "memory_cost": 7441637, "cpu_limit_exceeded": false, "memory_limit_exceeded": false, } -['pool::tests::budget::set_ir_params'] = { - "cpu_cost": 26467287, - "memory_cost": 4270868, +['pool::tests::budget::set_pause'] = { + "cpu_cost": 27682212, + "memory_cost": 4433707, "cpu_limit_exceeded": false, "memory_limit_exceeded": false, } -['pool::tests::budget::set_pause'] = { - "cpu_cost": 26451153, - "memory_cost": 4270141, +['pool::tests::budget::set_pool_configuration'] = { + "cpu_cost": 27898511, + "memory_cost": 4469950, "cpu_limit_exceeded": false, "memory_limit_exceeded": false, } ['pool::tests::budget::set_price_feed'] = { - "cpu_cost": 143627, - "memory_cost": 23213, + "cpu_cost": 198318, + "memory_cost": 36408, "cpu_limit_exceeded": false, "memory_limit_exceeded": false, } ['pool::tests::budget::set_reserve_status'] = { - "cpu_cost": 26631725, - "memory_cost": 4293680, + "cpu_cost": 27740942, + "memory_cost": 4435714, "cpu_limit_exceeded": false, "memory_limit_exceeded": false, } ['pool::tests::budget::stoken_underlying_balance'] = { - "cpu_cost": 26155882, - "memory_cost": 4197975, - "cpu_limit_exceeded": false, - "memory_limit_exceeded": false, -} -['pool::tests::budget::treasury'] = { - "cpu_cost": 53355, - "memory_cost": 8318, + "cpu_cost": 26979530, + "memory_cost": 4286044, "cpu_limit_exceeded": false, "memory_limit_exceeded": false, } ['pool::tests::budget::upgrade'] = { - "cpu_cost": 26357881, - "memory_cost": 4251119, + "cpu_cost": 27460118, + "memory_cost": 4387116, "cpu_limit_exceeded": false, "memory_limit_exceeded": false, } ['pool::tests::budget::upgrade_debt_token'] = { - "cpu_cost": 30104593, - "memory_cost": 5841537, + "cpu_cost": 31233039, + "memory_cost": 5978888, "cpu_limit_exceeded": false, "memory_limit_exceeded": false, } ['pool::tests::budget::upgrade_s_token'] = { - "cpu_cost": 33956201, - "memory_cost": 6282087, + "cpu_cost": 35094592, + "memory_cost": 6420415, "cpu_limit_exceeded": false, "memory_limit_exceeded": false, } ['pool::tests::budget::user_configuration'] = { - "cpu_cost": 25946808, - "memory_cost": 4166386, + "cpu_cost": 26987188, + "memory_cost": 4289039, "cpu_limit_exceeded": false, "memory_limit_exceeded": false, } ['pool::tests::budget::withdraw_full'] = { - "cpu_cost": 43179315, - "memory_cost": 9860825, + "cpu_cost": 42979060, + "memory_cost": 9758972, "cpu_limit_exceeded": false, "memory_limit_exceeded": false, } ['pool::tests::budget::withdraw_partial'] = { - "cpu_cost": 42785809, - "memory_cost": 9850813, + "cpu_cost": 42843758, + "memory_cost": 9774946, "cpu_limit_exceeded": false, "memory_limit_exceeded": false, } diff --git a/contracts/pool/src/tests/stoken_underlying_balance.rs b/contracts/pool/src/tests/stoken_underlying_balance.rs index e697dd24..9aee5815 100644 --- a/contracts/pool/src/tests/stoken_underlying_balance.rs +++ b/contracts/pool/src/tests/stoken_underlying_balance.rs @@ -14,9 +14,10 @@ fn should_not_be_changed_when_direct_transfer_to_underlying_asset() { sut.pool .deposit(&lender, &sut.reserves[0].token.address, &1_000_000_000); - let s_token_underlying_supply = sut - .pool - .stoken_underlying_balance(&sut.reserves[0].s_token().address); + let s_token_underlying_supply = sut.pool.token_balance( + &sut.reserves[0].token.address, + &sut.reserves[0].s_token().address, + ); assert_eq!(s_token_underlying_supply, 1_000_000_000); @@ -24,9 +25,10 @@ fn should_not_be_changed_when_direct_transfer_to_underlying_asset() { .token .transfer(&lender, &sut.reserves[0].s_token().address, &1_000_000_000); - let s_token_underlying_supply = sut - .pool - .stoken_underlying_balance(&sut.reserves[0].s_token().address); + let s_token_underlying_supply = sut.pool.token_balance( + &sut.reserves[0].token.address, + &sut.reserves[0].s_token().address, + ); assert_eq!(s_token_underlying_supply, 1_000_000_000); } diff --git a/contracts/pool/src/tests/sut.rs b/contracts/pool/src/tests/sut.rs index 9223af2b..6209ce00 100644 --- a/contracts/pool/src/tests/sut.rs +++ b/contracts/pool/src/tests/sut.rs @@ -61,6 +61,7 @@ pub(crate) fn create_pool_contract<'a>( e: &Env, admin: &Address, use_wasm: bool, + base_token: &Address, ) -> LendingPoolClient<'a> { let client = if use_wasm { LendingPoolClient::new(e, &e.register_contract_wasm(None, pool::WASM)) @@ -68,20 +69,28 @@ pub(crate) fn create_pool_contract<'a>( LendingPoolClient::new(e, &e.register_contract(None, LendingPool)) }; - let treasury = Address::generate(e); - let flash_loan_fee = 5; - let initial_health = 0; + let grace_period = 1; + + e.ledger() + .with_mut(|li| li.timestamp = li.timestamp + grace_period); client.initialize( &admin, - &treasury, - &flash_loan_fee, - &initial_health, - &IRParams { - alpha: 143, - initial_rate: 200, - max_rate: 50_000, - scaling_coeff: 9_000, + &PoolConfig { + base_asset_address: base_token.clone(), + base_asset_decimals: 7, + flash_loan_fee: 5, + initial_health: 0, + timestamp_window: 20, + grace_period: 1, + user_assets_limit: 4, + min_collat_amount: 0, + min_debt_amount: 0, + liquidation_protocol_fee: 0, + ir_alpha: 143, + ir_initial_rate: 200, + ir_max_rate: 50_000, + ir_scaling_coeff: 9_000, }, ); client @@ -139,15 +148,26 @@ pub(crate) fn init_pool<'a>(env: &Env, use_pool_wasm: bool) -> Sut<'a> { let admin = Address::generate(&env); let token_admin = Address::generate(&env); - let pool: LendingPoolClient<'_> = create_pool_contract(&env, &admin, use_pool_wasm); + let (base_token, _) = create_token_contract(&env, &token_admin); + let pool: LendingPoolClient<'_> = + create_pool_contract(&env, &admin, use_pool_wasm, &base_token.address); let price_feed: PriceFeedClient<'_> = create_price_feed_contract(&env); let flash_loan_receiver: FlashLoanReceiverClient<'_> = create_flash_loan_receiver_contract(&env); let reserves: std::vec::Vec> = (0..4) .map(|i| { - let (token, token_admin_client) = create_token_contract(&env, &token_admin); - let decimals = (i == 0).then(|| 7).unwrap_or(9); + let (token, token_admin_client, decimals) = if i == 0 { + ( + TokenClient::new(env, &base_token.address.clone()), + TokenAdminClient::new(env, &base_token.address.clone()), + 7, + ) + } else { + let (token, token_admin_client) = create_token_contract(&env, &token_admin); + + (token, token_admin_client, 9) + }; let (s_token, debt_token) = if i == 3 { pool.init_reserve(&token.address, &ReserveType::RWA); @@ -165,7 +185,22 @@ pub(crate) fn init_pool<'a>(env: &Env, use_pool_wasm: bool) -> Sut<'a> { }; if i == 0 { - pool.set_base_asset(&token.address, &decimals) + pool.set_pool_configuration(&PoolConfig { + base_asset_address: token.address.clone(), + base_asset_decimals: decimals, + flash_loan_fee: 5, + initial_health: 0, + timestamp_window: 20, + grace_period: 1, + user_assets_limit: 4, + min_collat_amount: 0, + min_debt_amount: 0, + liquidation_protocol_fee: 0, + ir_alpha: 143, + ir_initial_rate: 200, + ir_max_rate: 50_000, + ir_scaling_coeff: 9_000, + }); } let liquidity_cap = 100_000_000 * 10_i128.pow(decimals); // 100M @@ -174,7 +209,7 @@ pub(crate) fn init_pool<'a>(env: &Env, use_pool_wasm: bool) -> Sut<'a> { let discount = 6000; //60% pool.configure_as_collateral( - &token.address, + &token.address.clone(), &CollateralParamsInput { liq_cap: liquidity_cap, pen_order: pen_order, @@ -199,7 +234,7 @@ pub(crate) fn init_pool<'a>(env: &Env, use_pool_wasm: bool) -> Sut<'a> { &env, PriceData { price: 100_000_000_000_000, - timestamp: 1704790200000, + timestamp: 0, }, ], ), @@ -209,7 +244,7 @@ pub(crate) fn init_pool<'a>(env: &Env, use_pool_wasm: bool) -> Sut<'a> { &env, PriceData { price: 10_000_000_000_000_000, - timestamp: 1704790200000, + timestamp: 0, }, ], ), @@ -219,7 +254,7 @@ pub(crate) fn init_pool<'a>(env: &Env, use_pool_wasm: bool) -> Sut<'a> { &env, PriceData { price: 10_000_000_000_000_000, - timestamp: 1704790200000, + timestamp: 0, }, ], ), @@ -229,7 +264,7 @@ pub(crate) fn init_pool<'a>(env: &Env, use_pool_wasm: bool) -> Sut<'a> { &env, PriceData { price: 10_000_000_000_000_000, - timestamp: 1704790200000, + timestamp: 0, }, ], ), @@ -251,6 +286,8 @@ pub(crate) fn init_pool<'a>(env: &Env, use_pool_wasm: bool) -> Sut<'a> { PriceFeedConfigInput { asset: reserves[0].token.address.clone(), asset_decimals: 7, + min_sanity_price_in_base: 5_000_000, + max_sanity_price_in_base: 100_000_000, feeds: vec![ &env, PriceFeed { @@ -258,6 +295,7 @@ pub(crate) fn init_pool<'a>(env: &Env, use_pool_wasm: bool) -> Sut<'a> { feed_asset: OracleAsset::Stellar(reserves[0].token.address.clone()), feed_decimals: 14, twap_records: 10, + min_timestamp_delta: 100, timestamp_precision: TimestampPrecision::Sec, }, ], @@ -265,6 +303,8 @@ pub(crate) fn init_pool<'a>(env: &Env, use_pool_wasm: bool) -> Sut<'a> { PriceFeedConfigInput { asset: reserves[1].token.address.clone(), asset_decimals: 9, + min_sanity_price_in_base: 5_000_000, + max_sanity_price_in_base: 100_000_000, feeds: vec![ &env, PriceFeed { @@ -272,6 +312,7 @@ pub(crate) fn init_pool<'a>(env: &Env, use_pool_wasm: bool) -> Sut<'a> { feed_asset: OracleAsset::Stellar(reserves[1].token.address.clone()), feed_decimals: 16, twap_records: 10, + min_timestamp_delta: 100, timestamp_precision: TimestampPrecision::Sec, }, ], @@ -279,6 +320,8 @@ pub(crate) fn init_pool<'a>(env: &Env, use_pool_wasm: bool) -> Sut<'a> { PriceFeedConfigInput { asset: reserves[2].token.address.clone(), asset_decimals: 9, + min_sanity_price_in_base: 5_000_000, + max_sanity_price_in_base: 100_000_000, feeds: vec![ &env, PriceFeed { @@ -286,6 +329,7 @@ pub(crate) fn init_pool<'a>(env: &Env, use_pool_wasm: bool) -> Sut<'a> { feed_asset: OracleAsset::Stellar(reserves[2].token.address.clone()), feed_decimals: 16, twap_records: 10, + min_timestamp_delta: 100, timestamp_precision: TimestampPrecision::Sec, }, ], @@ -293,6 +337,8 @@ pub(crate) fn init_pool<'a>(env: &Env, use_pool_wasm: bool) -> Sut<'a> { PriceFeedConfigInput { asset: reserves[3].token.address.clone(), asset_decimals: 9, + min_sanity_price_in_base: 5_000_000, + max_sanity_price_in_base: 100_000_000, feeds: vec![ &env, PriceFeed { @@ -300,6 +346,7 @@ pub(crate) fn init_pool<'a>(env: &Env, use_pool_wasm: bool) -> Sut<'a> { feed_asset: OracleAsset::Stellar(reserves[3].token.address.clone()), feed_decimals: 15, twap_records: 10, + min_timestamp_delta: 100, timestamp_precision: TimestampPrecision::Sec, }, ], @@ -357,7 +404,7 @@ pub(crate) fn fill_pool<'a, 'b>( ); } - env.ledger().with_mut(|li| li.timestamp = DAY); + set_time(&env, &sut, DAY, false); //borrower deposit first token and borrow second token sut.pool @@ -372,6 +419,34 @@ pub(crate) fn fill_pool<'a, 'b>( (lender, borrower, &sut.reserves[1]) } +pub(crate) fn set_time<'a, 'b>(env: &'b Env, sut: &'a Sut, time: u64, append: bool) { + let new_time = append + .then(|| env.ledger().timestamp() + time) + .unwrap_or(time); + + for i in 0..4 { + let token = sut.reserves[i].token.address.clone(); + + let prices = sut + .price_feed + .prices(&Asset::Stellar(token.clone()), &1000) + .unwrap(); + + let mut new_prices = Vec::new(env); + + for price in prices.iter() { + new_prices.push_back(PriceData { + price: price.price, + timestamp: new_time, + }); + } + + sut.price_feed.init(&Asset::Stellar(token), &new_prices); + } + + env.ledger().with_mut(|li| li.timestamp = new_time); +} + /// Fill lending pool with two lenders and one borrower pub(crate) fn fill_pool_two<'a, 'b>( env: &'b Env, @@ -387,7 +462,7 @@ pub(crate) fn fill_pool_two<'a, 'b>( assert_eq!(sut.reserves[i].token.balance(&lender_2), amount); } - env.ledger().with_mut(|li| li.timestamp = 2 * DAY); + set_time(&env, &sut, 2 * DAY, false); //lender deposit all tokens for i in 0..3 { @@ -417,12 +492,12 @@ pub(crate) fn fill_pool_three<'a, 'b>( let liquidator = Address::generate(&env); - env.ledger().with_mut(|li| li.timestamp = 2 * DAY); + set_time(&env, &sut, 2 * DAY, false); debt_config.token_admin.mint(&liquidator, &1_000_000_000); sut.pool.borrow(&borrower, &debt_token, &60_000_000); - env.ledger().with_mut(|li| li.timestamp = 3 * DAY); + set_time(&env, &sut, 3 * DAY, false); (lender, borrower, liquidator, debt_config) } @@ -446,7 +521,7 @@ pub(crate) fn fill_pool_four<'a, 'b>(env: &'b Env, sut: &'a Sut) -> (Address, Ad .deposit(&lender, &sut.reserves[i].token.address, &amount); } - env.ledger().with_mut(|li| li.timestamp = 1 * DAY); + set_time(&env, &sut, 1 * DAY, false); sut.pool .deposit(&borrower1, &sut.reserves[0].token.address, &100_000_000); @@ -462,7 +537,7 @@ pub(crate) fn fill_pool_four<'a, 'b>(env: &'b Env, sut: &'a Sut) -> (Address, Ad sut.pool .borrow(&borrower2, &sut.reserves[1].token.address, &5_999_000_000); - env.ledger().with_mut(|li| li.timestamp = 2 * DAY); + set_time(&env, &sut, 2 * DAY, false); (lender, borrower1, borrower2) } diff --git a/contracts/pool/src/tests/treasury.rs b/contracts/pool/src/tests/treasury.rs index fc78931c..7b88d22c 100644 --- a/contracts/pool/src/tests/treasury.rs +++ b/contracts/pool/src/tests/treasury.rs @@ -1,30 +1,32 @@ -use crate::*; -use soroban_sdk::testutils::Address as _; +// use crate::*; +// use soroban_sdk::testutils::Address as _; -#[test] -fn should_return_treasury_address() { - let env = Env::default(); - env.mock_all_auths(); +// #[test] +// fn should_return_treasury_address() { +// let env = Env::default(); +// env.mock_all_auths(); - let pool = LendingPoolClient::new(&env, &env.register_contract(None, LendingPool)); +// let pool = LendingPoolClient::new(&env, &env.register_contract(None, LendingPool)); - let admin = Address::generate(&env); - let treasury = Address::generate(&env); - let flash_loan_fee = 5; - let initial_health = 2_500; +// let admin = Address::generate(&env); +// let treasury = Address::generate(&env); +// let flash_loan_fee = 5; +// let initial_health = 2_500; +// let grace_period = 60 * 60 * 24; - pool.initialize( - &admin, - &treasury, - &flash_loan_fee, - &initial_health, - &IRParams { - alpha: 143, - initial_rate: 200, - max_rate: 50_000, - scaling_coeff: 9_000, - }, - ); +// pool.initialize( +// &admin, +// &treasury, +// &flash_loan_fee, +// &initial_health, +// &IRParams { +// alpha: 143, +// initial_rate: 200, +// max_rate: 50_000, +// scaling_coeff: 9_000, +// }, +// &grace_period, +// ); - assert_eq!(pool.treasury(), treasury); -} +// assert_eq!(pool.treasury(), treasury); +// } diff --git a/contracts/pool/src/tests/twap_median_price.rs b/contracts/pool/src/tests/twap_median_price.rs index 1b0e0814..b5912a81 100644 --- a/contracts/pool/src/tests/twap_median_price.rs +++ b/contracts/pool/src/tests/twap_median_price.rs @@ -4,15 +4,15 @@ use pool_interface::types::price_feed_config_input::PriceFeedConfigInput; use pool_interface::types::timestamp_precision::TimestampPrecision; use price_feed_interface::types::asset::Asset; use price_feed_interface::types::price_data::PriceData; -use soroban_sdk::testutils::{Address as _, Ledger}; +use soroban_sdk::testutils::Address as _; use soroban_sdk::{symbol_short, vec, Address, Env}; use crate::tests::sut::init_pool; -use super::sut::create_price_feed_contract; +use super::sut::{create_price_feed_contract, set_time}; #[test] -#[should_panic(expected = "HostError: Error(Contract, #2)")] +#[should_panic(expected = "HostError: Error(Contract, #1)")] fn should_fail_when_feed_is_missing_for_asset() { let env = Env::default(); env.mock_all_auths(); @@ -40,6 +40,8 @@ fn should_fail_when_feed_is_missing_for_asset() { PriceFeedConfigInput { asset: asset_1.clone(), asset_decimals: 9, + min_sanity_price_in_base: 5_000_000, + max_sanity_price_in_base: 50_000_000_000, feeds: vec![ &env, // price feed with Stellar asset @@ -48,6 +50,7 @@ fn should_fail_when_feed_is_missing_for_asset() { feed_asset: OracleAsset::Stellar(asset_1.clone()), feed_decimals: 14, twap_records: 10, + min_timestamp_delta: 100, timestamp_precision: TimestampPrecision::Sec, }, ], @@ -56,13 +59,13 @@ fn should_fail_when_feed_is_missing_for_asset() { sut.pool.set_price_feeds(&price_feeds); - env.ledger().with_mut(|li| li.timestamp = 1704790800000); // delta = 600_000 + set_time(&env, &sut, 1704790800000, false); // delta = 600_000 sut.pool.twap_median_price(&asset_2, &1_000_000_000); } #[test] -#[should_panic(expected = "HostError: Error(Contract, #104)")] +#[should_panic(expected = "HostError: Error(Contract, #102)")] fn should_fail_when_price_is_missing_in_feed() { let env = Env::default(); env.mock_all_auths(); @@ -93,6 +96,8 @@ fn should_fail_when_price_is_missing_in_feed() { PriceFeedConfigInput { asset: asset_1.clone(), asset_decimals: 9, + min_sanity_price_in_base: 5_000_000, + max_sanity_price_in_base: 50_000_000_000, feeds: vec![ &env, // price feed with Stellar asset @@ -101,6 +106,7 @@ fn should_fail_when_price_is_missing_in_feed() { feed_asset: OracleAsset::Stellar(asset_1.clone()), feed_decimals: 14, twap_records: 10, + min_timestamp_delta: 100, timestamp_precision: TimestampPrecision::Sec, }, // price feed with Other asset @@ -109,6 +115,7 @@ fn should_fail_when_price_is_missing_in_feed() { feed_asset: OracleAsset::Other(symbol_short!("XRP")), feed_decimals: 10, twap_records: 10, + min_timestamp_delta: 100, timestamp_precision: TimestampPrecision::Sec, }, ], @@ -117,7 +124,259 @@ fn should_fail_when_price_is_missing_in_feed() { sut.pool.set_price_feeds(&price_feeds); - env.ledger().with_mut(|li| li.timestamp = 1704790800000); // delta = 600_000 + set_time(&env, &sut, 1704790800000, false); // delta = 600_000 + + sut.pool.twap_median_price(&asset_1, &1_000_000_000); +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #102)")] +fn should_fail_when_all_price_feeds_throws_error() { + let env = Env::default(); + env.mock_all_auths(); + + let sut = init_pool(&env, false); + + let asset_1 = sut.reserves[1].token.address.clone(); + + let asset_1_feed_1 = create_price_feed_contract(&env); + let asset_1_feed_2 = create_price_feed_contract(&env); + let asset_1_feed_3 = create_price_feed_contract(&env); + + asset_1_feed_1.init(&Asset::Stellar(asset_1.clone()), &vec![&env]); + asset_1_feed_2.init(&Asset::Other(symbol_short!("XRP")), &vec![&env]); + asset_1_feed_3.init(&Asset::Other(symbol_short!("XRP")), &vec![&env]); + + let price_feeds = vec![ + &env, + PriceFeedConfigInput { + asset: asset_1.clone(), + asset_decimals: 9, + min_sanity_price_in_base: 5_000_000, + max_sanity_price_in_base: 50_000_000_000, + feeds: vec![ + &env, + PriceFeed { + feed: asset_1_feed_1.address.clone(), + feed_asset: OracleAsset::Stellar(asset_1.clone()), + feed_decimals: 14, + twap_records: 10, + min_timestamp_delta: 1_000_000, + timestamp_precision: TimestampPrecision::Sec, + }, + PriceFeed { + feed: asset_1_feed_2.address.clone(), + feed_asset: OracleAsset::Other(symbol_short!("XRP")), + feed_decimals: 10, + twap_records: 10, + min_timestamp_delta: 1_000_000, + timestamp_precision: TimestampPrecision::Msec, + }, + PriceFeed { + feed: asset_1_feed_3.address.clone(), + feed_asset: OracleAsset::Other(symbol_short!("XRP")), + feed_decimals: 10, + twap_records: 10, + min_timestamp_delta: 1_000_000, + timestamp_precision: TimestampPrecision::Msec, + }, + ], + }, + ]; + + sut.pool.set_price_feeds(&price_feeds); + + sut.pool.twap_median_price(&asset_1, &1_000_000_000); +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #102)")] +fn should_fail_when_price_is_not_stale() { + let env = Env::default(); + env.mock_all_auths(); + + let sut = init_pool(&env, false); + + let asset_1 = sut.reserves[1].token.address.clone(); + + let asset_1_feed_1 = create_price_feed_contract(&env); + + asset_1_feed_1.init( + &Asset::Stellar(asset_1.clone()), + &vec![ + &env, + PriceData { + price: 120_000_000_000_000_000, + timestamp: 1704790200, // delta = 300_000 + }, + PriceData { + price: 100_000_000_000_000_000, + timestamp: 1704789900, // delta = 300_000 + }, + PriceData { + price: 90_000_000_000_000_000, + timestamp: 1704789600, // delta = 300_000 + }, + PriceData { + price: 100_000_000_000_000_000, + timestamp: 1704789300, // delta = 0 + }, + ], + ); + + let price_feeds = vec![ + &env, + PriceFeedConfigInput { + asset: asset_1.clone(), + asset_decimals: 9, + min_sanity_price_in_base: 5_000_000, + max_sanity_price_in_base: 50_000_000_000, + feeds: vec![ + &env, + PriceFeed { + feed: asset_1_feed_1.address.clone(), + feed_asset: OracleAsset::Stellar(asset_1.clone()), + feed_decimals: 10, + twap_records: 10, + min_timestamp_delta: 200, + timestamp_precision: TimestampPrecision::Sec, + }, + ], + }, + ]; + + sut.pool.set_price_feeds(&price_feeds); + + set_time(&env, &sut, 1704790800, false); // delta = 600 + extern crate std; + + sut.pool.twap_median_price(&asset_1, &1_000_000_000); +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #103)")] +fn should_fail_when_price_is_below_min_sanity() { + let env = Env::default(); + env.mock_all_auths(); + + let sut = init_pool(&env, false); + + let asset_1 = sut.reserves[1].token.address.clone(); + + let asset_1_feed_1 = create_price_feed_contract(&env); + + asset_1_feed_1.init( + &Asset::Stellar(asset_1.clone()), + &vec![ + &env, + PriceData { + price: 45_500_000_000, + timestamp: 1704790200, // delta = 300_000 + }, + PriceData { + price: 65_500_000_000, + timestamp: 1704789900, // delta = 300_000 + }, + PriceData { + price: 25_500_000_000, + timestamp: 1704789600, // delta = 300_000 + }, + PriceData { + price: 55_500_000_000, + timestamp: 1704789300, // delta = 0 + }, + ], + ); + + let price_feeds = vec![ + &env, + PriceFeedConfigInput { + asset: asset_1.clone(), + asset_decimals: 9, + min_sanity_price_in_base: 50_000_000, + max_sanity_price_in_base: 50_000_000_000, + feeds: vec![ + &env, + PriceFeed { + feed: asset_1_feed_1.address.clone(), + feed_asset: OracleAsset::Stellar(asset_1.clone()), + feed_decimals: 10, + twap_records: 10, + min_timestamp_delta: 1_000_000, + timestamp_precision: TimestampPrecision::Sec, + }, + ], + }, + ]; + + sut.pool.set_price_feeds(&price_feeds); + + set_time(&env, &sut, 1704790800, false); // delta = 600 + extern crate std; + + sut.pool.twap_median_price(&asset_1, &1_000_000_000); +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #103)")] +fn should_fail_when_price_is_below_max_sanity() { + let env = Env::default(); + env.mock_all_auths(); + + let sut = init_pool(&env, false); + + let asset_1 = sut.reserves[1].token.address.clone(); + + let asset_1_feed_1 = create_price_feed_contract(&env); + + asset_1_feed_1.init( + &Asset::Stellar(asset_1.clone()), + &vec![ + &env, + PriceData { + price: 1_000_000_000_000, + timestamp: 1704790200, // delta = 300_000 + }, + PriceData { + price: 990_000_000_000, + timestamp: 1704789900, // delta = 300_000 + }, + PriceData { + price: 1_200_000_000_000, + timestamp: 1704789600, // delta = 300_000 + }, + PriceData { + price: 1_800_000_000_000, + timestamp: 1704789300, // delta = 0 + }, + ], + ); + + let price_feeds = vec![ + &env, + PriceFeedConfigInput { + asset: asset_1.clone(), + asset_decimals: 9, + min_sanity_price_in_base: 5_000_000, + max_sanity_price_in_base: 1_000_000_000, + feeds: vec![ + &env, + PriceFeed { + feed: asset_1_feed_1.address.clone(), + feed_asset: OracleAsset::Stellar(asset_1.clone()), + feed_decimals: 10, + twap_records: 10, + min_timestamp_delta: 1_000_000, + timestamp_precision: TimestampPrecision::Sec, + }, + ], + }, + ]; + + sut.pool.set_price_feeds(&price_feeds); + + set_time(&env, &sut, 1704790800, false); // delta = 600 + extern crate std; sut.pool.twap_median_price(&asset_1, &1_000_000_000); } @@ -174,6 +433,8 @@ fn should_return_twap_median_price() { PriceFeedConfigInput { asset: asset_1.clone(), asset_decimals: 9, + min_sanity_price_in_base: 5_000_000, + max_sanity_price_in_base: 50_000_000_000, feeds: vec![ &env, // price feed with Stellar asset @@ -182,6 +443,7 @@ fn should_return_twap_median_price() { feed_asset: OracleAsset::Stellar(asset_1.clone()), feed_decimals: 14, twap_records: 10, + min_timestamp_delta: 1_000_000, timestamp_precision: TimestampPrecision::Sec, }, // price feed with Other asset @@ -190,6 +452,7 @@ fn should_return_twap_median_price() { feed_asset: OracleAsset::Other(symbol_short!("XRP")), feed_decimals: 10, twap_records: 10, + min_timestamp_delta: 1_000_000, timestamp_precision: TimestampPrecision::Msec, }, ], @@ -198,7 +461,7 @@ fn should_return_twap_median_price() { sut.pool.set_price_feeds(&price_feeds); - env.ledger().with_mut(|li| li.timestamp = 1704790800); // delta = 600_000 + set_time(&env, &sut, 1704790800, false); // delta = 600_000 extern crate std; let twap_median_price_1 = sut.pool.twap_median_price(&asset_1, &1_000_000_000); @@ -222,6 +485,8 @@ fn should_return_twap_median_price() { PriceFeedConfigInput { asset: asset_1.clone(), asset_decimals: 9, + min_sanity_price_in_base: 5_000_000, + max_sanity_price_in_base: 50_000_000_000, feeds: vec![ &env, // price feed with Stellar asset @@ -230,6 +495,7 @@ fn should_return_twap_median_price() { feed_asset: OracleAsset::Stellar(asset_1.clone()), feed_decimals: 14, twap_records: 10, + min_timestamp_delta: 1_000_000, timestamp_precision: TimestampPrecision::Sec, }, // price feed with Other asset @@ -238,6 +504,7 @@ fn should_return_twap_median_price() { feed_asset: OracleAsset::Other(symbol_short!("XRP")), feed_decimals: 10, twap_records: 10, + min_timestamp_delta: 1_000_000, timestamp_precision: TimestampPrecision::Msec, }, PriceFeed { @@ -245,6 +512,7 @@ fn should_return_twap_median_price() { feed_asset: OracleAsset::Other(symbol_short!("XRP")), feed_decimals: 10, twap_records: 10, + min_timestamp_delta: 1_000_000, timestamp_precision: TimestampPrecision::Msec, }, ], @@ -258,3 +526,188 @@ fn should_return_twap_median_price() { // median([1_060, 1_000, 1_090]) = 1_060 assert_eq!(twap_median_price_2, 10_600_000_000); } + +#[test] +fn should_return_twap_median_price_when_unsorted_prices() { + let env = Env::default(); + env.mock_all_auths(); + + let sut = init_pool(&env, false); + + let asset_1 = sut.reserves[1].token.address.clone(); + + let asset_1_feed_1 = create_price_feed_contract(&env); + let asset_1_feed_2 = create_price_feed_contract(&env); + + asset_1_feed_1.init( + &Asset::Stellar(asset_1.clone()), + &vec![ + &env, + PriceData { + price: 100_000_000_000_000_000, + timestamp: 1704789300, + }, + PriceData { + price: 100_000_000_000_000_000, + timestamp: 1704789900, + }, + PriceData { + price: 120_000_000_000_000_000, + timestamp: 1704790200, + }, + PriceData { + price: 90_000_000_000_000_000, + timestamp: 1704789600, + }, + ], + ); + + asset_1_feed_2.init( + &Asset::Other(symbol_short!("XRP")), + &vec![ + &env, + PriceData { + price: 10_000_000_000_000, + timestamp: 1704790200000, + }, + ], + ); + + let price_feeds = vec![ + &env, + PriceFeedConfigInput { + asset: asset_1.clone(), + asset_decimals: 9, + min_sanity_price_in_base: 5_000_000, + max_sanity_price_in_base: 50_000_000_000, + feeds: vec![ + &env, + // price feed with Stellar asset + PriceFeed { + feed: asset_1_feed_1.address.clone(), + feed_asset: OracleAsset::Stellar(asset_1.clone()), + feed_decimals: 14, + twap_records: 10, + min_timestamp_delta: 1_000_000, + timestamp_precision: TimestampPrecision::Sec, + }, + // price feed with Other asset + PriceFeed { + feed: asset_1_feed_2.address.clone(), + feed_asset: OracleAsset::Other(symbol_short!("XRP")), + feed_decimals: 10, + twap_records: 10, + min_timestamp_delta: 1_000_000, + timestamp_precision: TimestampPrecision::Msec, + }, + ], + }, + ]; + + sut.pool.set_price_feeds(&price_feeds); + + set_time(&env, &sut, 1704790800, false); // delta = 600_000 + extern crate std; + + let twap_median_price_1 = sut.pool.twap_median_price(&asset_1, &1_000_000_000); + + // median([1_060, 1_000]) = 1_030 + assert_eq!(twap_median_price_1, 10_300_000_000); +} + +#[test] +fn should_use_backup_price_feed() { + let env = Env::default(); + env.mock_all_auths(); + + let sut = init_pool(&env, false); + + let asset_1 = sut.reserves[1].token.address.clone(); + + let asset_1_feed_1 = create_price_feed_contract(&env); + let asset_1_feed_2 = create_price_feed_contract(&env); + let asset_1_feed_3 = create_price_feed_contract(&env); + + asset_1_feed_3.init( + &Asset::Stellar(asset_1.clone()), + &vec![ + &env, + PriceData { + price: 120_000_000_000_000_000, + timestamp: 1704790200, // delta = 300_000 + }, + PriceData { + price: 100_000_000_000_000_000, + timestamp: 1704789900, // delta = 300_000 + }, + PriceData { + price: 90_000_000_000_000_000, + timestamp: 1704789600, // delta = 300_000 + }, + PriceData { + price: 100_000_000_000_000_000, + timestamp: 1704789300, // delta = 0 + }, + ], + ); + + asset_1_feed_2.init(&Asset::Other(symbol_short!("XRP")), &vec![&env]); + asset_1_feed_2.init( + &Asset::Other(symbol_short!("XRP")), + &vec![ + &env, + PriceData { + price: 10_000_000_000_000, + timestamp: 1704789700000, // not stale price + }, + ], + ); + + let price_feeds = vec![ + &env, + PriceFeedConfigInput { + asset: asset_1.clone(), + asset_decimals: 9, + min_sanity_price_in_base: 5_000_000, + max_sanity_price_in_base: 50_000_000_000, + feeds: vec![ + &env, + PriceFeed { + feed: asset_1_feed_1.address.clone(), + feed_asset: OracleAsset::Other(symbol_short!("XRP")), + feed_decimals: 10, + twap_records: 10, + min_timestamp_delta: 600_000, + timestamp_precision: TimestampPrecision::Msec, + }, + PriceFeed { + feed: asset_1_feed_2.address.clone(), + feed_asset: OracleAsset::Other(symbol_short!("XRP")), + feed_decimals: 10, + twap_records: 10, + min_timestamp_delta: 600_000, + timestamp_precision: TimestampPrecision::Msec, + }, + // backup price oracle + PriceFeed { + feed: asset_1_feed_3.address.clone(), + feed_asset: OracleAsset::Stellar(asset_1.clone()), + feed_decimals: 14, + twap_records: 10, + min_timestamp_delta: 600_000, + timestamp_precision: TimestampPrecision::Sec, + }, + ], + }, + ]; + + sut.pool.set_price_feeds(&price_feeds); + + set_time(&env, &sut, 1704790800, false); // delta = 600_000 + extern crate std; + + let twap_median_price_1 = sut.pool.twap_median_price(&asset_1, &1_000_000_000); + + // median([1_060]) = 1_030 + assert_eq!(twap_median_price_1, 10_600_000_000); +} diff --git a/contracts/pool/src/tests/upgrade.rs b/contracts/pool/src/tests/upgrade.rs index 75193e9c..7f0deae8 100644 --- a/contracts/pool/src/tests/upgrade.rs +++ b/contracts/pool/src/tests/upgrade.rs @@ -61,8 +61,8 @@ fn should_upgrade_contracts() { let s_token_version_before = sut.s_token().version(); let debt_token_version_before = sut.debt_token().version(); - sut.pool.upgrade_s_token(&asset, &s_token_v2_wasm); - sut.pool.upgrade_debt_token(&asset, &debt_token_v2_wasm); + sut.pool.upgrade_token(&asset, &true, &s_token_v2_wasm); + sut.pool.upgrade_token(&asset, &false, &debt_token_v2_wasm); sut.pool.upgrade(&pool_v2_wasm); let pool_version_after = sut.pool.version(); diff --git a/contracts/pool/src/tests/user_configuration.rs b/contracts/pool/src/tests/user_configuration.rs index 26e1a0fc..0efe5e0a 100644 --- a/contracts/pool/src/tests/user_configuration.rs +++ b/contracts/pool/src/tests/user_configuration.rs @@ -3,7 +3,7 @@ use crate::tests::sut::init_pool; use crate::*; #[test] -#[should_panic(expected = "HostError: Error(Contract, #202)")] +#[should_panic(expected = "HostError: Error(Contract, #1)")] fn should_fail_when_user_config_not_exist() { let env = Env::default(); env.mock_all_auths(); diff --git a/contracts/pool/src/tests/withdraw.rs b/contracts/pool/src/tests/withdraw.rs index e45ff57b..86df3caa 100644 --- a/contracts/pool/src/tests/withdraw.rs +++ b/contracts/pool/src/tests/withdraw.rs @@ -2,8 +2,9 @@ use super::sut::fill_pool; use crate::tests::sut::{fill_pool_two, init_pool, DAY}; use crate::*; use soroban_sdk::symbol_short; -use soroban_sdk::testutils::{Address as _, AuthorizedFunction, Events, Ledger}; +use soroban_sdk::testutils::{Address as _, AuthorizedFunction, Events}; use soroban_sdk::{vec, IntoVal, Symbol}; +use tests::sut::set_time; #[test] fn should_require_authorized_caller() { @@ -14,7 +15,7 @@ fn should_require_authorized_caller() { let (_, borrower, _) = fill_pool(&env, &sut, true); let token_address = sut.token().address.clone(); - env.ledger().with_mut(|li| li.timestamp = 2 * DAY); + set_time(&env, &sut, 2 * DAY, false); sut.pool .withdraw(&borrower, &token_address, &10_000, &borrower); @@ -36,7 +37,7 @@ fn should_require_authorized_caller() { } #[test] -#[should_panic(expected = "HostError: Error(Contract, #3)")] +#[should_panic(expected = "HostError: Error(Contract, #2)")] fn should_fail_when_pool_paused() { let env = Env::default(); env.mock_all_auths(); @@ -45,7 +46,7 @@ fn should_fail_when_pool_paused() { let (_, borrower, _) = fill_pool(&env, &sut, true); let token_address = sut.token().address.clone(); - env.ledger().with_mut(|li| li.timestamp = 2 * DAY); + set_time(&env, &sut, 2 * DAY, false); sut.pool.set_pause(&true); sut.pool @@ -53,7 +54,7 @@ fn should_fail_when_pool_paused() { } #[test] -#[should_panic(expected = "HostError: Error(Contract, #304)")] +#[should_panic(expected = "HostError: Error(Contract, #302)")] fn should_fail_when_invalid_amount() { let env = Env::default(); env.mock_all_auths(); @@ -62,13 +63,13 @@ fn should_fail_when_invalid_amount() { let (_, borrower, _) = fill_pool(&env, &sut, true); let token_address = sut.token().address.clone(); - env.ledger().with_mut(|li| li.timestamp = 2 * DAY); + set_time(&env, &sut, 2 * DAY, false); sut.pool.withdraw(&borrower, &token_address, &-1, &borrower); } #[test] -#[should_panic(expected = "HostError: Error(Contract, #101)")] +#[should_panic(expected = "HostError: Error(Contract, #100)")] fn should_fail_when_reserve_deactivated() { let env = Env::default(); env.mock_all_auths(); @@ -77,7 +78,7 @@ fn should_fail_when_reserve_deactivated() { let (_, borrower, _) = fill_pool(&env, &sut, true); let token_address = sut.token().address.clone(); - env.ledger().with_mut(|li| li.timestamp = 2 * DAY); + set_time(&env, &sut, 2 * DAY, false); sut.pool.set_reserve_status(&token_address, &false); sut.pool @@ -85,8 +86,8 @@ fn should_fail_when_reserve_deactivated() { } #[test] -#[should_panic(expected = "HostError: Error(Contract, #302)")] -fn should_fail_when_bad_position() { +#[should_panic(expected = "HostError: Error(Contract, #3)")] +fn should_fail_when_bellow_initial_health() { let env = Env::default(); env.mock_all_auths(); @@ -94,14 +95,14 @@ fn should_fail_when_bad_position() { let (_, borrower, _) = fill_pool(&env, &sut, true); let token_address = sut.token().address.clone(); - env.ledger().with_mut(|li| li.timestamp = 2 * DAY); + set_time(&env, &sut, 2 * DAY, false); sut.pool .withdraw(&borrower, &token_address, &50_000_000, &borrower); } #[test] -#[should_panic(expected = "HostError: Error(Contract, #100)")] +#[should_panic(expected = "HostError: Error(Contract, #1)")] fn should_fail_when_unknown_asset() { let env = Env::default(); env.mock_all_auths(); @@ -110,7 +111,7 @@ fn should_fail_when_unknown_asset() { let sut = init_pool(&env, false); let (_, borrower, _) = fill_pool(&env, &sut, true); - env.ledger().with_mut(|li| li.timestamp = 2 * DAY); + set_time(&env, &sut, 2 * DAY, false); sut.pool .withdraw(&borrower, &unknown_asset, &1_000_000, &borrower); @@ -140,10 +141,12 @@ fn should_change_user_config() { user_config_before.is_using_as_collateral(&env, reserve.get_id()), true ); + assert_eq!(user_config_before.total_assets(), 1); assert_eq!( user_config.is_using_as_collateral(&env, reserve.get_id()), false ); + assert_eq!(user_config.total_assets(), 0); } #[test] @@ -156,34 +159,32 @@ fn should_partially_withdraw() { let (lender, _, _, debt_config) = fill_pool_two(&env, &sut); let debt_token = &debt_config.token.address; - env.ledger().with_mut(|li| li.timestamp = 60 * DAY); + set_time(&env, &sut, 60 * DAY, false); let s_token_supply_before = debt_config.s_token().total_supply(); let lender_stoken_balance_before = debt_config.s_token().balance(&lender); let lender_underlying_balance_before = debt_config.token.balance(&lender); let s_token_underlying_supply_before = sut .pool - .stoken_underlying_balance(&debt_config.s_token().address); + .token_balance(&debt_config.token.address, &debt_config.s_token().address); sut.pool.withdraw(&lender, debt_token, &50_000_000, &lender); - env.ledger().with_mut(|li| li.timestamp = 60 * DAY + 1); - let lender_stoken_balance = debt_config.s_token().balance(&lender); let lender_underlying_balance = debt_config.token.balance(&lender); let s_token_supply = debt_config.s_token().total_supply(); let s_token_underlying_supply = sut .pool - .stoken_underlying_balance(&debt_config.s_token().address); + .token_balance(&debt_config.token.address, &debt_config.s_token().address); assert_eq!(lender_stoken_balance_before, 100_000_000); assert_eq!(lender_underlying_balance_before, 900_000_000); - assert_eq!(s_token_supply_before, 199_991_811); + assert_eq!(s_token_supply_before, 199_991_812); assert_eq!(s_token_underlying_supply_before, 160_000_000); - assert_eq!(lender_stoken_balance, 50_043_049); + assert_eq!(lender_stoken_balance, 50_043_047); assert_eq!(lender_underlying_balance, 950_000_000); - assert_eq!(s_token_supply, 150_034_860); + assert_eq!(s_token_supply, 150_034_859); assert_eq!(s_token_underlying_supply, 110_000_000); } @@ -197,35 +198,35 @@ fn should_fully_withdraw() { let (lender, _, _, debt_config) = fill_pool_two(&env, &sut); let debt_token = &debt_config.token.address; - env.ledger().with_mut(|li| li.timestamp = 60 * DAY); + set_time(&env, &sut, 60 * DAY, false); let s_token_supply_before = debt_config.s_token().total_supply(); let lender_stoken_balance_before = debt_config.s_token().balance(&lender); let lender_underlying_balance_before = debt_config.token.balance(&lender); let s_token_underlying_supply_before = sut .pool - .stoken_underlying_balance(&debt_config.s_token().address); + .token_balance(&debt_config.token.address, &debt_config.s_token().address); sut.pool.withdraw(&lender, debt_token, &i128::MAX, &lender); - env.ledger().with_mut(|li| li.timestamp = 60 * DAY + 1); + set_time(&env, &sut, 60 * DAY + 1, false); let lender_stoken_balance = debt_config.s_token().balance(&lender); let lender_underlying_balance = debt_config.token.balance(&lender); let s_token_supply = debt_config.s_token().total_supply(); let s_token_underlying_supply = sut .pool - .stoken_underlying_balance(&debt_config.s_token().address); + .token_balance(&debt_config.token.address, &debt_config.s_token().address); assert_eq!(lender_stoken_balance_before, 100_000_000); assert_eq!(lender_underlying_balance_before, 900_000_000); - assert_eq!(s_token_supply_before, 199_991_811); + assert_eq!(s_token_supply_before, 199_991_812); assert_eq!(s_token_underlying_supply_before, 160_000_000); assert_eq!(lender_stoken_balance, 0); - assert_eq!(lender_underlying_balance, 1_000_086_170); - assert_eq!(s_token_supply, 99_991_811); - assert_eq!(s_token_underlying_supply, 59_913_830); + assert_eq!(lender_underlying_balance, 1_000_086_169); + assert_eq!(s_token_supply, 99_991_812); + assert_eq!(s_token_underlying_supply, 59_913_831); } #[test] @@ -238,14 +239,14 @@ fn should_affect_coeffs() { let (lender, _, _, debt_config) = fill_pool_two(&env, &sut); let debt_token = &debt_config.token.address; - env.ledger().with_mut(|li| li.timestamp = 2 * DAY); + set_time(&env, &sut, 2 * DAY, false); let collat_coeff_prev = sut.pool.collat_coeff(&debt_token); let debt_coeff_prev = sut.pool.debt_coeff(&debt_token); sut.pool.withdraw(&lender, debt_token, &i128::MAX, &lender); - env.ledger().with_mut(|li| li.timestamp = 3 * DAY); + set_time(&env, &sut, 3 * DAY, false); let collat_coeff = sut.pool.collat_coeff(&debt_token); let debt_coeff = sut.pool.debt_coeff(&debt_token); @@ -265,14 +266,14 @@ fn should_affect_account_data() { let account_position_prev = sut.pool.account_position(&borrower); - env.ledger().with_mut(|li| li.timestamp = 2 * DAY); + set_time(&env, &sut, 2 * DAY, false); sut.pool .withdraw(&borrower, &token_address, &100_000, &borrower); let account_position = sut.pool.account_position(&borrower); - env.ledger().with_mut(|li| li.timestamp = 2 * DAY + 1); + set_time(&env, &sut, 2 * DAY + 1, false); let collat_token_total_supply = sut.s_token().total_supply(); let pool_collat_token_total_supply = sut.pool.token_total_supply(&sut.s_token().address); @@ -298,7 +299,7 @@ fn should_allow_withdraw_to_other_address() { let (lender, borrower, _, debt_config) = fill_pool_two(&env, &sut); let debt_token = &debt_config.token.address; - env.ledger().with_mut(|li| li.timestamp = 60 * DAY); + set_time(&env, &sut, 60 * DAY, false); let borrower_underlying_balance_before = debt_config.token.balance(&borrower); let lender_stoken_balance_before = debt_config.s_token().balance(&lender); @@ -306,31 +307,29 @@ fn should_allow_withdraw_to_other_address() { let s_token_supply_before = debt_config.s_token().total_supply(); let s_token_underlying_supply_before = sut .pool - .stoken_underlying_balance(&debt_config.s_token().address); + .token_balance(&debt_config.token.address, &debt_config.s_token().address); sut.pool .withdraw(&lender, debt_token, &50_000_000, &borrower); - env.ledger().with_mut(|li| li.timestamp = 60 * DAY + 1); - let borrower_underlying_balance = debt_config.token.balance(&borrower); let lender_stoken_balance = debt_config.s_token().balance(&lender); let lender_underlying_balance = debt_config.token.balance(&lender); let s_token_supply = debt_config.s_token().total_supply(); let s_token_underlying_supply = sut .pool - .stoken_underlying_balance(&debt_config.s_token().address); + .token_balance(&debt_config.token.address, &debt_config.s_token().address); assert_eq!(borrower_underlying_balance_before, 900_000_000); assert_eq!(lender_stoken_balance_before, 100_000_000); assert_eq!(lender_underlying_balance_before, 900_000_000); - assert_eq!(s_token_supply_before, 199_991_811); + assert_eq!(s_token_supply_before, 199_991_812); assert_eq!(s_token_underlying_supply_before, 160_000_000); assert_eq!(borrower_underlying_balance, 950000000); - assert_eq!(lender_stoken_balance, 50_043_049); + assert_eq!(lender_stoken_balance, 50_043_047); assert_eq!(lender_underlying_balance, 900_000_000); - assert_eq!(s_token_supply, 150_034_860); + assert_eq!(s_token_supply, 150_034_859); assert_eq!(s_token_underlying_supply, 110_000_000); } @@ -366,7 +365,7 @@ fn should_emit_events() { ] ); - let event = events.pop_back_unchecked(); + let event = events.get(23).unwrap(); assert_eq!( vec![&env, event], @@ -398,7 +397,7 @@ fn rwa_partially_withdraw() { sut.pool .deposit(&lender, &rwa_config.token.address, &100_000_000); - env.ledger().with_mut(|li| li.timestamp = 60 * DAY); + set_time(&env, &sut, 60 * DAY, false); let lender_rwa_balance_before = rwa_config.token.balance(&lender); let pool_rwa_balance_before = rwa_config.token.balance(&sut.pool.address); @@ -406,7 +405,7 @@ fn rwa_partially_withdraw() { sut.pool .withdraw(&lender, &rwa_config.token.address, &50_000_000, &lender); - env.ledger().with_mut(|li| li.timestamp = 60 * DAY + 1); + set_time(&env, &sut, 60 * DAY + 1, false); let lender_rwa_balance = rwa_config.token.balance(&lender); let pool_rwa_balance = rwa_config.token.balance(&sut.pool.address); @@ -430,7 +429,7 @@ fn rwa_fully_withdraw() { sut.pool .deposit(&lender, &rwa_config.token.address, &100_000_000); - env.ledger().with_mut(|li| li.timestamp = 60 * DAY); + set_time(&env, &sut, 60 * DAY, false); let lender_rwa_balance_before = rwa_config.token.balance(&lender); let pool_rwa_balance_before = rwa_config.token.balance(&sut.pool.address); @@ -438,7 +437,7 @@ fn rwa_fully_withdraw() { sut.pool .withdraw(&lender, &rwa_config.token.address, &i128::MAX, &lender); - env.ledger().with_mut(|li| li.timestamp = 60 * DAY + 1); + set_time(&env, &sut, 60 * DAY + 1, false); let lender_rwa_balance = rwa_config.token.balance(&lender); let pool_rwa_balance = rwa_config.token.balance(&sut.pool.address); @@ -463,7 +462,7 @@ fn rwa_should_not_affect_coeffs() { sut.pool .deposit(&lender, &rwa_config.token.address, &100_000_000); - env.ledger().with_mut(|li| li.timestamp = 2 * DAY); + set_time(&env, &sut, 2 * DAY, false); let collat_coeff_prev = sut.pool.collat_coeff(debt_token); let debt_coeff_prev = sut.pool.debt_coeff(debt_token); @@ -493,14 +492,14 @@ fn rwa_should_affect_account_data() { let account_position_prev = sut.pool.account_position(&borrower); - env.ledger().with_mut(|li| li.timestamp = 2 * DAY); + set_time(&env, &sut, 2 * DAY, false); sut.pool .withdraw(&borrower, rwa_address, &100_000, &borrower); let account_position = sut.pool.account_position(&borrower); - env.ledger().with_mut(|li| li.timestamp = 2 * DAY + 1); + set_time(&env, &sut, 2 * DAY + 1, false); let collat_token_total_supply = sut.s_token().total_supply(); let pool_collat_token_total_supply = sut.pool.token_total_supply(&sut.s_token().address); @@ -517,13 +516,30 @@ fn rwa_should_affect_account_data() { } #[test] -#[should_panic(expected = "HostError: Error(Contract, #302)")] +#[should_panic(expected = "HostError: Error(Contract, #3)")] fn should_fail_when_bad_position_after_withdraw() { let env = Env::default(); env.mock_all_auths(); let sut = init_pool(&env, false); + sut.pool.set_pool_configuration(&PoolConfig { + base_asset_address: sut.reserves[0].token.address.clone(), + base_asset_decimals: sut.reserves[0].token.decimals(), + flash_loan_fee: 5, + initial_health: 2_500, + timestamp_window: 20, + grace_period: 1, + user_assets_limit: 4, + min_collat_amount: 0, + min_debt_amount: 0, + liquidation_protocol_fee: 0, + ir_alpha: 143, + ir_initial_rate: 200, + ir_max_rate: 50_000, + ir_scaling_coeff: 9_000, + }); + let lender = Address::generate(&env); let borrower = Address::generate(&env); sut.reserves[0].token_admin.mint(&lender, &1_000_000_000); @@ -548,3 +564,102 @@ fn should_fail_when_bad_position_after_withdraw() { &borrower, ); } + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #3)")] +fn should_fail_when_collat_lt_min_position_amount() { + let env = Env::default(); + env.mock_all_auths(); + + let sut = init_pool(&env, false); + + sut.pool.set_pool_configuration(&PoolConfig { + base_asset_address: sut.reserves[0].token.address.clone(), + base_asset_decimals: sut.reserves[0].token.decimals(), + flash_loan_fee: 5, + initial_health: 2_500, + timestamp_window: 20, + grace_period: 1, + user_assets_limit: 4, + min_collat_amount: 115_000_000, + min_debt_amount: 0, + liquidation_protocol_fee: 0, + ir_alpha: 143, + ir_initial_rate: 200, + ir_max_rate: 50_000, + ir_scaling_coeff: 9_000, + }); + + let lender = Address::generate(&env); + let borrower = Address::generate(&env); + sut.reserves[0].token_admin.mint(&lender, &1_000_000_000); + sut.reserves[1] + .token_admin + .mint(&borrower, &100_000_000_000); + + sut.pool + .deposit(&lender, &sut.reserves[0].token.address, &500_000_000); + sut.pool + .deposit(&borrower, &sut.reserves[1].token.address, &20_000_000_000); + + sut.pool + .borrow(&borrower, &sut.reserves[0].token.address, &50_000_000); + sut.pool + .borrow(&borrower, &sut.reserves[0].token.address, &39_000_000); + + sut.pool.withdraw( + &borrower, + &sut.reserves[1].token.address, + &1_000_000_000, + &borrower, + ); +} + +#[test] +#[should_panic(expected = "HostError: Error(Contract, #5)")] +fn should_fail_in_grace_period() { + let env = Env::default(); + env.mock_all_auths(); + + let sut = init_pool(&env, false); + let (_, borrower, _) = fill_pool(&env, &sut, false); + let collat_address = sut.reserves[0].token.address.clone(); + sut.pool.withdraw(&borrower, &collat_address, &1, &borrower); + + sut.pool.set_pause(&true); + sut.pool.set_pause(&false); + sut.pool.withdraw(&borrower, &collat_address, &1, &borrower); +} + +#[test] +fn should_not_fail_after_grace_period() { + let env = Env::default(); + env.mock_all_auths(); + + let sut = init_pool(&env, false); + let (_, borrower, _) = fill_pool(&env, &sut, false); + let collat_address = sut.reserves[0].token.address.clone(); + let pause_info = sut.pool.pause_info(); + let start = env.ledger().timestamp(); + let gap = 500; + + let s_token_before = sut.reserves[0].s_token().balance(&borrower); + sut.pool.withdraw(&borrower, &collat_address, &1, &borrower); + let s_token_after = sut.reserves[0].debt_token().balance(&borrower); + assert!(s_token_after < s_token_before); + + sut.pool.set_pause(&true); + set_time(&env, &sut, start + gap, false); + sut.pool.set_pause(&false); + set_time( + &env, + &sut, + start + gap + pause_info.grace_period_secs, + false, + ); + + let s_token_before = sut.reserves[0].s_token().balance(&borrower); + sut.pool.withdraw(&borrower, &collat_address, &1, &borrower); + let s_token_after = sut.reserves[0].debt_token().balance(&borrower); + assert!(s_token_after < s_token_before); +} diff --git a/contracts/pool/src/types/account_data.rs b/contracts/pool/src/types/account_data.rs index ccfbafa1..e527ab97 100644 --- a/contracts/pool/src/types/account_data.rs +++ b/contracts/pool/src/types/account_data.rs @@ -5,15 +5,11 @@ use super::liquidation_asset::LiquidationAsset; #[derive(Debug, Clone, Default)] pub struct AccountData { - /// Total collateral expresed in XLM pub discounted_collateral: i128, - /// Total debt expressed in XLM pub debt: i128, - /// Net position value in XLM pub npv: i128, - /// Liquidation debt ordered by max utilization + pub collat: Option, pub liq_debts: Option>, - /// Liquidation collateral ordered by liquidation_order pub liq_collats: Option>, } diff --git a/contracts/pool/src/types/price_provider.rs b/contracts/pool/src/types/price_provider.rs index 6c769d95..de385f05 100644 --- a/contracts/pool/src/types/price_provider.rs +++ b/contracts/pool/src/types/price_provider.rs @@ -1,63 +1,96 @@ use core::ops::Div; -use common::FixedI128; -use pool_interface::types::base_asset_config::BaseAssetConfig; use pool_interface::types::error::Error; +use pool_interface::types::pool_config::PoolConfig; use pool_interface::types::price_feed::PriceFeed; use pool_interface::types::price_feed_config::PriceFeedConfig; use pool_interface::types::timestamp_precision::TimestampPrecision; use price_feed_interface::PriceFeedClient; -use soroban_sdk::{Address, Env, Map, Vec}; +use soroban_sdk::{assert_with_error, Address, Env, Map, Vec}; -use crate::storage::{read_base_asset, read_price_feeds}; +use crate::storage::read_price_feeds; pub struct PriceProvider<'a> { env: &'a Env, - base_asset: BaseAssetConfig, + base_asset_address: Address, + base_asset_decimals: u32, configs: Map, prices: Map, } impl<'a> PriceProvider<'a> { - pub fn new(env: &'a Env) -> Result { - let base_asset = read_base_asset(env)?; - + pub fn new(env: &'a Env, pool_config: &PoolConfig) -> Result { Ok(Self { env, - base_asset, + base_asset_address: pool_config.base_asset_address.clone(), + base_asset_decimals: pool_config.base_asset_decimals, configs: Map::new(env), prices: Map::new(env), }) } pub fn convert_to_base(&mut self, asset: &Address, amount: i128) -> Result { - if self.base_asset.address == *asset { + if self.base_asset_address == *asset { return Ok(amount); } let config = self.config(asset)?; - let median_twap_price = self.price(asset, &config)?; + let median_twap_price = self.price_in_base(asset, &config)?; + + let precision = 10i128 + .checked_pow(config.asset_decimals) + .ok_or(Error::MathOverflowError)?; median_twap_price - .mul_int(amount) - .and_then(|a| FixedI128::from_rational(a, 10i128.pow(config.asset_decimals))) - .and_then(|a| a.to_precision(self.base_asset.decimals)) + .checked_mul(amount) + .ok_or(Error::InvalidAssetPrice)? + .checked_div(precision) .ok_or(Error::InvalidAssetPrice) } - pub fn convert_from_base(&mut self, asset: &Address, amount: i128) -> Result { - if self.base_asset.address == *asset { + pub fn convert_from_base( + &mut self, + asset: &Address, + amount: i128, + round_ceil: bool, + ) -> Result { + if self.base_asset_address == *asset { return Ok(amount); } let config = self.config(asset)?; - let median_twap_price = self.price(asset, &config)?; + let median_twap_price = self.price_in_base(asset, &config)?; - median_twap_price - .recip_mul_int(amount) - .and_then(|a| FixedI128::from_rational(a, 10i128.pow(self.base_asset.decimals))) - .and_then(|a| a.to_precision(config.asset_decimals)) - .ok_or(Error::InvalidAssetPrice) + let precision = 10i128 + .checked_pow(config.asset_decimals) + .ok_or(Error::MathOverflowError)?; + + if round_ceil { + amount + .checked_mul(precision) + .ok_or(Error::InvalidAssetPrice)? + .checked_div(median_twap_price) + .ok_or(Error::InvalidAssetPrice) + } else { + amount + .checked_mul(precision) + .ok_or(Error::InvalidAssetPrice)? + .checked_div(median_twap_price) + .map(|res| { + let res_1 = res.abs(); + let other_1 = amount.abs(); + let self_1 = median_twap_price.abs(); + + if res_1 == 0 { + 1 + } else if other_1 % self_1 == 0 { + res + } else { + res + 1 + } + }) + .ok_or(Error::InvalidAssetPrice) + } } fn config(&mut self, asset: &Address) -> Result { @@ -72,32 +105,56 @@ impl<'a> PriceProvider<'a> { } } - fn price(&mut self, asset: &Address, config: &PriceFeedConfig) -> Result { + fn price_in_base(&mut self, asset: &Address, config: &PriceFeedConfig) -> Result { let price = self.prices.get(asset.clone()); - let price = match price { - Some(price) => price, + match price { + Some(price) => Ok(price), None => { let mut sorted_twap_prices = Map::new(self.env); for feed in config.feeds.iter() { - let twap_price = - FixedI128::from_rational(self.twap(&feed)?, 10i128.pow(feed.feed_decimals)) - .ok_or(Error::MathOverflowError)? - .into_inner(); + let twap = self.twap(&feed); + + if twap.is_err() { + continue; + } + + let base_precision = 10i128 + .checked_pow(self.base_asset_decimals) + .ok_or(Error::MathOverflowError)?; + + let feed_precision = 10i128 + .checked_pow(feed.feed_decimals) + .ok_or(Error::MathOverflowError)?; + + let twap_price = twap? + .checked_mul(base_precision) + .ok_or(Error::MathOverflowError)? + .checked_div(feed_precision) + .ok_or(Error::MathOverflowError)?; + + let is_sanity_price = twap_price >= config.min_sanity_price_in_base + && twap_price <= config.max_sanity_price_in_base; + + assert_with_error!(self.env, is_sanity_price, Error::InvalidAssetPrice); sorted_twap_prices.set(twap_price, twap_price); } + assert_with_error!( + self.env, + !sorted_twap_prices.is_empty(), + Error::NoPriceForAsset + ); + let median_twap_price = self.median(&sorted_twap_prices.keys())?; self.prices.set(asset.clone(), median_twap_price); - median_twap_price + Ok(median_twap_price) } - }; - - Ok(FixedI128::from_inner(price)) + } } fn twap(&mut self, config: &PriceFeed) -> Result { @@ -113,40 +170,56 @@ impl<'a> PriceProvider<'a> { let prices_len = prices.len(); - if prices_len == 1 { - return Ok(prices.first_unchecked().price); + let curr_time = precise_timestamp(self.env, &config.timestamp_precision); + + let mut sorted_prices = Map::new(self.env); + + for price in prices { + sorted_prices.set(price.timestamp, price.price); } - let curr_time = precise_timestamp(self.env, &config.timestamp_precision); + let prices = sorted_prices.values(); + let timestamps = sorted_prices.keys(); + + let timestamp_delta = curr_time + .checked_sub(timestamps.last_unchecked()) + .ok_or(Error::MathOverflowError)?; + + if timestamp_delta > config.min_timestamp_delta { + return Err(Error::NoPriceForAsset); + } + + if prices_len == 1 { + return Ok(sorted_prices.values().first_unchecked()); + } let mut cum_price = { - let price_curr = prices.get_unchecked(0); + let price_curr = prices.last_unchecked(); + let timestamp_curr = timestamps.last_unchecked(); let time_delta = curr_time - .checked_sub(price_curr.timestamp) + .checked_sub(timestamp_curr) .ok_or(Error::MathOverflowError)?; if time_delta.eq(&0) { - price_curr.price + price_curr } else { price_curr - .price .checked_mul(time_delta.into()) .ok_or(Error::MathOverflowError)? } }; - for i in 1..prices_len { - let price_prev = prices.get_unchecked(i - 1); - let price_curr = prices.get_unchecked(i); + for i in (1..prices_len).rev() { + let price_curr = prices.get_unchecked(i - 1); + let timestamp_curr = timestamps.get_unchecked(i - 1); + let timestamp_prev = timestamps.get_unchecked(i); - let time_delta = price_prev - .timestamp - .checked_sub(price_curr.timestamp) + let time_delta = timestamp_prev + .checked_sub(timestamp_curr) .ok_or(Error::MathOverflowError)?; let tw_price = price_curr - .price .checked_mul(time_delta.into()) .ok_or(Error::MathOverflowError)?; @@ -156,7 +229,7 @@ impl<'a> PriceProvider<'a> { } let twap_time = curr_time - .checked_sub(prices.last_unchecked().timestamp) + .checked_sub(timestamps.first_unchecked()) .ok_or(Error::MathOverflowError)?; let twap_price = cum_price diff --git a/contracts/pool/src/types/user_configurator.rs b/contracts/pool/src/types/user_configurator.rs index ba00876d..d3d51f61 100644 --- a/contracts/pool/src/types/user_configurator.rs +++ b/contracts/pool/src/types/user_configurator.rs @@ -3,22 +3,30 @@ use pool_interface::types::user_config::UserConfiguration; use soroban_sdk::{Address, Env}; use crate::event; +use crate::methods::utils::validation::require_not_exceed_assets_limit; use crate::storage::{read_user_config, write_user_config}; pub struct UserConfigurator<'a> { env: &'a Env, user: &'a Address, + assets_limit: u32, create_if_none: bool, should_write: bool, user_config: Option, } impl<'a> UserConfigurator<'a> { - pub fn new(env: &'a Env, user: &'a Address, create_if_none: bool) -> Self { + pub fn new( + env: &'a Env, + user: &'a Address, + create_if_none: bool, + mb_assets_limit: Option, + ) -> Self { Self { env, create_if_none, user, + assets_limit: mb_assets_limit.unwrap_or_default(), user_config: None, should_write: false, } @@ -38,6 +46,7 @@ impl<'a> UserConfigurator<'a> { let user_config = self.read_user_config()?.user_config.as_mut().unwrap(); user_config.set_using_as_collateral(env, reserve_id, false); + event::reserve_used_as_collateral_disabled(env, self.user, asset); self.should_write = true; @@ -56,9 +65,12 @@ impl<'a> UserConfigurator<'a> { } let env = self.env; + let assets_limit = self.assets_limit; let user_config = self.read_user_config()?.user_config.as_mut().unwrap(); user_config.set_using_as_collateral(env, reserve_id, true); + require_not_exceed_assets_limit(env, user_config.total_assets(), assets_limit); + event::reserve_used_as_collateral_enabled(env, self.user, asset); self.should_write = true; @@ -72,9 +84,11 @@ impl<'a> UserConfigurator<'a> { } let env = self.env; + let assets_limit = self.assets_limit; let user_config = self.read_user_config()?.user_config.as_mut().unwrap(); user_config.set_borrowing(env, reserve_id, true); + require_not_exceed_assets_limit(env, user_config.total_assets(), assets_limit); self.should_write = true; @@ -88,6 +102,7 @@ impl<'a> UserConfigurator<'a> { let env = self.env; let user_config = self.read_user_config()?.user_config.as_mut().unwrap(); + user_config.set_borrowing(env, reserve_id, false); self.should_write = true; diff --git a/contracts/s-token/src/tests/test.rs b/contracts/s-token/src/tests/test.rs index 9405d0bb..d6090549 100644 --- a/contracts/s-token/src/tests/test.rs +++ b/contracts/s-token/src/tests/test.rs @@ -10,7 +10,7 @@ use soroban_sdk::token::{Client as TokenClient, StellarAssetClient as TokenAdmin use soroban_sdk::{symbol_short, vec, Address, Env, IntoVal, Symbol}; use self::pool::{ - CollateralParamsInput, IRParams, OracleAsset, PriceFeed, PriceFeedConfigInput, ReserveType, + CollateralParamsInput, OracleAsset, PoolConfig, PriceFeed, PriceFeedConfigInput, ReserveType, TimestampPrecision, }; @@ -39,30 +39,39 @@ fn create_token<'a>( TokenClient, TokenAdminClient, ) { - let pool = pool::Client::new(e, &e.register_contract_wasm(None, pool::WASM)); let pool_admin = Address::generate(e); - let treasury = Address::generate(e); + + let pool = pool::Client::new(e, &e.register_contract_wasm(None, pool::WASM)); + let s_token = STokenClient::new(e, &e.register_contract(None, SToken {})); + let stellar_asset = &e.register_stellar_asset_contract(pool_admin.clone()); + + let underlying_asset = TokenClient::new(e, stellar_asset); + let underlying_asset_admin = TokenAdminClient::new(e, stellar_asset); + let flash_loan_fee = 5; let initial_health = 2_500; + let grace_period = 60 * 60 * 24; pool.initialize( &pool_admin, - &treasury, - &flash_loan_fee, - &initial_health, - &IRParams { - alpha: 143, - initial_rate: 200, - max_rate: 50_000, - scaling_coeff: 9_000, + &PoolConfig { + base_asset_address: underlying_asset.address.clone(), + base_asset_decimals: 7, + flash_loan_fee: flash_loan_fee, + initial_health: initial_health, + timestamp_window: 20, + grace_period: grace_period, + user_assets_limit: 4, + min_collat_amount: 0, + min_debt_amount: 0, + liquidation_protocol_fee: 0, + ir_alpha: 143, + ir_initial_rate: 200, + ir_max_rate: 50_000, + ir_scaling_coeff: 9_000, }, ); - let s_token = STokenClient::new(e, &e.register_contract(None, SToken {})); - - let stellar_asset = &e.register_stellar_asset_contract(pool_admin.clone()); - let underlying_asset = TokenClient::new(e, stellar_asset); - let underlying_asset_admin = TokenAdminClient::new(e, stellar_asset); e.budget().reset_default(); let price_feed = oracle::Client::new(e, &e.register_contract_wasm(None, oracle::WASM)); @@ -71,6 +80,8 @@ fn create_token<'a>( PriceFeedConfigInput { asset: underlying_asset.address.clone(), asset_decimals: 7, + min_sanity_price_in_base: 5_000_000, + max_sanity_price_in_base: 100_000_000, feeds: vec![ &e, PriceFeed { @@ -78,6 +89,7 @@ fn create_token<'a>( feed_asset: OracleAsset::Stellar(underlying_asset.address.clone()), feed_decimals: 14, twap_records: 10, + min_timestamp_delta: 100, timestamp_precision: TimestampPrecision::Sec, }, ], diff --git a/deploy/artifacts/.contracts b/deploy/artifacts/.contracts index 8c5b62b2..b4afb305 100644 --- a/deploy/artifacts/.contracts +++ b/deploy/artifacts/.contracts @@ -1,33 +1,37 @@ -SLENDER_TOKEN_XLM=CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC -SLENDER_TOKEN_XRP=CCH3KNXV6ALUFSCI63UCWCMC3BCXORAAPMVS2VY54NVK4PRZ5D5TBJ7R -SLENDER_TOKEN_USDC=CCLCYHJME2TW7ZUEE4AMMPSC3Y2ZGFOOFQWJWSKNU7DSJTA3SJN7SOGU -SLENDER_TOKEN_RWA=CDHOTYVNOMSPETTE2CCWRR5SKZKRIAV24G35FNTFYOCBD4OXHQSGCTPL -SLENDER_S_TOKEN_HASH=4fc7d63972b375df6144dd5fd7ffa9ed7ed1ae6638641c2ba763ca030dd19bcd -SLENDER_DEBT_TOKEN_HASH=1aa23951740091f238ddb75281c9aeeac6bd3b6b29a4de645efcc5c5ffb7e133 -SLENDER_DEPLOYER=CAXYUJAXKTQVT4DC5HKGMDU7DRGACQ4V4KLOOOZ6CBXGCI4QODL4S5L2 -SLENDER_PRICE_FEED=CDRTYJNX2FDQ6K6YI437NCFUKOAPJPVJFCSQWN26UQY64YP5BGYZBFJU -SLENDER_POOL_HASH=b95962b04a4ef57addd5a8ad57e25b4bdb617df26793fdf71620f5bc589a20ae +SLENDER_TOKEN_XLM=CCQH5F34EMEO66YMGWEWXHAWC7OVUXT2LPYR232KCEQ6XS44TLSYAX52 +SLENDER_TOKEN_XRP=CAVVJUDV6WA6ZZ2UFBDTGZFEKYPFCHUN7HMN3653HUDNKJCZD3REBKWH +SLENDER_TOKEN_USDC=CALPNCN3ECNNKMKZHU4IISYYJVNAW7FKC54GIJLNGRDQFDLG67Z3XXIG +SLENDER_TOKEN_RWA=CBCX77MRH3IUGFPSNJ5UENT65ERZPNRLJJZRE5GCKIMMQU5GK6DVRIKS +SLENDER_S_TOKEN_HASH=94b81be3fcde0ecbb84179f875b92449505240931da26a9ce5eadd5d1ea98d81 +SLENDER_DEBT_TOKEN_HASH=0945751b90ec3843ecb7736d6cd009d731a83a1032c10fffd18c3e259a81b7a9 +SLENDER_DEPLOYER=CBY2KAMNWTN4DML4YPO6YV6LOOZ4CAJ4KIEU3RWEVXD5GSSGB2SHVVBM +SLENDER_PRICE_FEED=CBHTLM5BTMA6BOCICAQ4AXHBBA3EEU7Z3XQB2VLDBYLZEPCWMUJCGXEM +SLENDER_POOL_HASH=7d7241a32ea35c8a98cfe39747185a67f614fac4c410bf5e74187e61f572ba24 +SLENDER_TOKEN_XLM_INITIALIZED=TRUE SLENDER_TOKEN_XRP_INITIALIZED=TRUE SLENDER_TOKEN_USDC_INITIALIZED=TRUE SLENDER_TOKEN_RWA_INITIALIZED=TRUE -SLENDER_POOL=CC2XUP23MLB6SM3F5DKR4XSNMR3O5QY66ZHPYFSVWFAHOZYP6RBMYVTK -SLENDER_S_TOKEN_XRP=CABPO4V4YVBC7N2QDPXNZHD7IYJAIKZVSFHBAJ6VO7WJRRJJ2IMQ2ND3 -SLENDER_S_TOKEN_USDC=CCRO6DHZYHCSCVCWALSCR6W7XZUOSPDC3REZIAP3R7VS53L4LWCDRCNG -SLENDER_S_TOKEN_XLM=CB4QPUEV5PO5PD2QIGN6U2JFREZRCPQGR7WNXBVM2AS73VNBMHJBEXVJ -SLENDER_DEBT_TOKEN_XLM=CBFPNCFDNJND2JZWMSNPQCTBA3SBFCO36RIMVVBL2TOKW26UUOZGAGCS -SLENDER_DEBT_TOKEN_XRP=CAV2XCMC7GMSXMBHAKSSX2RC633AESJZ3WOZGFHXWTA3TTWYQE2KC2XK -SLENDER_DEBT_TOKEN_USDC=CDULNP7LB6F6R7QAUHMHKZ57N5BUCVWUI6UHOO3WIKXHGZLZC5ESGLYA +SLENDER_POOL=CDIJGOLSKC4WOFSBEVI7WNELQ7VPMKR6BLOJA2W5OV2DZIZVUD6GURHL +SLENDER_S_TOKEN_XRP=CBT4KY44RB3FWP2GPBTMCX3EBKH6MMLZ5L7JUAAH6VKESI7K6PC4FS36 +SLENDER_S_TOKEN_USDC=CCXSJSCWGSNR7XCUFPQEKNAP4ZRSUGZCUR5I4HKSRW7O6SR6XWW7XSNS +SLENDER_S_TOKEN_XLM=CBLT6I5LIYVBYABLVOAUBBY66QWGXTXGS7XQWIJHPFPMARUDCLIOUK5C +SLENDER_DEBT_TOKEN_XLM=CB5J2XJSSO6RQ3YMR7NIXL6BVJUKYN7W6EYASRQB6UUG774ENEHJSM55 +SLENDER_DEBT_TOKEN_XRP=CBYHX32CUFILARJYVTDGY2KWE7KVTPVJM564YSCKJWBMMCW27XBZXVYE +SLENDER_DEBT_TOKEN_USDC=CD6JS764YESU3BQLIC4WYIY3LNLA2SJFHU4FCNAAUC2SQQQBTELLNSTG SLENDER_POOL_XLM_RESERVE_INITIALIZED=TRUE SLENDER_POOL_XRP_RESERVE_INITIALIZED=TRUE SLENDER_POOL_USDC_RESERVE_INITIALIZED=TRUE +SLENDER_POOL_RWA_RESERVE_INITIALIZED=TRUE SLENDER_POOL_XRP_COLLATERAL_CONFIGURED=TRUE SLENDER_POOL_USDC_COLLATERAL_CONFIGURED=TRUE SLENDER_POOL_XLM_COLLATERAL_CONFIGURED=TRUE +SLENDER_POOL_RWA_COLLATERAL_CONFIGURED=TRUE SLENDER_POOL_XLM_BORROWING_ENABLED=TRUE SLENDER_POOL_XRP_BORROWING_ENABLED=TRUE SLENDER_POOL_USDC_BORROWING_ENABLED=TRUE -SLENDER_POOL_XLM_BASE_ASSET_SET=TRUE -SLENDER_POOL_PRICE_FEED_SET=TRUE -SLENDER_GCR7O7JRQ4GM5MIQLC4KWBY64WKWBPCBISHBMTSLM4BTMATYOBL4KNVD_XRP_MINTED=TRUE -SLENDER_GCR7O7JRQ4GM5MIQLC4KWBY64WKWBPCBISHBMTSLM4BTMATYOBL4KNVD_USDC_MINTED=TRUE \ No newline at end of file +SLENDER_GCR7O7JRQ4GM5MIQLC4KWBY64WKWBPCBISHBMTSLM4BTMATYOBL4KNVD_XLM_MINTED=TRUE +SLENDER_GDE5PTZHHL64OOIHOP6O6XVJCQCXTB6MXEKGJA4OHHAAOXQNHZCFPFKK_XRP_MINTED=TRUE +SLENDER_GCPGCSXWDVXVTI3VZCU4TVF43XYSWZZYXPONPKPYZXH5KJSIPQ5K6Y57_USDC_MINTED=TRUE +SLENDER_GDLJFYGSJS6STVIWDVXT2GOM3BTNWGSMWHQF3CEIP633SOAS7KH3Q5N5_USDC_MINTED=TRUE +SLENDER_POOL_PRICE_FEED_SET=TRUE \ No newline at end of file diff --git a/deploy/artifacts/debt_token.wasm b/deploy/artifacts/debt_token.wasm index 34ec745e..001864aa 100755 Binary files a/deploy/artifacts/debt_token.wasm and b/deploy/artifacts/debt_token.wasm differ diff --git a/deploy/artifacts/deployer.wasm b/deploy/artifacts/deployer.wasm index 302a12bf..23f838b0 100755 Binary files a/deploy/artifacts/deployer.wasm and b/deploy/artifacts/deployer.wasm differ diff --git a/deploy/artifacts/flash_loan_receiver_mock.wasm b/deploy/artifacts/flash_loan_receiver_mock.wasm index 979c7927..730aa669 100755 Binary files a/deploy/artifacts/flash_loan_receiver_mock.wasm and b/deploy/artifacts/flash_loan_receiver_mock.wasm differ diff --git a/deploy/artifacts/pool.wasm b/deploy/artifacts/pool.wasm index 373db5ac..4389e314 100755 Binary files a/deploy/artifacts/pool.wasm and b/deploy/artifacts/pool.wasm differ diff --git a/deploy/artifacts/price_feed_mock.wasm b/deploy/artifacts/price_feed_mock.wasm index 84e025a0..250b798e 100755 Binary files a/deploy/artifacts/price_feed_mock.wasm and b/deploy/artifacts/price_feed_mock.wasm differ diff --git a/deploy/artifacts/s_token.wasm b/deploy/artifacts/s_token.wasm index d089b8c7..8eb942a5 100755 Binary files a/deploy/artifacts/s_token.wasm and b/deploy/artifacts/s_token.wasm differ diff --git a/integration-tests/package.json b/integration-tests/package.json index f894a4de..47365dcf 100644 --- a/integration-tests/package.json +++ b/integration-tests/package.json @@ -17,8 +17,8 @@ "@types/chai-as-promised": "^7.1.6", "@types/mocha": "^10.0.1", "@types/node": "^20.4.1", - "@stellar/stellar-sdk": "11.2.2", - "stellar-sdk": "11.2.2", + "@stellar/stellar-sdk": "12.0.1", + "stellar-sdk": "12.0.1", "bigint-conversion": "^2.4.1", "bignumber.js": "^9.1.1", "chai": "^4.3.7", diff --git a/integration-tests/tests/pool.sut.ts b/integration-tests/tests/pool.sut.ts index 0778ca9a..8d1472e6 100644 --- a/integration-tests/tests/pool.sut.ts +++ b/integration-tests/tests/pool.sut.ts @@ -41,6 +41,8 @@ interface PriceFeedConfig { feed_asset: SlenderAsset; feed_decimals: number; twap_records: number; + min_timestamp_delta: number; + timestamp_precision: string; } interface PriceData { @@ -69,7 +71,7 @@ export async function init(client: SorobanClient, customXlm = true): Promise { const xdrResponse = await client.simulateTransaction( process.env.SLENDER_POOL, - "stoken_underlying_balance", - convertToScvAddress(process.env[`SLENDER_S_TOKEN_${asset}`]) + "token_balance", + convertToScvAddress(process.env[`SLENDER_TOKEN_${asset}`]), + convertToScvAddress(process.env[`SLENDER_S_TOKEN_${asset}`]), ); return parseScvToJs(xdrResponse); @@ -427,7 +444,7 @@ export async function deployReceiverMock(): Promise { const flashLoadReceiverMockAddress = ( (await new Promise((resolve, reject) => { exec( - `soroban contract deploy \ + `soroban contract deploy \ --wasm ../target/wasm32-unknown-unknown/release/flash_loan_receiver_mock.wasm \ --source ${adminKeys.secret()} \ --rpc-url "${process.env.SOROBAN_RPC_URL}" \ @@ -459,7 +476,7 @@ export async function liquidateCli( const liquidateResult = ( (await new Promise((resolve) => { exec( - `soroban --very-verbose contract invoke \ + `soroban --very-verbose contract invoke \ --id ${process.env.SLENDER_POOL} \ --source ${liquidatorKeys.secret()} \ --rpc-url "${process.env.SOROBAN_RPC_URL}" \ @@ -714,7 +731,11 @@ async function initDToken( ); } -async function initPool(client: SorobanClient, salt: string): Promise { +async function initPool( + client: SorobanClient, + salt: string, + base_asset: SlenderAsset +): Promise { await initContract>( "POOL", () => @@ -726,14 +747,21 @@ async function initPool(client: SorobanClient, salt: string): Promise { convertToScvBytes(salt, "hex"), convertToScvBytes(process.env.SLENDER_POOL_HASH, "hex"), convertToScvAddress(adminKeys.publicKey()), - convertToScvAddress(treasuryKeys.publicKey()), - convertToScvU32(5), - convertToScvU32(2_500), convertToScvMap({ - alpha: convertToScvU32(143), - initial_rate: convertToScvU32(200), - max_rate: convertToScvU32(50_000), - scaling_coeff: convertToScvU32(9_000), + base_asset_address: convertToScvAddress(process.env[`SLENDER_TOKEN_${base_asset}`]), + base_asset_decimals: convertToScvU32(7), + flash_loan_fee: convertToScvU32(5), + grace_period: convertToScvU64(1), + initial_health: convertToScvU32(2_500), + ir_alpha: convertToScvU32(143), + ir_initial_rate: convertToScvU32(200), + ir_max_rate: convertToScvU32(50_000), + ir_scaling_coeff: convertToScvU32(9_000), + liquidation_protocol_fee: convertToScvU32(0), + min_collat_amount: convertToScvI128(1n), + min_debt_amount: convertToScvI128(1n), + timestamp_window: convertToScvU64(20), + user_assets_limit: convertToScvU32(4), }) ), (result) => result[0] @@ -788,6 +816,8 @@ async function initPoolPriceFeed( inputs: { asset: SlenderAsset, asset_decimals: number, + max_sanity_price_in_base: bigint, + min_sanity_price_in_base: bigint, priceFeedConfig: PriceFeedConfig }[] ): Promise { @@ -809,9 +839,13 @@ async function initPoolPriceFeed( convertToScvAddress(process.env[`SLENDER_TOKEN_${input.priceFeedConfig.feed_asset}`]) ]), "feed_decimals": convertToScvU32(input.priceFeedConfig.feed_decimals), + "min_timestamp_delta": convertToScvU64(input.priceFeedConfig.min_timestamp_delta), + "timestamp_precision": convertToScvVec([xdr.ScVal.scvSymbol(input.priceFeedConfig.timestamp_precision)]), "twap_records": convertToScvU32(input.priceFeedConfig.twap_records) }) ]), + "max_sanity_price_in_base": convertToScvI128(input.max_sanity_price_in_base), + "min_sanity_price_in_base": convertToScvI128(input.min_sanity_price_in_base) }))) ) ); @@ -831,24 +865,6 @@ async function initPoolBorrowing(client: SorobanClient, asset: SlenderAsset): Pr ); } -async function initBaseAsset( - client: SorobanClient, - asset: SlenderAsset, - decimals: number -): Promise { - await initContract( - `POOL_${asset}_BASE_ASSET_SET`, - () => client.sendTransaction( - process.env.SLENDER_POOL, - "set_base_asset", - adminKeys, - 3, - convertToScvAddress(process.env[`SLENDER_TOKEN_${asset}`]), - convertToScvU32(decimals) - ) - ); -} - export async function initPrice( client: SorobanClient, asset: SlenderAsset, diff --git a/integration-tests/yarn.lock b/integration-tests/yarn.lock index 4f761bcf..c807e624 100644 --- a/integration-tests/yarn.lock +++ b/integration-tests/yarn.lock @@ -32,38 +32,36 @@ resolved "https://registry.yarnpkg.com/@juanelas/base64/-/base64-1.1.2.tgz#968ff0f9c48adcfa79a0802385d56a2a661cb6cd" integrity sha512-mr2pfRQpWap0Uq4tlrCgp3W+Yjx1/Bpq4QJsYeAQUh1mExgyQvXz7xUhmYT2HcLLspuAL5dpnos8P2QhaCSXsQ== -"@stellar/js-xdr@^3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@stellar/js-xdr/-/js-xdr-3.1.0.tgz#37c23e6c913d011f750808f3d8b60174b633e137" - integrity sha512-mYTyFnhgyQgyvpAYZRO1LurUn2MxcIZRj74zZz/BxKEk7zrL4axhQ1ez0HL2BRi0wlG6cHn5BeD/t9Xcyp7CSQ== +"@stellar/js-xdr@^3.1.1": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@stellar/js-xdr/-/js-xdr-3.1.1.tgz#be0ff90c8a861d6e1101bca130fa20e74d5599bb" + integrity sha512-3gnPjAz78htgqsNEDkEsKHKosV2BF2iZkoHCNxpmZwUxiPsw+2VaXMed8RRMe0rGk3d5GZe7RrSba8zV80J3Ag== -"@stellar/stellar-base@^11.0.0": - version "11.0.0" - resolved "https://registry.yarnpkg.com/@stellar/stellar-base/-/stellar-base-11.0.0.tgz#9c2595f0aa1fe955191656fe9bda7a6b34ef82f0" - integrity sha512-KPTjaWJCG2m7hMCPRWFGGPaG5qOkgPLWvFVOhe1HUy7dlE4MxxPfdusz0mcLkf6VT7doqhLB1rIt0D9M2GgQcQ== +"@stellar/stellar-base@^12.0.0": + version "12.0.0" + resolved "https://registry.yarnpkg.com/@stellar/stellar-base/-/stellar-base-12.0.0.tgz#d46e2a45b756c3e5b4b8f82f224fcc467dac3ade" + integrity sha512-uGpahDFFXbE39myOmBnEZSjVys4yUkQ/ASNuYENGyS8MNdfA0TS7DPWkiO6t17P+rW+FRV1zmumJtjRHwxxMdw== dependencies: - "@stellar/js-xdr" "^3.1.0" + "@stellar/js-xdr" "^3.1.1" base32.js "^0.1.0" bignumber.js "^9.1.2" buffer "^6.0.3" sha.js "^2.3.6" tweetnacl "^1.0.3" - typescript "^5.3.3" optionalDependencies: - sodium-native "^4.0.8" + sodium-native "^4.1.1" -"@stellar/stellar-sdk@11.2.2": - version "11.2.2" - resolved "https://registry.yarnpkg.com/@stellar/stellar-sdk/-/stellar-sdk-11.2.2.tgz#e2768d519a05f396ff3f89f9fb7a7c5ed22c7c74" - integrity sha512-50dpxpZE2e87LjIln1EZnBh7r+JFWwyGs+NiGPOZ+LzJd2YRf54+YxzDX6ELVOrUqp2TRTLnzthXXTNFeVobFw== +"@stellar/stellar-sdk@12.0.1": + version "12.0.1" + resolved "https://registry.yarnpkg.com/@stellar/stellar-sdk/-/stellar-sdk-12.0.1.tgz#0ded98bdd606cc2dc491ba6095a30530d690380d" + integrity sha512-0+YXUTS2LpZ+Of383hSYVpsRl9BJ3X9lHcj05ouS3VkVL9BH7w+Par8RHPkiHS6lLYn3gWRgaJauTebkamY/Jw== dependencies: - "@stellar/stellar-base" "^11.0.0" - axios "^1.6.7" + "@stellar/stellar-base" "^12.0.0" + axios "^1.7.2" bignumber.js "^9.1.2" eventsource "^2.0.2" randombytes "^2.1.0" toml "^3.0.0" - typescript "^5.3.3" urijs "^1.19.1" "@tsconfig/node10@^1.0.7": @@ -168,12 +166,12 @@ asynckit@^0.4.0: resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== -axios@^1.6.7: - version "1.6.7" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.7.tgz#7b48c2e27c96f9c68a2f8f31e2ab19f59b06b0a7" - integrity sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA== +axios@^1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.2.tgz#b625db8a7051fbea61c35a3cbb3a1daa7b9c7621" + integrity sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw== dependencies: - follow-redirects "^1.15.4" + follow-redirects "^1.15.6" form-data "^4.0.0" proxy-from-env "^1.1.0" @@ -435,10 +433,10 @@ flat@^5.0.2: resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== -follow-redirects@^1.15.4: - version "1.15.5" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.5.tgz#54d4d6d062c0fa7d9d17feb008461550e3ba8020" - integrity sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw== +follow-redirects@^1.15.6: + version "1.15.6" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" + integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== form-data@^4.0.0: version "4.0.0" @@ -662,10 +660,10 @@ nanoid@3.3.3: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.3.tgz#fd8e8b7aa761fe807dba2d1b98fb7241bb724a25" integrity sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w== -node-gyp-build@^4.6.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.6.0.tgz#0c52e4cbf54bbd28b709820ef7b6a3c2d6209055" - integrity sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ== +node-gyp-build@^4.8.0: + version "4.8.1" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.1.tgz#976d3ad905e71b76086f4f0b0d3637fe79b6cda5" + integrity sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw== normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" @@ -774,25 +772,24 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -sodium-native@^4.0.8: - version "4.0.10" - resolved "https://registry.yarnpkg.com/sodium-native/-/sodium-native-4.0.10.tgz#24af8db06518807a8ddf4e5143d819f14bf7a837" - integrity sha512-vrJQt4gASntDbnltRRk9vN4rks1SehjM12HkqQtu250JtWT+/lo8oEOa1HvSq3+8hzJdYcCJuLR5qRGxoRDjAg== +sodium-native@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/sodium-native/-/sodium-native-4.1.1.tgz#109bc924dd55c13db87c6dd30da047487595723c" + integrity sha512-LXkAfRd4FHtkQS4X6g+nRcVaN7mWVNepV06phIsC6+IZFvGh1voW5TNQiQp2twVaMf05gZqQjuS+uWLM6gHhNQ== dependencies: - node-gyp-build "^4.6.0" + node-gyp-build "^4.8.0" -stellar-sdk@11.2.2: - version "11.2.2" - resolved "https://registry.yarnpkg.com/stellar-sdk/-/stellar-sdk-11.2.2.tgz#930ba86e10bdc71b2ad7a3f0bd60323ff708e12a" - integrity sha512-xecQW4gkPIxAvxcVFcw4ZSTtzpUmJPd4A4e4Mr3EkOdyWnshMIZQMzFox5DuAikrThofgihScJGYrDCmo3I/BA== +stellar-sdk@12.0.1: + version "12.0.1" + resolved "https://registry.yarnpkg.com/stellar-sdk/-/stellar-sdk-12.0.1.tgz#48fb76a5f44854ffb907e034eaae5f72d09adbdc" + integrity sha512-Bjx5XOPKDUoXXGdPqHScIcGWi2o/5idQdGEvCmaFTsmbuZIs80HfjJRqHJ2uEL4cSYd8FzNDtai4pgFgvRWSfg== dependencies: - "@stellar/stellar-base" "^11.0.0" - axios "^1.6.7" + "@stellar/stellar-base" "^12.0.0" + axios "^1.7.2" bignumber.js "^9.1.2" eventsource "^2.0.2" randombytes "^2.1.0" toml "^3.0.0" - typescript "^5.3.3" urijs "^1.19.1" string-width@^4.1.0, string-width@^4.2.0: @@ -876,11 +873,6 @@ typescript@^5.1.6: resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.6.tgz#02f8ac202b6dad2c0dd5e0913745b47a37998274" integrity sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA== -typescript@^5.3.3: - version "5.3.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37" - integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw== - urijs@^1.19.1: version "1.19.11" resolved "https://registry.yarnpkg.com/urijs/-/urijs-1.19.11.tgz#204b0d6b605ae80bea54bea39280cdb7c9f923cc" diff --git a/interfaces/pool-interface/src/lib.rs b/interfaces/pool-interface/src/lib.rs index c3ca6312..e40ed5d2 100644 --- a/interfaces/pool-interface/src/lib.rs +++ b/interfaces/pool-interface/src/lib.rs @@ -3,11 +3,11 @@ use soroban_sdk::{contractclient, contractspecfn, Address, Bytes, BytesN, Env, Vec}; use types::account_position::AccountPosition; -use types::base_asset_config::BaseAssetConfig; use types::collateral_params_input::CollateralParamsInput; use types::error::Error; use types::flash_loan_asset::FlashLoanAsset; -use types::ir_params::IRParams; +use types::pause_info::PauseInfo; +use types::pool_config::PoolConfig; use types::price_feed_config::PriceFeedConfig; use types::price_feed_config_input::PriceFeedConfigInput; use types::reserve_data::ReserveData; @@ -22,21 +22,16 @@ pub struct Spec; #[contractspecfn(name = "Spec", export = false)] #[contractclient(name = "LendingPoolClient")] pub trait LendingPoolTrait { - fn initialize( - env: Env, - admin: Address, - treasury: Address, - flash_loan_fee: u32, - initial_health: u32, - ir_params: IRParams, - ) -> Result<(), Error>; + fn initialize(env: Env, admin: Address, pool_config: PoolConfig) -> Result<(), Error>; fn upgrade(env: Env, new_wasm_hash: BytesN<32>) -> Result<(), Error>; - fn upgrade_s_token(env: Env, asset: Address, new_wasm_hash: BytesN<32>) -> Result<(), Error>; - - fn upgrade_debt_token(env: Env, asset: Address, new_wasm_hash: BytesN<32>) - -> Result<(), Error>; + fn upgrade_token( + env: Env, + asset: Address, + s_token: bool, + new_wasm_hash: BytesN<32>, + ) -> Result<(), Error>; fn version() -> u32; @@ -58,26 +53,14 @@ pub trait LendingPoolTrait { fn debt_coeff(env: Env, asset: Address) -> Result; - fn base_asset(env: Env) -> Result; - - fn set_base_asset(env: Env, asset: Address, decimals: u32) -> Result<(), Error>; - - fn initial_health(env: Env) -> Result; + fn set_pool_configuration(env: Env, config: PoolConfig) -> Result<(), Error>; - fn set_initial_health(env: Env, value: u32) -> Result<(), Error>; + fn pool_configuration(env: Env) -> Result; fn set_price_feeds(env: Env, inputs: Vec) -> Result<(), Error>; fn price_feeds(env: Env, asset: Address) -> Option; - fn set_ir_params(env: Env, input: IRParams) -> Result<(), Error>; - - fn reserve_timestamp_window(env: Env) -> u64; - - fn set_reserve_timestamp_window(env: Env, window: u64) -> Result<(), Error>; - - fn ir_params(env: Env) -> Option; - fn deposit(env: Env, who: Address, asset: Address, amount: i128) -> Result<(), Error>; fn repay(env: Env, who: Address, asset: Address, amount: i128) -> Result<(), Error>; @@ -102,8 +85,6 @@ pub trait LendingPoolTrait { to: Address, ) -> Result<(), Error>; - fn stoken_underlying_balance(env: Env, stoken_address: Address) -> i128; - fn token_balance(env: Env, token: Address, account: Address) -> i128; fn token_total_supply(env: Env, token: Address) -> i128; @@ -112,18 +93,11 @@ pub trait LendingPoolTrait { fn set_pause(env: Env, value: bool) -> Result<(), Error>; - fn paused(env: Env) -> bool; - - fn treasury(e: Env) -> Address; + fn pause_info(env: Env) -> PauseInfo; fn account_position(env: Env, who: Address) -> Result; - fn liquidate( - env: Env, - liquidator: Address, - who: Address, - receive_stoken: bool, - ) -> Result<(), Error>; + fn liquidate(env: Env, liquidator: Address, who: Address) -> Result<(), Error>; fn set_as_collateral( env: Env, @@ -134,10 +108,6 @@ pub trait LendingPoolTrait { fn user_configuration(env: Env, who: Address) -> Result; - fn set_flash_loan_fee(env: Env, fee: u32) -> Result<(), Error>; - - fn flash_loan_fee(env: Env) -> u32; - fn flash_loan( env: Env, who: Address, @@ -148,5 +118,7 @@ pub trait LendingPoolTrait { fn twap_median_price(env: Env, asset: Address, amount: i128) -> Result; - fn balance(env: Env, id: Address, asset: Address) -> i128; + fn protocol_fee(env: Env, asset: Address) -> i128; + + fn claim_protocol_fee(env: Env, asset: Address, recipient: Address) -> Result<(), Error>; } diff --git a/interfaces/pool-interface/src/types/error.rs b/interfaces/pool-interface/src/types/error.rs index 1ef17e64..b0c4074d 100644 --- a/interfaces/pool-interface/src/types/error.rs +++ b/interfaces/pool-interface/src/types/error.rs @@ -6,45 +6,35 @@ use soroban_sdk::contracterror; pub enum Error { AlreadyInitialized = 0, Uninitialized = 1, - NoPriceFeed = 2, - Paused = 3, + Paused = 2, + BellowMinValue = 3, + ExceededMaxValue = 4, + GracePeriod = 5, - NoReserveExistForAsset = 100, - NoActiveReserve = 101, - ReserveFrozen = 102, - ReservesMaxCapacityExceeded = 103, - NoPriceForAsset = 104, - ReserveAlreadyInitialized = 105, - InvalidAssetPrice = 106, - BaseAssetNotInitialized = 107, - InitialHealthNotInitialized = 108, - LiquidationOrderMustBeUnique = 109, - NotFungible = 110, + NoActiveReserve = 100, + ReservesMaxCapacityExceeded = 101, + NoPriceForAsset = 102, + InvalidAssetPrice = 103, + LiquidationOrderMustBeUnique = 104, + NotFungible = 105, - UserConfigInvalidIndex = 200, - NotEnoughAvailableUserBalance = 201, - UserConfigNotExists = 202, - MustHaveDebt = 203, - MustNotHaveDebt = 204, + NotEnoughAvailableUserBalance = 200, + DebtError = 201, - BorrowingNotEnabled = 300, - BelowInitialHealth = 301, - BadPosition = 302, - GoodPosition = 303, - InvalidAmount = 304, - ValidateBorrowMathError = 305, - CalcAccountDataMathError = 306, - LiquidateMathError = 309, - MustNotBeInCollateralAsset = 310, - UtilizationCapExceeded = 311, - LiqCapExceeded = 312, - FlashLoanReceiverError = 313, + BorrowingDisabled = 300, + GoodPosition = 301, + InvalidAmount = 302, + ValidateBorrowMathError = 303, + CalcAccountDataMathError = 304, + LiquidateMathError = 305, + MustNotBeInCollateralAsset = 306, + FlashLoanReceiverError = 307, MathOverflowError = 400, MustBeLtePercentageFactor = 401, MustBeLtPercentageFactor = 402, MustBeGtPercentageFactor = 403, - MustBePositive = 404, + MustBeNonNegative = 404, AccruedRateMathError = 500, CollateralCoeffMathError = 501, diff --git a/interfaces/pool-interface/src/types/ir_params.rs b/interfaces/pool-interface/src/types/ir_params.rs deleted file mode 100644 index 07a3ad23..00000000 --- a/interfaces/pool-interface/src/types/ir_params.rs +++ /dev/null @@ -1,11 +0,0 @@ -use soroban_sdk::contracttype; - -/// Interest rate parameters -#[contracttype] -#[derive(Clone)] -pub struct IRParams { - pub alpha: u32, - pub initial_rate: u32, - pub max_rate: u32, - pub scaling_coeff: u32, -} diff --git a/interfaces/pool-interface/src/types/mod.rs b/interfaces/pool-interface/src/types/mod.rs index 7c60a2d9..189dd914 100644 --- a/interfaces/pool-interface/src/types/mod.rs +++ b/interfaces/pool-interface/src/types/mod.rs @@ -4,8 +4,9 @@ pub mod base_asset_config; pub mod collateral_params_input; pub mod error; pub mod flash_loan_asset; -pub mod ir_params; pub mod oracle_asset; +pub mod pause_info; +pub mod pool_config; pub mod price_feed; pub mod price_feed_config; pub mod price_feed_config_input; diff --git a/interfaces/pool-interface/src/types/pause_info.rs b/interfaces/pool-interface/src/types/pause_info.rs new file mode 100644 index 00000000..91f66def --- /dev/null +++ b/interfaces/pool-interface/src/types/pause_info.rs @@ -0,0 +1,14 @@ +use soroban_sdk::contracttype; + +#[contracttype] +pub struct PauseInfo { + pub paused: bool, + pub grace_period_secs: u64, + pub unpaused_at: u64, +} + +impl PauseInfo { + pub fn grace_period_ends_at(&self) -> u64 { + self.unpaused_at + self.grace_period_secs + } +} diff --git a/interfaces/pool-interface/src/types/pool_config.rs b/interfaces/pool-interface/src/types/pool_config.rs new file mode 100644 index 00000000..2cea5d80 --- /dev/null +++ b/interfaces/pool-interface/src/types/pool_config.rs @@ -0,0 +1,20 @@ +use soroban_sdk::{contracttype, Address}; + +#[derive(Clone)] +#[contracttype] +pub struct PoolConfig { + pub base_asset_address: Address, + pub base_asset_decimals: u32, + pub initial_health: u32, + pub grace_period: u64, + pub timestamp_window: u64, + pub flash_loan_fee: u32, + pub user_assets_limit: u32, + pub min_collat_amount: i128, + pub min_debt_amount: i128, + pub liquidation_protocol_fee: u32, + pub ir_alpha: u32, + pub ir_initial_rate: u32, + pub ir_max_rate: u32, + pub ir_scaling_coeff: u32, +} diff --git a/interfaces/pool-interface/src/types/price_feed.rs b/interfaces/pool-interface/src/types/price_feed.rs index b3c570c7..df67bd57 100644 --- a/interfaces/pool-interface/src/types/price_feed.rs +++ b/interfaces/pool-interface/src/types/price_feed.rs @@ -10,5 +10,6 @@ pub struct PriceFeed { pub feed_asset: OracleAsset, pub feed_decimals: u32, pub twap_records: u32, + pub min_timestamp_delta: u64, pub timestamp_precision: TimestampPrecision, } diff --git a/interfaces/pool-interface/src/types/price_feed_config.rs b/interfaces/pool-interface/src/types/price_feed_config.rs index 26ff2792..ffbbebe5 100644 --- a/interfaces/pool-interface/src/types/price_feed_config.rs +++ b/interfaces/pool-interface/src/types/price_feed_config.rs @@ -6,5 +6,7 @@ use crate::types::price_feed::PriceFeed; #[contracttype] pub struct PriceFeedConfig { pub asset_decimals: u32, + pub min_sanity_price_in_base: i128, + pub max_sanity_price_in_base: i128, pub feeds: Vec, } diff --git a/interfaces/pool-interface/src/types/price_feed_config_input.rs b/interfaces/pool-interface/src/types/price_feed_config_input.rs index e73dabee..d4e50f0d 100644 --- a/interfaces/pool-interface/src/types/price_feed_config_input.rs +++ b/interfaces/pool-interface/src/types/price_feed_config_input.rs @@ -7,5 +7,7 @@ use crate::types::price_feed::PriceFeed; pub struct PriceFeedConfigInput { pub asset: Address, pub asset_decimals: u32, + pub min_sanity_price_in_base: i128, + pub max_sanity_price_in_base: i128, pub feeds: Vec, } diff --git a/interfaces/pool-interface/src/types/reserve_data.rs b/interfaces/pool-interface/src/types/reserve_data.rs index 703ce29a..b8250114 100644 --- a/interfaces/pool-interface/src/types/reserve_data.rs +++ b/interfaces/pool-interface/src/types/reserve_data.rs @@ -1,7 +1,8 @@ use common::FixedI128; -use soroban_sdk::{contracttype, BytesN, Env}; +use soroban_sdk::{contracttype, Address, BytesN, Env}; use super::collateral_params_input::CollateralParamsInput; +use super::error::Error; use super::reserve_configuration::ReserveConfiguration; use super::reserve_type::ReserveType; @@ -43,6 +44,14 @@ impl ReserveData { pub fn get_id(&self) -> u8 { self.id.get(0).unwrap() } + + pub fn get_fungible(&self) -> Result<(&Address, &Address), Error> { + if let ReserveType::Fungible(s_token_address, debt_token_address) = &self.reserve_type { + Ok((s_token_address, debt_token_address)) + } else { + Err(Error::NotFungible) + } + } } fn zero_bytes(env: &Env) -> BytesN { diff --git a/interfaces/pool-interface/src/types/user_config.rs b/interfaces/pool-interface/src/types/user_config.rs index c46e0a84..05d355f3 100644 --- a/interfaces/pool-interface/src/types/user_config.rs +++ b/interfaces/pool-interface/src/types/user_config.rs @@ -6,30 +6,50 @@ const BORROWING_MASK: u128 = 0x55555555555555555555555555555555; #[contracttype] #[derive(Default)] -/// Implements the bitmap logic to handle the user configuration. -/// Even positions is collateral flags and uneven is borrowing flags. -pub struct UserConfiguration(u128); +pub struct UserConfiguration(u128, u32); impl UserConfiguration { - pub fn set_borrowing(&mut self, env: &Env, reserve_index: u8, borrowing: bool) { + pub fn set_borrowing(&mut self, env: &Env, reserve_index: u8, borrow: bool) { Self::require_reserve_index(env, reserve_index); + let is_borrowed_before = self.is_borrowing(env, reserve_index); let reserve_index: u128 = reserve_index.into(); self.0 = (self.0 & !(1 << (reserve_index * 2))) - | ((if borrowing { 1 } else { 0 }) << (reserve_index * 2)); + | ((if borrow { 1 } else { 0 }) << (reserve_index * 2)); + + if is_borrowed_before == borrow { + return; + } + + if borrow { + self.1 += 1; + } else { + self.1 -= 1; + }; } pub fn set_using_as_collateral( &mut self, env: &Env, reserve_index: u8, - using_as_collateral: bool, + use_as_collateral: bool, ) { Self::require_reserve_index(env, reserve_index); + let is_collat_before = self.is_using_as_collateral(env, reserve_index); let reserve_index: u128 = reserve_index.into(); self.0 = (self.0 & !(1 << (reserve_index * 2 + 1))) - | ((if using_as_collateral { 1 } else { 0 }) << (reserve_index * 2 + 1)); + | ((if use_as_collateral { 1 } else { 0 }) << (reserve_index * 2 + 1)); + + if is_collat_before == use_as_collateral { + return; + } + + if use_as_collateral { + self.1 += 1; + } else { + self.1 -= 1; + }; } pub fn is_using_as_collateral(&self, env: &Env, reserve_index: u8) -> bool { @@ -60,11 +80,15 @@ impl UserConfiguration { self.0 == 0 } + pub fn total_assets(&self) -> u32 { + self.1 + } + fn require_reserve_index(env: &Env, reserve_index: u8) { assert_with_error!( env, - reserve_index < core::mem::size_of::() as u8 / 2, - Error::UserConfigInvalidIndex + reserve_index < core::mem::size_of::() as u8 / 2, // todo: size_of returns bytes, not bits, need to multiply by 8 + Error::ExceededMaxValue ); } }