diff --git a/Cargo.lock b/Cargo.lock index 11068a87..18d5a99b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1053,9 +1053,9 @@ checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" [[package]] name = "soroban-builtin-sdk-macros" -version = "20.0.0" +version = "20.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42487d6b0268748f753feeb579c6f7908dbb002faf20b703e6a7185b12f0527" +checksum = "55ef302d2118a14267e441e50e33705adc4f0da56616e7d2d9f198448d5714b2" dependencies = [ "itertools", "proc-macro2", @@ -1065,9 +1065,9 @@ dependencies = [ [[package]] name = "soroban-env-common" -version = "20.0.0" +version = "20.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bb493483fa3e3ebfb4c081472495d14b0abcfbe04ba142a56ff63056cc62700" +checksum = "fc40ac91f70bb93aed7dff6057caac8810d49a8c451f44286e1e49243c799beb" dependencies = [ "arbitrary", "crate-git-revision", @@ -1083,9 +1083,9 @@ dependencies = [ [[package]] name = "soroban-env-guest" -version = "20.0.0" +version = "20.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f31a738ef5faf4084c4b1824a8e3f93dfff0261a3909e86060f818e728479a3" +checksum = "949587b3608cb05fe1d5eecce24aed1c33063c38fa79402f2e5b1c2a29466350" dependencies = [ "soroban-env-common", "static_assertions", @@ -1093,9 +1093,9 @@ dependencies = [ [[package]] name = "soroban-env-host" -version = "20.0.0" +version = "20.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdd1172a76c0bc2ce67ec7f28ca37dddbe9fefabe583f80434f5f60aaee3547e" +checksum = "faaa4e738232cacae7deb7947adfd4718e47cd2b50676e9518a8a38ee00930c9" dependencies = [ "backtrace", "curve25519-dalek", @@ -1120,9 +1120,9 @@ dependencies = [ [[package]] name = "soroban-env-macros" -version = "20.0.0" +version = "20.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c0536648cea69ab3bae1801d35f92c0a31e7449cd2c7d14a18fb5e413f43279" +checksum = "ff09cd5f1e4968e6dbac40eb4fbb2bdbb478fa989a96088fe0466d09e8ff40c6" dependencies = [ "itertools", "proc-macro2", @@ -1144,9 +1144,9 @@ dependencies = [ [[package]] name = "soroban-ledger-snapshot" -version = "20.0.0" +version = "20.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37960eec21d7dc5dbd976fa16e38c056429663a89243798486b07afbb263c9b5" +checksum = "95a7b822725a73a90ef650bc1f325d13c8bae7a808156c101953092327e2edee" dependencies = [ "serde", "serde_json", @@ -1158,9 +1158,9 @@ dependencies = [ [[package]] name = "soroban-sdk" -version = "20.0.0" +version = "20.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f08e1fdb18dbee88160ea6640962faf021a49f22eb1bd212c4d8b0cef32c582c" +checksum = "fdff4b5fc50f554499b81aa6ecbb4045beb84742ecda9777ebbdc90c0d93ec62" dependencies = [ "arbitrary", "bytes-lit", @@ -1178,9 +1178,9 @@ dependencies = [ [[package]] name = "soroban-sdk-macros" -version = "20.0.0" +version = "20.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5cae44f304f2fd32f9cfa9a31a9b58eb1c10aa07a7d5b591921cf7fa649e44" +checksum = "12d147c3ce37842919893946a4467632aa012f567a7ab2286abe19e5ecc25e05" dependencies = [ "crate-git-revision", "darling", @@ -1198,9 +1198,9 @@ dependencies = [ [[package]] name = "soroban-spec" -version = "20.0.0" +version = "20.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7539cfa0abe36f3d33c49fe1253f6b652c91c9a9841fe83dedc1799b7f4bb55f" +checksum = "6b7a132b7c234edf6ef3add4ffb17807f3b25a4ce5ab944ebbaf4d2326470eb1" dependencies = [ "base64 0.13.1", "stellar-xdr", @@ -1210,9 +1210,9 @@ dependencies = [ [[package]] name = "soroban-spec-rust" -version = "20.0.0" +version = "20.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb6189ef3ede0061db14b0cf9fa2692a2cb6c6e8d941689f0c9ca82b68c47ab2" +checksum = "e8d396f3b29800138e8abf2562aba0b579d09d8c2d2b956379fc9e68914a6e62" dependencies = [ "prettyplease", "proc-macro2", @@ -1226,9 +1226,9 @@ dependencies = [ [[package]] name = "soroban-wasmi" -version = "0.31.1-soroban.20.0.0" +version = "0.31.1-soroban.20.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1aaa682a67cbd2173f1d60cb1e7b951d490d7c4e0b7b6f5387cbb952e963c46" +checksum = "710403de32d0e0c35375518cb995d4fc056d0d48966f2e56ea471b8cb8fc9719" dependencies = [ "smallvec", "spin", @@ -1272,9 +1272,9 @@ dependencies = [ [[package]] name = "stellar-xdr" -version = "20.0.0" +version = "20.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9595b775539e475da4179fa46212b11e4575f526d57b13308989a8c1dd59238c" +checksum = "e59cdf3eb4467fb5a4b00b52e7de6dca72f67fac6f9b700f55c95a5d86f09c9d" dependencies = [ "arbitrary", "base64 0.13.1", diff --git a/Cargo.toml b/Cargo.toml index 51f365e6..626de7d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,7 @@ codegen-units = 1 lto = true [workspace.dependencies.soroban-sdk] -version = "20.0.0" +version = "20.3.2" [workspace.dependencies.soroban-fixed-point-math] version = "1.0.0" diff --git a/backstop/src/backstop/deposit.rs b/backstop/src/backstop/deposit.rs index b5d7423b..583c6b74 100644 --- a/backstop/src/backstop/deposit.rs +++ b/backstop/src/backstop/deposit.rs @@ -20,6 +20,9 @@ pub fn execute_deposit(e: &Env, from: &Address, pool_address: &Address, amount: backstop_token_client.transfer(from, &e.current_contract_address(), &amount); let to_mint = pool_balance.convert_to_shares(amount); + if to_mint == 0 { + panic_with_error!(e, &BackstopError::InvalidShareMintAmount); + } pool_balance.deposit(amount, to_mint); user_balance.add_shares(to_mint); @@ -35,6 +38,7 @@ mod tests { use crate::{ backstop::execute_donate, + constants::SCALAR_7, testutils::{create_backstop, create_backstop_token, create_mock_pool_factory}, }; @@ -208,4 +212,65 @@ mod tests { execute_deposit(&e, &samwise, &pool_0_id, 100); }); } + + #[test] + #[should_panic(expected = "Error(Contract, #1005)")] + fn test_execute_deposit_zero_share_mint() { + let e = Env::default(); + e.budget().reset_unlimited(); + e.mock_all_auths_allowing_non_root_auth(); + + let backstop_address = create_backstop(&e); + let bombadil = Address::generate(&e); + let samwise = Address::generate(&e); + let frodo = Address::generate(&e); + let pool_0_id = Address::generate(&e); + let pool_1_id = Address::generate(&e); + + let (_, backstop_token_client) = create_backstop_token(&e, &backstop_address, &bombadil); + backstop_token_client.mint(&samwise, &100_0000000); + backstop_token_client.mint(&frodo, &100_000_000_0000000); + + let (_, mock_pool_factory_client) = create_mock_pool_factory(&e, &backstop_address); + mock_pool_factory_client.set_pool(&pool_0_id); + mock_pool_factory_client.set_pool(&pool_1_id); + + // initialize pool 0 with funds + some profit + e.as_contract(&backstop_address, || { + execute_deposit(&e, &frodo, &pool_0_id, SCALAR_7); + execute_donate(&e, &frodo, &pool_0_id, 10_000_000 * SCALAR_7); + }); + + e.as_contract(&backstop_address, || { + execute_deposit(&e, &samwise, &pool_0_id, SCALAR_7); + }); + } + + // #[test] + // #[should_panic(expected = "Error(Contract, #1005)")] + // fn test_execute_deposit_small_initial_mint() { + // let e = Env::default(); + // e.budget().reset_unlimited(); + // e.mock_all_auths_allowing_non_root_auth(); + + // let backstop_address = create_backstop(&e); + // let bombadil = Address::generate(&e); + // let samwise = Address::generate(&e); + // let frodo = Address::generate(&e); + // let pool_0_id = Address::generate(&e); + // let pool_1_id = Address::generate(&e); + + // let (_, backstop_token_client) = create_backstop_token(&e, &backstop_address, &bombadil); + // backstop_token_client.mint(&samwise, &100_0000000); + // backstop_token_client.mint(&frodo, &100_0000000); + + // let (_, mock_pool_factory_client) = create_mock_pool_factory(&e, &backstop_address); + // mock_pool_factory_client.set_pool(&pool_0_id); + // mock_pool_factory_client.set_pool(&pool_1_id); + + // e.as_contract(&backstop_address, || { + // execute_donate(&e, &frodo, &pool_0_id, SCALAR_7); + // execute_deposit(&e, &samwise, &pool_0_id, SCALAR_7 / 10 - 1); + // }); + // } } diff --git a/backstop/src/backstop/fund_management.rs b/backstop/src/backstop/fund_management.rs index 18d68c4f..cd2374f7 100644 --- a/backstop/src/backstop/fund_management.rs +++ b/backstop/src/backstop/fund_management.rs @@ -9,6 +9,8 @@ use soroban_sdk::{panic_with_error, unwrap::UnwrapOptimized, Address, Env}; use super::require_is_from_pool_factory; /// Perform a draw from a pool's backstop +/// +/// `pool_address` MUST be authenticated before calling pub fn execute_draw(e: &Env, pool_address: &Address, amount: i128, to: &Address) { require_nonnegative(e, amount); diff --git a/backstop/src/backstop/pool.rs b/backstop/src/backstop/pool.rs index a6abd99b..6f5b1e6a 100644 --- a/backstop/src/backstop/pool.rs +++ b/backstop/src/backstop/pool.rs @@ -57,18 +57,9 @@ pub fn require_is_from_pool_factory(e: &Env, address: &Address, balance: i128) { } } -/// TODO: Duplicated from pool/pool/status.rs. Can this be moved to a common location? -/// /// Calculate the threshold for the pool's backstop balance /// /// Returns true if the pool's backstop balance is above the threshold -/// NOTE: The calculation is the percentage^5 to simplify the calculation of the pools product constant. -/// Some useful calculation results: -/// - greater than 1 = 100+% -/// - 1_0000000 = 100% -/// - 0_0000100 = ~10% -/// - 0_0000003 = ~5% -/// - 0_0000000 = ~0-4% pub fn require_pool_above_threshold(pool_backstop_data: &PoolBackstopData) -> bool { // @dev: Calculation for pools product constant of underlying will often overflow i128 // so saturating mul is used. This is safe because the threshold is below i128::MAX and the @@ -76,17 +67,16 @@ pub fn require_pool_above_threshold(pool_backstop_data: &PoolBackstopData) -> bo // The calculation is: // - Threshold % = (bal_blnd^4 * bal_usdc) / PC^5 such that PC is 200k let threshold_pc = 320_000_000_000_000_000_000_000_000i128; // 3.2e26 (200k^5) - // floor balances to nearest full unit and calculate saturated pool product constant - // and scale to SCALAR_7 to get final division result in SCALAR_7 points + + // floor balances to nearest full unit and calculate saturated pool product constant let bal_blnd = pool_backstop_data.blnd / SCALAR_7; let bal_usdc = pool_backstop_data.usdc / SCALAR_7; let saturating_pool_pc = bal_blnd .saturating_mul(bal_blnd) .saturating_mul(bal_blnd) .saturating_mul(bal_blnd) - .saturating_mul(bal_usdc) - .saturating_mul(SCALAR_7); // 10^7 * 10^7 - saturating_pool_pc / threshold_pc >= SCALAR_7 + .saturating_mul(bal_usdc); + saturating_pool_pc >= threshold_pc } /// The pool's backstop balances @@ -134,8 +124,6 @@ impl PoolBalance { /// Deposit tokens and shares into the pool /// - /// If this is the first time - /// /// ### Arguments /// * `tokens` - The amount of tokens to add /// * `shares` - The amount of shares to add diff --git a/backstop/src/backstop/withdrawal.rs b/backstop/src/backstop/withdrawal.rs index 29e4ee4d..4bf739f0 100644 --- a/backstop/src/backstop/withdrawal.rs +++ b/backstop/src/backstop/withdrawal.rs @@ -1,6 +1,6 @@ -use crate::{contract::require_nonnegative, emissions, storage}; +use crate::{contract::require_nonnegative, emissions, storage, BackstopError}; use sep_41_token::TokenClient; -use soroban_sdk::{unwrap::UnwrapOptimized, Address, Env}; +use soroban_sdk::{panic_with_error, unwrap::UnwrapOptimized, Address, Env}; use super::Q4W; @@ -56,6 +56,9 @@ pub fn execute_withdraw(e: &Env, from: &Address, pool_address: &Address, amount: user_balance.dequeue_shares_for_withdrawal(e, amount, true); let to_return = pool_balance.convert_to_tokens(amount); + if to_return == 0 { + panic_with_error!(e, &BackstopError::InvalidTokenWithdrawAmount); + } pool_balance.withdraw(e, to_return, amount); storage::set_user_balance(e, pool_address, from, &user_balance); @@ -75,7 +78,7 @@ mod tests { }; use crate::{ - backstop::{execute_deposit, execute_donate}, + backstop::{execute_deposit, execute_donate, execute_draw}, testutils::{ assert_eq_vec_q4w, create_backstop, create_backstop_token, create_mock_pool_factory, }, @@ -363,6 +366,7 @@ mod tests { assert_eq!(backstop_token_client.balance(&samwise), tokens); }); } + #[test] #[should_panic(expected = "Error(Contract, #8)")] fn test_execute_withdrawal_negative_amount() { @@ -413,4 +417,58 @@ mod tests { execute_withdraw(&e, &samwise, &pool_address, -42_0000000); }); } + + #[test] + #[should_panic(expected = "Error(Contract, #1006)")] + fn test_execute_withdrawal_zero_tokens() { + let e = Env::default(); + e.mock_all_auths_allowing_non_root_auth(); + + let backstop_address = create_backstop(&e); + let pool_address = Address::generate(&e); + let bombadil = Address::generate(&e); + let samwise = Address::generate(&e); + let frodo = Address::generate(&e); + + let (_, backstop_token_client) = create_backstop_token(&e, &backstop_address, &bombadil); + backstop_token_client.mint(&samwise, &150_0000000); + backstop_token_client.mint(&frodo, &150_0000000); + + let (_, mock_pool_factory_client) = create_mock_pool_factory(&e, &backstop_address); + mock_pool_factory_client.set_pool(&pool_address); + + e.ledger().set(LedgerInfo { + protocol_version: 20, + sequence_number: 200, + timestamp: 10000, + network_id: Default::default(), + base_reserve: 10, + min_temp_entry_ttl: 10, + min_persistent_entry_ttl: 10, + max_entry_ttl: 2000000, + }); + + // setup pool with queue for withdrawal and allow the backstop to incur a profit + e.as_contract(&backstop_address, || { + execute_deposit(&e, &frodo, &pool_address, 1_0000001); + execute_deposit(&e, &samwise, &pool_address, 1_0000000); + execute_queue_withdrawal(&e, &samwise, &pool_address, 1_0000000); + execute_draw(&e, &pool_address, 1_9999999, &frodo); + }); + + e.ledger().set(LedgerInfo { + protocol_version: 20, + sequence_number: 200, + timestamp: 10000 + 21 * 24 * 60 * 60 + 1, + network_id: Default::default(), + base_reserve: 10, + min_temp_entry_ttl: 10, + min_persistent_entry_ttl: 10, + max_entry_ttl: 2000000, + }); + + e.as_contract(&backstop_address, || { + execute_withdraw(&e, &samwise, &pool_address, 1_0000000); + }); + } } diff --git a/backstop/src/contract.rs b/backstop/src/contract.rs index d3037c37..72f6ecf7 100644 --- a/backstop/src/contract.rs +++ b/backstop/src/contract.rs @@ -135,7 +135,7 @@ pub trait Backstop { /********** Fund Management *********/ - /// Take backstop token from a pools backstop + /// (Only Pool) Take backstop token from a pools backstop /// /// ### Arguments /// * `from` - The address of the pool drawing tokens from the backstop @@ -144,10 +144,11 @@ pub trait Backstop { /// * `to` - The address to send the backstop tokens to /// /// ### Errors - /// If the pool does not have enough backstop tokens + /// If the pool does not have enough backstop tokens, or if the pool does + /// not authorize the call fn draw(e: Env, pool_address: Address, amount: i128, to: Address); - /// Sends backstop tokens from "from" to a pools backstop + /// (Only Pool) Sends backstop tokens from "from" to a pools backstop /// /// NOTE: This is not a deposit, and "from" will permanently lose access to the funds /// @@ -157,7 +158,8 @@ pub trait Backstop { /// * `amount` - The amount of BLND to add /// /// ### Errors - /// If the `pool_address` is not valid + /// If the `pool_address` is not valid, or if the pool does not + /// authorize the call fn donate(e: Env, from: Address, pool_address: Address, amount: i128); /// Updates the underlying value of 1 backstop token @@ -193,6 +195,8 @@ impl Backstop for BackstopContract { storage::set_blnd_token(&e, &blnd_token); storage::set_usdc_token(&e, &usdc_token); storage::set_pool_factory(&e, &pool_factory); + // NOTE: For a replacement backstop, this value likely needs to be stored in persistent storage to avoid + // an expiration occuring before a backstop swap is finalized. storage::set_drop_list(&e, &drop_list); storage::set_emitter(&e, &emitter); @@ -324,6 +328,7 @@ impl Backstop for BackstopContract { fn donate(e: Env, from: Address, pool_address: Address, amount: i128) { storage::extend_instance(&e); from.require_auth(); + pool_address.require_auth(); backstop::execute_donate(&e, &from, &pool_address, amount); e.events() diff --git a/backstop/src/errors.rs b/backstop/src/errors.rs index 8eaec76e..e5d3966c 100644 --- a/backstop/src/errors.rs +++ b/backstop/src/errors.rs @@ -22,4 +22,6 @@ pub enum BackstopError { InvalidRewardZoneEntry = 1002, InsufficientFunds = 1003, NotPool = 1004, + InvalidShareMintAmount = 1005, + InvalidTokenWithdrawAmount = 1006, } diff --git a/backstop/src/storage.rs b/backstop/src/storage.rs index b0febc17..bb62343d 100644 --- a/backstop/src/storage.rs +++ b/backstop/src/storage.rs @@ -81,10 +81,10 @@ pub fn extend_instance(e: &Env) { } /// Fetch an entry in persistent storage that has a default value if it doesn't exist -fn get_persistent_default, V: TryFromVal>( +fn get_persistent_default, V: TryFromVal, F: FnOnce() -> V>( e: &Env, key: &K, - default: V, + default: F, bump_threshold: u32, bump_amount: u32, ) -> V { @@ -94,7 +94,7 @@ fn get_persistent_default, V: TryFromVal>( .extend_ttl(key, bump_threshold, bump_amount); result } else { - default + default() } } @@ -217,7 +217,7 @@ pub fn get_user_balance(e: &Env, pool: &Address, user: &Address) -> UserBalance get_persistent_default( e, &key, - UserBalance { + || UserBalance { shares: 0, q4w: vec![&e], }, @@ -253,7 +253,7 @@ pub fn get_pool_balance(e: &Env, pool: &Address) -> PoolBalance { get_persistent_default( e, &key, - PoolBalance { + || PoolBalance { shares: 0, tokens: 0, q4w: 0, @@ -285,7 +285,7 @@ pub fn get_last_distribution_time(e: &Env) -> u64 { get_persistent_default( e, &Symbol::new(e, LAST_DISTRO_KEY), - 0u64, + || 0u64, LEDGER_THRESHOLD_SHARED, LEDGER_BUMP_SHARED, ) @@ -313,7 +313,7 @@ pub fn get_reward_zone(e: &Env) -> Vec
{ get_persistent_default( e, &Symbol::new(e, REWARD_ZONE_KEY), - vec![e], + || vec![e], LEDGER_THRESHOLD_SHARED, LEDGER_BUMP_SHARED, ) @@ -340,7 +340,13 @@ pub fn set_reward_zone(e: &Env, reward_zone: &Vec
) { /// * `pool` - The pool pub fn get_pool_emissions(e: &Env, pool: &Address) -> i128 { let key = BackstopDataKey::PoolEmis(pool.clone()); - get_persistent_default(e, &key, 0i128, LEDGER_THRESHOLD_SHARED, LEDGER_BUMP_SHARED) + get_persistent_default( + e, + &key, + || 0i128, + LEDGER_THRESHOLD_SHARED, + LEDGER_BUMP_SHARED, + ) } /// Set the current emissions accrued for the pool @@ -366,10 +372,10 @@ pub fn set_pool_emissions(e: &Env, pool: &Address, emissions: i128) { /// * `pool` - The pool pub fn get_backstop_emis_config(e: &Env, pool: &Address) -> Option { let key = BackstopDataKey::BEmisCfg(pool.clone()); - get_persistent_default::>( + get_persistent_default( e, &key, - None, + || None, LEDGER_THRESHOLD_SHARED, LEDGER_BUMP_SHARED, ) @@ -397,10 +403,10 @@ pub fn set_backstop_emis_config( /// * `pool` - The pool pub fn get_backstop_emis_data(e: &Env, pool: &Address) -> Option { let key = BackstopDataKey::BEmisData(pool.clone()); - get_persistent_default::>( + get_persistent_default( e, &key, - None, + || None, LEDGER_THRESHOLD_SHARED, LEDGER_BUMP_SHARED, ) @@ -428,13 +434,7 @@ pub fn get_user_emis_data(e: &Env, pool: &Address, user: &Address) -> Option>( - e, - &key, - None, - LEDGER_THRESHOLD_USER, - LEDGER_BUMP_USER, - ) + get_persistent_default(e, &key, || None, LEDGER_THRESHOLD_USER, LEDGER_BUMP_USER) } /// Set the user's backstop emissions data diff --git a/emitter/src/storage.rs b/emitter/src/storage.rs index a0e57ce8..b5928db3 100644 --- a/emitter/src/storage.rs +++ b/emitter/src/storage.rs @@ -178,7 +178,7 @@ pub fn set_last_distro_time(e: &Env, backstop: &Address, last_distro: u64) { /// Returns true if the emitter has dropped pub fn get_drop_status(e: &Env, backstop: &Address) -> bool { e.storage() - .instance() + .persistent() .get::(&EmitterDataKey::Dropped(backstop.clone())) .unwrap_or(false) } @@ -189,6 +189,6 @@ pub fn get_drop_status(e: &Env, backstop: &Address) -> bool { /// * `new_status` - new drop status pub fn set_drop_status(e: &Env, backstop: &Address) { e.storage() - .instance() + .persistent() .set::(&EmitterDataKey::Dropped(backstop.clone()), &true); } diff --git a/mocks/mock-pool-factory/src/pool_factory.rs b/mocks/mock-pool-factory/src/pool_factory.rs index 9662d5f3..0eefef10 100644 --- a/mocks/mock-pool-factory/src/pool_factory.rs +++ b/mocks/mock-pool-factory/src/pool_factory.rs @@ -69,6 +69,7 @@ impl MockPoolFactoryTrait for MockPoolFactory { max_positions: u32, ) -> Address { storage::extend_instance(&e); + admin.require_auth(); let pool_init_meta = storage::get_pool_init_meta(&e); // verify backstop take rate is within [0,1) with 9 decimals diff --git a/pool-factory/src/pool_factory.rs b/pool-factory/src/pool_factory.rs index 9891f942..298021e8 100644 --- a/pool-factory/src/pool_factory.rs +++ b/pool-factory/src/pool_factory.rs @@ -3,8 +3,8 @@ use crate::{ storage::{self, PoolInitMeta}, }; use soroban_sdk::{ - contract, contractclient, contractimpl, panic_with_error, vec, Address, BytesN, Env, IntoVal, - Symbol, Val, Vec, + contract, contractclient, contractimpl, panic_with_error, vec, Address, Bytes, BytesN, Env, + IntoVal, Symbol, Val, Vec, }; const SCALAR_7: u32 = 1_0000000; @@ -22,9 +22,10 @@ pub trait PoolFactory { /// Deploys and initializes a lending pool /// - /// # Arguments + /// ### Arguments /// * `admin` - The admin address for the pool /// * `name` - The name of the pool + /// * `salt` - The salt for the pool address /// * `oracle` - The oracle address for the pool /// * `backstop_take_rate` - The backstop take rate for the pool (7 decimals) /// * `max_positions` - The maximum user positions supported by the pool @@ -42,7 +43,7 @@ pub trait PoolFactory { /// /// Returns true if pool was deployed by factory and false otherwise /// - /// # Arguments + /// ### Arguments /// * `pool_id` - The contract address to be checked fn is_pool(e: Env, pool_id: Address) -> bool; } @@ -78,6 +79,17 @@ impl PoolFactory for PoolFactoryContract { panic_with_error!(&e, PoolFactoryError::InvalidPoolInitArgs); } + // verify max positions is at least 2 + if max_positions < 2 { + panic_with_error!(&e, PoolFactoryError::InvalidPoolInitArgs); + } + + let mut as_u8s: [u8; 56] = [0; 56]; + admin.to_string().copy_into_slice(&mut as_u8s); + let mut salt_as_bytes: Bytes = salt.into_val(&e); + salt_as_bytes.extend_from_array(&as_u8s); + let new_salt = e.crypto().keccak256(&salt_as_bytes); + let mut init_args: Vec = vec![&e]; init_args.push_back(admin.to_val()); init_args.push_back(name.to_val()); @@ -89,7 +101,7 @@ impl PoolFactory for PoolFactoryContract { init_args.push_back(pool_init_meta.usdc_id.to_val()); let pool_address = e .deployer() - .with_current_contract(salt) + .with_current_contract(new_salt) .deploy(pool_init_meta.pool_hash); e.invoke_contract::(&pool_address, &Symbol::new(&e, "initialize"), init_args); diff --git a/pool-factory/src/test.rs b/pool-factory/src/test.rs index 2ccf3af5..987bbd84 100644 --- a/pool-factory/src/test.rs +++ b/pool-factory/src/test.rs @@ -132,7 +132,7 @@ fn test_pool_factory() { #[test] #[should_panic(expected = "Error(Contract, #1300)")] -fn test_pool_factory_invalid_pool_init_args() { +fn test_pool_factory_invalid_pool_init_args_backstop_rate() { let e = Env::default(); e.budget().reset_unlimited(); e.mock_all_auths_allowing_non_root_auth(); @@ -169,3 +169,102 @@ fn test_pool_factory_invalid_pool_init_args() { &max_positions, ); } + +#[test] +#[should_panic(expected = "Error(Contract, #1300)")] +fn test_pool_factory_invalid_pool_init_args_max_positions() { + let e = Env::default(); + e.budget().reset_unlimited(); + e.mock_all_auths_allowing_non_root_auth(); + let (_, pool_factory_client) = create_pool_factory(&e); + + let wasm_hash = e.deployer().upload_contract_wasm(pool::WASM); + + let backstop_id = Address::generate(&e); + let blnd_id = Address::generate(&e); + let usdc_id = Address::generate(&e); + + let pool_init_meta = PoolInitMeta { + backstop: backstop_id.clone(), + pool_hash: wasm_hash.clone(), + blnd_id: blnd_id.clone(), + usdc_id: usdc_id.clone(), + }; + pool_factory_client.initialize(&pool_init_meta); + + let bombadil = Address::generate(&e); + let oracle = Address::generate(&e); + let backstop_rate: u32 = 0_1000000; + let max_positions: u32 = 1; + + let name1 = Symbol::new(&e, "pool1"); + let salt = BytesN::<32>::random(&e); + + pool_factory_client.deploy( + &bombadil, + &name1, + &salt, + &oracle, + &backstop_rate, + &max_positions, + ); +} + +#[test] +fn test_pool_factory_frontrun_protection() { + let e = Env::default(); + e.budget().reset_unlimited(); + e.mock_all_auths(); + + let (_, pool_factory_client) = create_pool_factory(&e); + + let wasm_hash = e.deployer().upload_contract_wasm(pool::WASM); + + let bombadil = Address::generate(&e); + let sauron = Address::generate(&e); + + let oracle = Address::generate(&e); + let backstop_id = Address::generate(&e); + let backstop_rate: u32 = 0_1000000; + let max_positions: u32 = 6; + let blnd_id = Address::generate(&e); + let usdc_id = Address::generate(&e); + + let pool_init_meta = PoolInitMeta { + backstop: backstop_id.clone(), + pool_hash: wasm_hash.clone(), + blnd_id: blnd_id.clone(), + usdc_id: usdc_id.clone(), + }; + pool_factory_client.initialize(&pool_init_meta); + + let name1 = Symbol::new(&e, "pool1"); + let name2 = Symbol::new(&e, "pool_front_run"); + let salt = BytesN::<32>::random(&e); + + // verify two different users don't get the same pool address with the same + // salt parameter + e.budget().reset_unlimited(); + let deployed_pool_address_sauron = pool_factory_client.deploy( + &sauron, + &name2, + &salt, + &oracle, + &backstop_rate, + &max_positions, + ); + e.budget().print(); + + let deployed_pool_address_bombadil = pool_factory_client.deploy( + &bombadil, + &name1, + &salt, + &oracle, + &backstop_rate, + &max_positions, + ); + + assert!(deployed_pool_address_sauron != deployed_pool_address_bombadil); + assert!(pool_factory_client.is_pool(&deployed_pool_address_sauron)); + assert!(pool_factory_client.is_pool(&deployed_pool_address_bombadil)); +} diff --git a/pool/src/auctions/auction.rs b/pool/src/auctions/auction.rs index 8ec87f85..a035af79 100644 --- a/pool/src/auctions/auction.rs +++ b/pool/src/auctions/auction.rs @@ -97,6 +97,11 @@ pub fn create_interest_auction(e: &Env, assets: &Vec
) -> AuctionData { /// ### Panics /// If the auction is unable to be created pub fn create_liquidation(e: &Env, user: &Address, percent_liquidated: u64) -> AuctionData { + let user_clone = user.clone(); + if user_clone == e.current_contract_address() || user_clone == storage::get_backstop(e) { + panic_with_error!(e, PoolError::InvalidLiquidation); + } + let auction_data = create_user_liq_auction_data(e, user, percent_liquidated); storage::set_auction( @@ -148,6 +153,9 @@ pub fn fill( filler_state: &mut User, percent_filled: u64, ) { + if user.clone() == filler_state.address { + panic_with_error!(e, PoolError::InvalidLiquidation); + } let auction_data = storage::get_auction(e, &auction_type, user); let (to_fill_auction, remaining_auction) = scale_auction(e, &auction_data, percent_filled); match AuctionType::from_u32(e, auction_type) { @@ -270,7 +278,6 @@ fn scale_auction( #[cfg(test)] mod tests { - use crate::{ pool::Positions, storage::PoolConfig, @@ -627,6 +634,7 @@ mod tests { max_positions: 4, }; e.as_contract(&pool_address, || { + storage::set_backstop(&e, &Address::generate(&e)); storage::set_user_positions(&e, &samwise, &positions); storage::set_pool_config(&e, &pool_config); @@ -636,6 +644,215 @@ mod tests { }); } + #[test] + #[should_panic(expected = "Error(Contract, #1211)")] + fn test_create_liquidation_for_pool() { + let e = Env::default(); + e.budget().reset_unlimited(); + e.mock_all_auths(); + e.ledger().set(LedgerInfo { + timestamp: 12345, + protocol_version: 20, + sequence_number: 50, + network_id: Default::default(), + base_reserve: 10, + min_temp_entry_ttl: 10, + min_persistent_entry_ttl: 10, + max_entry_ttl: 2000000, + }); + + let bombadil = Address::generate(&e); + + let pool_address = create_pool(&e); + let (oracle_address, oracle_client) = testutils::create_mock_oracle(&e); + + let (underlying_0, _) = testutils::create_token_contract(&e, &bombadil); + let (mut reserve_config_0, mut reserve_data_0) = testutils::default_reserve_meta(); + reserve_data_0.last_time = 12345; + reserve_data_0.b_rate = 1_100_000_000; + reserve_config_0.c_factor = 0_8500000; + reserve_config_0.l_factor = 0_9000000; + reserve_config_0.index = 0; + testutils::create_reserve( + &e, + &pool_address, + &underlying_0, + &reserve_config_0, + &reserve_data_0, + ); + + let (underlying_1, _) = testutils::create_token_contract(&e, &bombadil); + let (mut reserve_config_1, mut reserve_data_1) = testutils::default_reserve_meta(); + reserve_data_1.b_rate = 1_200_000_000; + reserve_config_1.c_factor = 0_7500000; + reserve_config_1.l_factor = 0_7500000; + reserve_data_1.last_time = 12345; + reserve_config_1.index = 1; + testutils::create_reserve( + &e, + &pool_address, + &underlying_1, + &reserve_config_1, + &reserve_data_1, + ); + + let (underlying_2, _) = testutils::create_token_contract(&e, &bombadil); + let (mut reserve_config_2, reserve_data_2) = testutils::default_reserve_meta(); + reserve_config_2.c_factor = 0_0000000; + reserve_config_2.l_factor = 0_7000000; + reserve_config_2.index = 2; + testutils::create_reserve( + &e, + &pool_address, + &underlying_2, + &reserve_config_2, + &reserve_data_2, + ); + + oracle_client.set_data( + &bombadil, + &Asset::Other(Symbol::new(&e, "USD")), + &vec![ + &e, + Asset::Stellar(underlying_0), + Asset::Stellar(underlying_1), + Asset::Stellar(underlying_2), + ], + &7, + &300, + ); + oracle_client.set_price_stable(&vec![&e, 2_0000000, 4_0000000, 50_0000000]); + + let liq_pct = 45; + let positions: Positions = Positions { + collateral: map![ + &e, + (reserve_config_0.index, 90_9100000), + (reserve_config_1.index, 04_5800000), + ], + liabilities: map![&e, (reserve_config_2.index, 02_7500000),], + supply: map![&e], + }; + let pool_config = PoolConfig { + oracle: oracle_address, + bstop_rate: 0_1000000, + status: 0, + max_positions: 4, + }; + e.as_contract(&pool_address, || { + storage::set_backstop(&e, &Address::generate(&e)); + storage::set_user_positions(&e, &pool_address, &positions); + storage::set_pool_config(&e, &pool_config); + + create_liquidation(&e, &pool_address, liq_pct); + }); + } + + #[test] + #[should_panic(expected = "Error(Contract, #1211)")] + fn test_create_liquidation_for_backstop() { + let e = Env::default(); + e.budget().reset_unlimited(); + e.mock_all_auths(); + e.ledger().set(LedgerInfo { + timestamp: 12345, + protocol_version: 20, + sequence_number: 50, + network_id: Default::default(), + base_reserve: 10, + min_temp_entry_ttl: 10, + min_persistent_entry_ttl: 10, + max_entry_ttl: 2000000, + }); + + let bombadil = Address::generate(&e); + + let pool_address = create_pool(&e); + let backstop = Address::generate(&e); + let (oracle_address, oracle_client) = testutils::create_mock_oracle(&e); + + let (underlying_0, _) = testutils::create_token_contract(&e, &bombadil); + let (mut reserve_config_0, mut reserve_data_0) = testutils::default_reserve_meta(); + reserve_data_0.last_time = 12345; + reserve_data_0.b_rate = 1_100_000_000; + reserve_config_0.c_factor = 0_8500000; + reserve_config_0.l_factor = 0_9000000; + reserve_config_0.index = 0; + testutils::create_reserve( + &e, + &pool_address, + &underlying_0, + &reserve_config_0, + &reserve_data_0, + ); + + let (underlying_1, _) = testutils::create_token_contract(&e, &bombadil); + let (mut reserve_config_1, mut reserve_data_1) = testutils::default_reserve_meta(); + reserve_data_1.b_rate = 1_200_000_000; + reserve_config_1.c_factor = 0_7500000; + reserve_config_1.l_factor = 0_7500000; + reserve_data_1.last_time = 12345; + reserve_config_1.index = 1; + testutils::create_reserve( + &e, + &pool_address, + &underlying_1, + &reserve_config_1, + &reserve_data_1, + ); + + let (underlying_2, _) = testutils::create_token_contract(&e, &bombadil); + let (mut reserve_config_2, reserve_data_2) = testutils::default_reserve_meta(); + reserve_config_2.c_factor = 0_0000000; + reserve_config_2.l_factor = 0_7000000; + reserve_config_2.index = 2; + testutils::create_reserve( + &e, + &pool_address, + &underlying_2, + &reserve_config_2, + &reserve_data_2, + ); + + oracle_client.set_data( + &bombadil, + &Asset::Other(Symbol::new(&e, "USD")), + &vec![ + &e, + Asset::Stellar(underlying_0), + Asset::Stellar(underlying_1), + Asset::Stellar(underlying_2), + ], + &7, + &300, + ); + oracle_client.set_price_stable(&vec![&e, 2_0000000, 4_0000000, 50_0000000]); + + let liq_pct = 45; + let positions: Positions = Positions { + collateral: map![ + &e, + (reserve_config_0.index, 90_9100000), + (reserve_config_1.index, 04_5800000), + ], + liabilities: map![&e, (reserve_config_2.index, 02_7500000),], + supply: map![&e], + }; + let pool_config = PoolConfig { + oracle: oracle_address, + bstop_rate: 0_1000000, + status: 0, + max_positions: 4, + }; + e.as_contract(&pool_address, || { + storage::set_backstop(&e, &backstop); + storage::set_user_positions(&e, &backstop, &positions); + storage::set_pool_config(&e, &pool_config); + + create_liquidation(&e, &backstop, liq_pct); + }); + } + #[test] fn test_delete_user_liquidation() { let e = Env::default(); @@ -1458,6 +1675,112 @@ mod tests { assert!(remaining_auction.is_none()); } + #[test] + #[should_panic(expected = "Error(Contract, #1211)")] + fn test_fill_liquidation_same_address() { + let e = Env::default(); + + e.mock_all_auths(); + e.ledger().set(LedgerInfo { + timestamp: 12345, + protocol_version: 20, + sequence_number: 175, + network_id: Default::default(), + base_reserve: 10, + min_temp_entry_ttl: 172800, + min_persistent_entry_ttl: 172800, + max_entry_ttl: 9999999, + }); + + let bombadil = Address::generate(&e); + let samwise = Address::generate(&e); + + let pool_address = create_pool(&e); + + let (oracle_address, _) = testutils::create_mock_oracle(&e); + + // creating reserves for a pool exhausts the budget + e.budget().reset_unlimited(); + let (underlying_0, _) = testutils::create_token_contract(&e, &bombadil); + let (mut reserve_config_0, reserve_data_0) = testutils::default_reserve_meta(); + reserve_config_0.index = 0; + testutils::create_reserve( + &e, + &pool_address, + &underlying_0, + &reserve_config_0, + &reserve_data_0, + ); + + let (underlying_1, _) = testutils::create_token_contract(&e, &bombadil); + let (mut reserve_config_1, reserve_data_1) = testutils::default_reserve_meta(); + reserve_config_1.index = 1; + testutils::create_reserve( + &e, + &pool_address, + &underlying_1, + &reserve_config_1, + &reserve_data_1, + ); + + let (underlying_2, _) = testutils::create_token_contract(&e, &bombadil); + let (mut reserve_config_2, reserve_data_2) = testutils::default_reserve_meta(); + reserve_config_2.index = 2; + testutils::create_reserve( + &e, + &pool_address, + &underlying_2, + &reserve_config_2, + &reserve_data_2, + ); + e.budget().reset_unlimited(); + + let auction_data = AuctionData { + bid: map![&e, (underlying_2.clone(), 1_2375000)], + lot: map![ + &e, + (underlying_0.clone(), 30_5595329), + (underlying_1.clone(), 1_5395739) + ], + block: 176, + }; + let pool_config = PoolConfig { + oracle: oracle_address, + bstop_rate: 0_1000000, + status: 0, + max_positions: 4, + }; + let positions: Positions = Positions { + collateral: map![ + &e, + (reserve_config_0.index, 90_9100000), + (reserve_config_1.index, 04_5800000), + ], + liabilities: map![&e, (reserve_config_2.index, 02_7500000),], + supply: map![&e], + }; + e.as_contract(&pool_address, || { + storage::set_user_positions(&e, &samwise, &positions); + storage::set_pool_config(&e, &pool_config); + storage::set_auction(&e, &0, &samwise, &auction_data); + + e.ledger().set(LedgerInfo { + timestamp: 12345 + 200 * 5, + protocol_version: 20, + sequence_number: 176 + 200, + network_id: Default::default(), + base_reserve: 10, + min_temp_entry_ttl: 172800, + min_persistent_entry_ttl: 172800, + max_entry_ttl: 9999999, + }); + e.budget().reset_unlimited(); + let mut pool = Pool::load(&e); + let mut samwise_state = User::load(&e, &samwise); + fill(&e, &mut pool, 0, &samwise, &mut samwise_state, 100); + }); + } + #[test] fn test_scale_auction_not_100_fill_pct() { // @dev: bids always round up, lots always round down diff --git a/pool/src/auctions/user_liquidation_auction.rs b/pool/src/auctions/user_liquidation_auction.rs index 1b365d3e..1f5085b6 100644 --- a/pool/src/auctions/user_liquidation_auction.rs +++ b/pool/src/auctions/user_liquidation_auction.rs @@ -4,7 +4,6 @@ use soroban_sdk::unwrap::UnwrapOptimized; use soroban_sdk::{map, panic_with_error, Address, Env}; use crate::auctions::auction::AuctionData; -use crate::constants::SCALAR_7; use crate::pool::{Pool, PositionData, User}; use crate::{errors::PoolError, storage}; @@ -22,7 +21,6 @@ pub fn create_user_liq_auction_data( if percent_liquidated > 100 || percent_liquidated == 0 { panic_with_error!(e, PoolError::InvalidLiquidation); } - let percent_liquidated_i128 = i128(percent_liquidated) * 1_00000; // scale to decimal form with 7 decimals let mut liquidation_quote = AuctionData { bid: map![e], @@ -30,7 +28,6 @@ pub fn create_user_liq_auction_data( block: e.ledger().sequence() + 1, }; let mut pool = Pool::load(e); - let oracle_scalar = 10i128.pow(pool.load_price_decimals(e)); let mut user_state = User::load(e, user); let reserve_list = storage::get_res_list(e); @@ -41,32 +38,37 @@ pub fn create_user_liq_auction_data( panic_with_error!(e, PoolError::InvalidLiquidation); } + let percent_liquidated_i128_scaled = i128(percent_liquidated) * position_data.scalar / 100; // scale to decimal form with scalar decimals + // ensure liquidation size is fair and the collateral is large enough to allow for the auction to price the liquidation let avg_cf = position_data .collateral_base - .fixed_div_floor(position_data.collateral_raw, oracle_scalar) + .fixed_div_floor(position_data.collateral_raw, position_data.scalar) .unwrap_optimized(); // avg_lf is the inverse of the average liability factor let avg_lf = position_data .liability_base - .fixed_div_floor(position_data.liability_raw, oracle_scalar) + .fixed_div_floor(position_data.liability_raw, position_data.scalar) .unwrap_optimized(); - let est_incentive = (SCALAR_7 - avg_cf.fixed_div_ceil(avg_lf, SCALAR_7).unwrap_optimized()) - .fixed_div_ceil(2 * SCALAR_7, SCALAR_7) - .unwrap_optimized() - + SCALAR_7; + let est_incentive = (position_data.scalar + - avg_cf + .fixed_div_ceil(avg_lf, position_data.scalar) + .unwrap_optimized()) + .fixed_div_ceil(2 * position_data.scalar, position_data.scalar) + .unwrap_optimized() + + position_data.scalar; let est_withdrawn_collateral = position_data .liability_raw - .fixed_mul_floor(percent_liquidated_i128, oracle_scalar) + .fixed_mul_floor(percent_liquidated_i128_scaled, position_data.scalar) .unwrap_optimized() - .fixed_mul_floor(est_incentive, SCALAR_7) + .fixed_mul_floor(est_incentive, position_data.scalar) .unwrap_optimized(); let mut est_withdrawn_collateral_pct = est_withdrawn_collateral - .fixed_div_ceil(position_data.collateral_raw, oracle_scalar) + .fixed_div_ceil(position_data.collateral_raw, position_data.scalar) .unwrap_optimized(); - if est_withdrawn_collateral_pct > SCALAR_7 { - est_withdrawn_collateral_pct = SCALAR_7; + if est_withdrawn_collateral_pct > position_data.scalar { + est_withdrawn_collateral_pct = position_data.scalar; } for (asset, amount) in user_state.positions.collateral.iter() { @@ -74,7 +76,7 @@ pub fn create_user_liq_auction_data( // Note: we multiply balance by estimated withdrawn collateral percent to allow // smoother scaling of liquidation modifiers let b_tokens_removed = amount - .fixed_mul_ceil(est_withdrawn_collateral_pct, SCALAR_7) + .fixed_mul_ceil(est_withdrawn_collateral_pct, position_data.scalar) .unwrap_optimized(); liquidation_quote .lot @@ -84,7 +86,7 @@ pub fn create_user_liq_auction_data( for (asset, amount) in user_state.positions.liabilities.iter() { let res_asset_address = reserve_list.get_unchecked(asset); let d_tokens_removed = amount - .fixed_mul_ceil(percent_liquidated_i128, SCALAR_7) + .fixed_mul_ceil(percent_liquidated_i128_scaled, position_data.scalar) .unwrap_optimized(); liquidation_quote .bid @@ -103,17 +105,17 @@ pub fn create_user_liq_auction_data( liquidation_quote.lot.clone(), liquidation_quote.bid.clone(), ); - let new_hf = PositionData::calculate_from_positions(e, &mut pool, &user_state.positions) - .as_health_factor(); + let new_data = PositionData::calculate_from_positions(e, &mut pool, &user_state.positions); - //check if liq is too large - if new_hf > 1_1500000 { - panic_with_error!(e, PoolError::InvalidLiqTooLarge); - } - // check if liq is too small - if new_hf < 1_0300000 { - panic_with_error!(e, PoolError::InvalidLiqTooSmall); - } + // Post-liq health factor must be under 1.15 + if new_data.is_hf_over(1_1500000) { + panic_with_error!(e, PoolError::InvalidLiqTooLarge) + }; + + // Post-liq heath factor must be over 1.03 + if new_data.is_hf_under(1_0300000) { + panic_with_error!(e, PoolError::InvalidLiqTooSmall) + }; } liquidation_quote } @@ -196,7 +198,7 @@ mod tests { } #[test] - fn test_create_user_liquidation_auction() { + fn test_create_user_liquidation_auction_normal_scalars() { let e = Env::default(); e.mock_all_auths(); @@ -296,7 +298,6 @@ mod tests { storage::set_user_positions(&e, &samwise, &positions); storage::set_pool_config(&e, &pool_config); - e.budget().reset_unlimited(); let result = create_user_liq_auction_data(&e, &samwise, liq_pct); assert_eq!(result.block, 51); assert_eq!(result.bid.get_unchecked(underlying_2), 1_2375000); @@ -307,6 +308,189 @@ mod tests { }); } + #[test] + fn test_create_user_liquidation_auction_weird_scalar() { + let e = Env::default(); + + e.mock_all_auths(); + e.ledger().set(LedgerInfo { + timestamp: 12345, + protocol_version: 20, + sequence_number: 50, + network_id: Default::default(), + base_reserve: 10, + min_temp_entry_ttl: 10, + min_persistent_entry_ttl: 10, + max_entry_ttl: 2000000, + }); + + let bombadil = Address::generate(&e); + let samwise = Address::generate(&e); + + let pool_address = create_pool(&e); + let (oracle_address, oracle_client) = testutils::create_mock_oracle(&e); + + // creating reserves for a pool exhausts the budget + e.budget().reset_unlimited(); + let (underlying_0, _) = testutils::create_token_contract(&e, &bombadil); + let (mut reserve_config_0, mut reserve_data_0) = testutils::default_reserve_meta(); + reserve_data_0.last_time = 12345; + reserve_data_0.b_rate = 1_000_206_159; + reserve_config_0.c_factor = 0_9000000; + reserve_config_0.l_factor = 0_9000000; + reserve_config_0.index = 0; + testutils::create_reserve( + &e, + &pool_address, + &underlying_0, + &reserve_config_0, + &reserve_data_0, + ); + + let (underlying_1, _) = testutils::create_token_contract(&e, &bombadil); + let (mut reserve_config_1, mut reserve_data_1) = testutils::default_reserve_meta(); + reserve_config_1.c_factor = 0_0000000; + reserve_config_1.l_factor = 0_9000000; + reserve_config_1.index = 1; + reserve_data_1.d_rate = 1000201748; + testutils::create_reserve( + &e, + &pool_address, + &underlying_1, + &reserve_config_1, + &reserve_data_1, + ); + + oracle_client.set_data( + &bombadil, + &Asset::Other(Symbol::new(&e, "USD")), + &vec![ + &e, + Asset::Stellar(underlying_0.clone()), + Asset::Stellar(underlying_1.clone()), + ], + &14, + &300, + ); + oracle_client.set_price_stable(&vec![&e, 1418501_2444444, 1_0261166_9700969]); + + let liq_pct = 69; + let positions: Positions = Positions { + collateral: map![&e, (reserve_config_0.index, 8999_1357639),], + liabilities: map![&e, (reserve_config_1.index, 1059_5526742),], + supply: map![&e], + }; + let pool_config = PoolConfig { + oracle: oracle_address, + bstop_rate: 0_1000000, + status: 0, + max_positions: 4, + }; + e.as_contract(&pool_address, || { + storage::set_user_positions(&e, &samwise, &positions); + storage::set_pool_config(&e, &pool_config); + + let result = create_user_liq_auction_data(&e, &samwise, liq_pct); + + assert_eq!(result.block, 51); + assert_eq!(result.bid.get_unchecked(underlying_1), 731_0913452); + assert_eq!(result.bid.len(), 1); + assert_eq!(result.lot.get_unchecked(underlying_0), 5791_1010751); + assert_eq!(result.lot.len(), 1); + }); + } + + #[test] + fn test_create_user_liquidation_auction_full_liquidation() { + let e = Env::default(); + + e.mock_all_auths(); + e.ledger().set(LedgerInfo { + timestamp: 12345, + protocol_version: 20, + sequence_number: 50, + network_id: Default::default(), + base_reserve: 10, + min_temp_entry_ttl: 10, + min_persistent_entry_ttl: 10, + max_entry_ttl: 2000000, + }); + + let bombadil = Address::generate(&e); + let samwise = Address::generate(&e); + + let pool_address = create_pool(&e); + let (oracle_address, oracle_client) = testutils::create_mock_oracle(&e); + + // creating reserves for a pool exhausts the budget + e.budget().reset_unlimited(); + let (underlying_0, _) = testutils::create_token_contract(&e, &bombadil); + let (mut reserve_config_0, mut reserve_data_0) = testutils::default_reserve_meta(); + reserve_data_0.last_time = 12345; + reserve_data_0.b_rate = 1_000_206_159; + reserve_config_0.c_factor = 0_9000000; + reserve_config_0.l_factor = 0_9000000; + reserve_config_0.index = 0; + testutils::create_reserve( + &e, + &pool_address, + &underlying_0, + &reserve_config_0, + &reserve_data_0, + ); + + let (underlying_1, _) = testutils::create_token_contract(&e, &bombadil); + let (mut reserve_config_1, mut reserve_data_1) = testutils::default_reserve_meta(); + reserve_config_1.c_factor = 0_0000000; + reserve_config_1.l_factor = 0_9000000; + reserve_config_1.index = 1; + reserve_config_1.decimals = 6; + reserve_data_1.d_rate = 1000201748; + testutils::create_reserve( + &e, + &pool_address, + &underlying_1, + &reserve_config_1, + &reserve_data_1, + ); + + oracle_client.set_data( + &bombadil, + &Asset::Other(Symbol::new(&e, "USD")), + &vec![ + &e, + Asset::Stellar(underlying_0.clone()), + Asset::Stellar(underlying_1.clone()), + ], + &5, + &300, + ); + oracle_client.set_price_stable(&vec![&e, 1_00000, 1_00000]); + + let liq_pct = 100; + let positions: Positions = Positions { + collateral: map![&e, (reserve_config_0.index, 8_000_0000),], + liabilities: map![&e, (reserve_config_1.index, 100_000_000),], + supply: map![&e], + }; + let pool_config = PoolConfig { + oracle: oracle_address, + bstop_rate: 0_1000000, + status: 0, + max_positions: 4, + }; + e.as_contract(&pool_address, || { + storage::set_user_positions(&e, &samwise, &positions); + storage::set_pool_config(&e, &pool_config); + let result = create_user_liq_auction_data(&e, &samwise, liq_pct); + assert_eq!(result.block, 51); + assert_eq!(result.bid.get_unchecked(underlying_1), 10_0000000); + assert_eq!(result.bid.len(), 1); + assert_eq!(result.lot.get_unchecked(underlying_0), 8_0000000); + assert_eq!(result.lot.len(), 1); + }); + } + #[test] #[should_panic(expected = "Error(Contract, #1213)")] fn test_create_user_liquidation_auction_bad_full_liq() { @@ -375,7 +559,6 @@ mod tests { &reserve_config_2, &reserve_data_2, ); - e.budget().reset_unlimited(); oracle_client.set_data( &bombadil, @@ -386,10 +569,10 @@ mod tests { Asset::Stellar(underlying_1), Asset::Stellar(underlying_2), ], - &7, + &8, &300, ); - oracle_client.set_price_stable(&vec![&e, 2_0000000, 4_0000000, 50_0000000]); + oracle_client.set_price_stable(&vec![&e, 2_0000000_0, 4_0000000_0, 50_0000000_0]); let liq_pct = 100; let pool_config = PoolConfig { @@ -411,7 +594,6 @@ mod tests { storage::set_user_positions(&e, &samwise, &positions); storage::set_pool_config(&e, &pool_config); - e.budget().reset_unlimited(); create_user_liq_auction_data(&e, &samwise, liq_pct); }); } @@ -483,7 +665,6 @@ mod tests { &reserve_config_2, &reserve_data_2, ); - e.budget().reset_unlimited(); oracle_client.set_data( &bombadil, @@ -494,10 +675,10 @@ mod tests { Asset::Stellar(underlying_1), Asset::Stellar(underlying_2), ], - &7, + &6, &300, ); - oracle_client.set_price_stable(&vec![&e, 2_0000000, 4_0000000, 50_0000000]); + oracle_client.set_price_stable(&vec![&e, 2_000000, 4_000000, 50_000000]); let liq_pct = 46; let pool_config = PoolConfig { @@ -519,7 +700,6 @@ mod tests { storage::set_user_positions(&e, &samwise, &positions); storage::set_pool_config(&e, &pool_config); - e.budget().reset_unlimited(); create_user_liq_auction_data(&e, &samwise, liq_pct); }); } @@ -592,7 +772,6 @@ mod tests { &reserve_config_2, &reserve_data_2, ); - e.budget().reset_unlimited(); oracle_client.set_data( &bombadil, @@ -603,10 +782,10 @@ mod tests { Asset::Stellar(underlying_1), Asset::Stellar(underlying_2), ], - &7, + &5, &300, ); - oracle_client.set_price_stable(&vec![&e, 2_0000000, 4_0000000, 50_0000000]); + oracle_client.set_price_stable(&vec![&e, 2_00000, 4_00000, 50_00000]); let liq_pct = 25; let pool_config = PoolConfig { @@ -628,7 +807,6 @@ mod tests { storage::set_user_positions(&e, &samwise, &positions); storage::set_pool_config(&e, &pool_config); - e.budget().reset_unlimited(); create_user_liq_auction_data(&e, &samwise, liq_pct); }); } @@ -701,7 +879,6 @@ mod tests { &reserve_config_2, &reserve_data_2, ); - e.budget().reset_unlimited(); oracle_client.set_data( &bombadil, @@ -758,7 +935,6 @@ mod tests { min_persistent_entry_ttl: 17280, max_entry_ttl: 9999999, }); - e.budget().reset_unlimited(); let mut pool = Pool::load(&e); let mut frodo_state = User::load(&e, &frodo); fill_user_liq_auction(&e, &mut pool, &mut auction_data, &samwise, &mut frodo_state); @@ -877,7 +1053,6 @@ mod tests { &reserve_config_2, &reserve_data_2, ); - e.budget().reset_unlimited(); oracle_client.set_data( &bombadil, @@ -934,7 +1109,6 @@ mod tests { min_persistent_entry_ttl: 17280, max_entry_ttl: 9999999, }); - e.budget().reset_unlimited(); let mut pool = Pool::load(&e); let mut frodo_state = User::load(&e, &frodo); fill_user_liq_auction(&e, &mut pool, &mut auction_data, &samwise, &mut frodo_state); @@ -942,7 +1116,7 @@ mod tests { let samwise_hf = PositionData::calculate_from_positions(&e, &mut pool, &samwise_positions) .as_health_factor(); - assert_eq!(samwise_hf, 1_1458978); + assert_eq!(samwise_hf, 1_1458977); }); } } diff --git a/pool/src/contract.rs b/pool/src/contract.rs index ab9761c4..3ad13a8f 100644 --- a/pool/src/contract.rs +++ b/pool/src/contract.rs @@ -247,6 +247,7 @@ impl Pool for PoolContract { usdc_id: Address, ) { storage::extend_instance(&e); + admin.require_auth(); pool::execute_initialize( &e, @@ -311,7 +312,7 @@ impl Pool for PoolContract { } fn set_reserve(e: Env, asset: Address) -> u32 { - let index = pool::execute_set_queued_reserve(&e, &asset); + let index = pool::execute_set_reserve(&e, &asset); e.events() .publish((Symbol::new(&e, "set_reserve"),), (asset, index)); diff --git a/pool/src/errors.rs b/pool/src/errors.rs index 7222b6e0..80acfd0a 100644 --- a/pool/src/errors.rs +++ b/pool/src/errors.rs @@ -39,4 +39,10 @@ pub enum PoolError { InvalidLiqTooLarge = 1213, InvalidLiqTooSmall = 1214, InterestTooSmall = 1215, + + // Share Token Errors + InvalidBTokenMintAmount = 1216, + InvalidBTokenBurnAmount = 1217, + InvalidDTokenMintAmount = 1218, + InvalidDTokenBurnAmount = 1219, } diff --git a/pool/src/pool/config.rs b/pool/src/pool/config.rs index c20f99bc..f3bbb802 100644 --- a/pool/src/pool/config.rs +++ b/pool/src/pool/config.rs @@ -1,7 +1,9 @@ use crate::{ constants::{SCALAR_7, SCALAR_9, SECONDS_PER_WEEK}, errors::PoolError, - storage::{self, PoolConfig, QueuedReserveInit, ReserveConfig, ReserveData}, + storage::{ + self, has_queued_reserve_set, PoolConfig, QueuedReserveInit, ReserveConfig, ReserveData, + }, }; use soroban_sdk::{panic_with_error, Address, Env, Symbol}; @@ -31,6 +33,11 @@ pub fn execute_initialize( panic_with_error!(e, PoolError::InvalidPoolInitArgs); } + // verify max positions is at least 2 + if *max_positions < 2 { + panic_with_error!(&e, PoolError::InvalidPoolInitArgs); + } + storage::set_admin(e, admin); storage::set_name(e, name); storage::set_backstop(e, backstop_address); @@ -63,6 +70,9 @@ pub fn execute_update_pool(e: &Env, backstop_take_rate: u32, max_positions: u32) /// Execute a queueing a reserve initialization for the pool pub fn execute_queue_set_reserve(e: &Env, asset: &Address, metadata: &ReserveConfig) { + if has_queued_reserve_set(e, asset) { + panic_with_error!(&e, PoolError::BadRequest) + } require_valid_reserve_metadata(e, metadata); let mut unlock_time = e.ledger().timestamp(); // require a timelock if pool status is not setup @@ -85,7 +95,7 @@ pub fn execute_cancel_queued_set_reserve(e: &Env, asset: &Address) { } /// Execute a queued reserve initialization for the pool -pub fn execute_set_queued_reserve(e: &Env, asset: &Address) -> u32 { +pub fn execute_set_reserve(e: &Env, asset: &Address) -> u32 { let queued_init = storage::get_queued_reserve_set(e, asset); if queued_init.unlock_time > e.ledger().timestamp() { @@ -115,7 +125,8 @@ fn initialize_reserve(e: &Env, asset: &Address, config: &ReserveConfig) -> u32 { panic_with_error!(e, PoolError::InvalidReserveMetadata); } // if any of the IR parameters were changed reset the IR modifier - if reserve_config.r_one != config.r_one + if reserve_config.r_base != config.r_base + || reserve_config.r_one != config.r_one || reserve_config.r_two != config.r_two || reserve_config.r_three != config.r_three || reserve_config.util != config.util @@ -144,6 +155,7 @@ fn initialize_reserve(e: &Env, asset: &Address, config: &ReserveConfig) -> u32 { l_factor: config.l_factor, util: config.util, max_util: config.max_util, + r_base: config.r_base, r_one: config.r_one, r_two: config.r_two, r_three: config.r_three, @@ -162,6 +174,8 @@ fn require_valid_reserve_metadata(e: &Env, metadata: &ReserveConfig) { || metadata.l_factor > SCALAR_7_U32 || metadata.util > 0_9500000 || (metadata.max_util > SCALAR_7_U32 || metadata.max_util <= metadata.util) + || metadata.r_base >= 1_0000000 + || metadata.r_base < 0_0001000 || (metadata.r_one > metadata.r_two || metadata.r_two > metadata.r_three) || (metadata.reactivity > 0_0001000) { @@ -284,10 +298,28 @@ mod tests { &blnd_id, &usdc_id, ); + }); + } + + #[test] + #[should_panic(expected = "Error(Contract, #1201)")] + fn test_execute_initialize_bad_max_positions() { + let e = Env::default(); + let pool = testutils::create_pool(&e); + + let admin = Address::generate(&e); + let name = Symbol::new(&e, "pool_name"); + let oracle = Address::generate(&e); + let bstop_rate = 0_1000000; + let max_positions = 1; + let backstop_address = Address::generate(&e); + let blnd_id = Address::generate(&e); + let usdc_id = Address::generate(&e); + e.as_contract(&pool, || { execute_initialize( &e, - &Address::generate(&e), + &admin, &name, &oracle, &bstop_rate, @@ -343,7 +375,7 @@ mod tests { } #[test] - fn test_queue_initial_reserve() { + fn test_queue_set_reserve_status_6() { let e = Env::default(); let pool = testutils::create_pool(&e); let bombadil = Address::generate(&e); @@ -357,6 +389,7 @@ mod tests { l_factor: 0_7500000, util: 0_5000000, max_util: 0_9500000, + r_base: 0_0100000, r_one: 0_0500000, r_two: 0_5000000, r_three: 1_5000000, @@ -373,22 +406,23 @@ mod tests { execute_queue_set_reserve(&e, &asset_id_0, &metadata); let queued_res = storage::get_queued_reserve_set(&e, &asset_id_0); let res_config_0 = queued_res.new_config; - assert_eq!(queued_res.unlock_time, e.ledger().timestamp()); assert_eq!(res_config_0.decimals, metadata.decimals); assert_eq!(res_config_0.c_factor, metadata.c_factor); assert_eq!(res_config_0.l_factor, metadata.l_factor); assert_eq!(res_config_0.util, metadata.util); - assert_eq!(res_config_0.max_util, metadata.max_util); + assert_eq!(res_config_0.r_base, metadata.r_base); + assert_eq!(res_config_0.r_one, metadata.r_one); assert_eq!(res_config_0.r_one, metadata.r_one); assert_eq!(res_config_0.r_two, metadata.r_two); assert_eq!(res_config_0.r_three, metadata.r_three); assert_eq!(res_config_0.reactivity, metadata.reactivity); assert_eq!(res_config_0.index, 0); + assert_eq!(queued_res.unlock_time, e.ledger().timestamp()); }); } #[test] - fn test_execute_queue_reserve_initialization() { + fn test_queue_set_reserve() { let e = Env::default(); let pool = testutils::create_pool(&e); let bombadil = Address::generate(&e); @@ -402,6 +436,7 @@ mod tests { l_factor: 0_7500000, util: 0_5000000, max_util: 0_9500000, + r_base: 0_0100000, r_one: 0_0500000, r_two: 0_5000000, r_three: 1_5000000, @@ -422,6 +457,7 @@ mod tests { assert_eq!(queued_init.new_config.l_factor, metadata.l_factor); assert_eq!(queued_init.new_config.util, metadata.util); assert_eq!(queued_init.new_config.max_util, metadata.max_util); + assert_eq!(queued_init.new_config.r_base, metadata.r_base); assert_eq!(queued_init.new_config.r_one, metadata.r_one); assert_eq!(queued_init.new_config.r_two, metadata.r_two); assert_eq!(queued_init.new_config.r_three, metadata.r_three); @@ -433,9 +469,10 @@ mod tests { ); }); } + #[test] - #[should_panic(expected = "Error(Storage, MissingValue)")] - fn test_execute_cancel_queued_reserve_initialization() { + #[should_panic(expected = "Error(Contract, #1200)")] + fn test_queue_set_reserve_duplicate() { let e = Env::default(); let pool = testutils::create_pool(&e); let bombadil = Address::generate(&e); @@ -449,70 +486,65 @@ mod tests { l_factor: 0_7500000, util: 0_5000000, max_util: 0_9500000, + r_base: 0_0100000, r_one: 0_0500000, r_two: 0_5000000, r_three: 1_5000000, reactivity: 100, }; + let pool_config = PoolConfig { + oracle: Address::generate(&e), + bstop_rate: 0_1000000, + status: 6, + max_positions: 2, + }; e.as_contract(&pool, || { - storage::set_queued_reserve_set( - &e, - &QueuedReserveInit { - new_config: metadata.clone(), - unlock_time: e.ledger().timestamp(), - }, - &asset_id_0, - ); - execute_cancel_queued_set_reserve(&e, &asset_id_0); - storage::get_queued_reserve_set(&e, &asset_id_0); + storage::set_pool_config(&e, &pool_config); + execute_queue_set_reserve(&e, &asset_id_0, &metadata); + let queued_res = storage::get_queued_reserve_set(&e, &asset_id_0); + let res_config_0 = queued_res.new_config; + assert_eq!(res_config_0.index, 0); + + // try and queue the same reserve + execute_queue_set_reserve(&e, &asset_id_0, &metadata); }); } + #[test] - fn test_execute_initialize_queued_reserve() { + #[should_panic(expected = "Error(Contract, #1202)")] + fn test_queue_set_reserve_validates_metadata() { let e = Env::default(); let pool = testutils::create_pool(&e); let bombadil = Address::generate(&e); - - let (asset_id_0, _) = testutils::create_token_contract(&e, &bombadil); + let (asset_id, _) = testutils::create_token_contract(&e, &bombadil); let metadata = ReserveConfig { index: 0, decimals: 7, c_factor: 0_7500000, - l_factor: 0_7500000, - util: 0_5000000, + l_factor: 1_7500000, + util: 1_0000000, max_util: 0_9500000, + r_base: 0_0100000, r_one: 0_0500000, r_two: 0_5000000, r_three: 1_5000000, reactivity: 100, }; + let pool_config = PoolConfig { + oracle: Address::generate(&e), + bstop_rate: 0_1000000, + status: 0, + max_positions: 2, + }; e.as_contract(&pool, || { - storage::set_queued_reserve_set( - &e, - &QueuedReserveInit { - new_config: metadata.clone(), - unlock_time: e.ledger().timestamp(), - }, - &asset_id_0, - ); - execute_set_queued_reserve(&e, &asset_id_0); - let res_config_0: ReserveConfig = storage::get_res_config(&e, &asset_id_0); - assert_eq!(res_config_0.decimals, metadata.decimals); - assert_eq!(res_config_0.c_factor, metadata.c_factor); - assert_eq!(res_config_0.l_factor, metadata.l_factor); - assert_eq!(res_config_0.util, metadata.util); - assert_eq!(res_config_0.max_util, metadata.max_util); - assert_eq!(res_config_0.r_one, metadata.r_one); - assert_eq!(res_config_0.r_two, metadata.r_two); - assert_eq!(res_config_0.r_three, metadata.r_three); - assert_eq!(res_config_0.reactivity, metadata.reactivity); - assert_eq!(res_config_0.index, 0); + storage::set_pool_config(&e, &pool_config); + execute_queue_set_reserve(&e, &asset_id, &metadata); }); } + #[test] - #[should_panic(expected = "Error(Contract, #1203)")] - fn test_execute_queued_initialize_reserve_requires_block_passed() { + fn test_execute_cancel_queued_reserve_initialization() { let e = Env::default(); let pool = testutils::create_pool(&e); let bombadil = Address::generate(&e); @@ -526,6 +558,7 @@ mod tests { l_factor: 0_7500000, util: 0_5000000, max_util: 0_9500000, + r_base: 0_0100000, r_one: 0_0500000, r_two: 0_5000000, r_three: 1_5000000, @@ -536,21 +569,24 @@ mod tests { &e, &QueuedReserveInit { new_config: metadata.clone(), - unlock_time: e.ledger().timestamp() + 1, + unlock_time: e.ledger().timestamp(), }, &asset_id_0, ); - execute_set_queued_reserve(&e, &asset_id_0); + execute_cancel_queued_set_reserve(&e, &asset_id_0); + let result = storage::has_queued_reserve_set(&e, &asset_id_0); + + assert!(!result); }); } + #[test] - fn test_initialize_reserve() { + fn test_execute_set_reserve_first_reserve() { let e = Env::default(); let pool = testutils::create_pool(&e); let bombadil = Address::generate(&e); let (asset_id_0, _) = testutils::create_token_contract(&e, &bombadil); - let (asset_id_1, _) = testutils::create_token_contract(&e, &bombadil); let metadata = ReserveConfig { index: 0, @@ -559,17 +595,23 @@ mod tests { l_factor: 0_7500000, util: 0_5000000, max_util: 0_9500000, + r_base: 0_0100000, r_one: 0_0500000, r_two: 0_5000000, r_three: 1_5000000, reactivity: 100, }; e.as_contract(&pool, || { - initialize_reserve(&e, &asset_id_0, &metadata); - - initialize_reserve(&e, &asset_id_1, &metadata); - let res_config_0 = storage::get_res_config(&e, &asset_id_0); - let res_config_1 = storage::get_res_config(&e, &asset_id_1); + storage::set_queued_reserve_set( + &e, + &QueuedReserveInit { + new_config: metadata.clone(), + unlock_time: e.ledger().timestamp(), + }, + &asset_id_0, + ); + execute_set_reserve(&e, &asset_id_0); + let res_config_0: ReserveConfig = storage::get_res_config(&e, &asset_id_0); assert_eq!(res_config_0.decimals, metadata.decimals); assert_eq!(res_config_0.c_factor, metadata.c_factor); assert_eq!(res_config_0.l_factor, metadata.l_factor); @@ -580,44 +622,46 @@ mod tests { assert_eq!(res_config_0.r_three, metadata.r_three); assert_eq!(res_config_0.reactivity, metadata.reactivity); assert_eq!(res_config_0.index, 0); - assert_eq!(res_config_1.index, 1); }); } #[test] - #[should_panic(expected = "Error(Contract, #1202)")] - fn test_queue_set_reserve_validates_metadata() { + #[should_panic(expected = "Error(Contract, #1203)")] + fn test_execute_set_reserve_requires_block_passed() { let e = Env::default(); let pool = testutils::create_pool(&e); let bombadil = Address::generate(&e); - let (asset_id, _) = testutils::create_token_contract(&e, &bombadil); + + let (asset_id_0, _) = testutils::create_token_contract(&e, &bombadil); let metadata = ReserveConfig { index: 0, decimals: 7, c_factor: 0_7500000, - l_factor: 1_7500000, - util: 1_0000000, + l_factor: 0_7500000, + util: 0_5000000, max_util: 0_9500000, + r_base: 0_0100000, r_one: 0_0500000, r_two: 0_5000000, r_three: 1_5000000, reactivity: 100, }; - let pool_config = PoolConfig { - oracle: Address::generate(&e), - bstop_rate: 0_1000000, - status: 0, - max_positions: 2, - }; e.as_contract(&pool, || { - storage::set_pool_config(&e, &pool_config); - execute_queue_set_reserve(&e, &asset_id, &metadata); + storage::set_queued_reserve_set( + &e, + &QueuedReserveInit { + new_config: metadata.clone(), + unlock_time: e.ledger().timestamp() + 1, + }, + &asset_id_0, + ); + execute_set_reserve(&e, &asset_id_0); }); } #[test] - fn test_execute_update_reserve() { + fn test_execute_set_reserve_update() { let e = Env::default(); e.mock_all_auths(); e.ledger().set(LedgerInfo { @@ -635,21 +679,16 @@ mod tests { let bombadil = Address::generate(&e); let (underlying, _) = testutils::create_token_contract(&e, &bombadil); - let (reserve_config, reserve_data) = testutils::default_reserve_meta(); + let (reserve_config, mut reserve_data) = testutils::default_reserve_meta(); + reserve_data.ir_mod = 1_001_000_000; testutils::create_reserve(&e, &pool, &underlying, &reserve_config, &reserve_data); - let new_metadata = ReserveConfig { - index: 99, - decimals: 7, - c_factor: 0_7500000, - l_factor: 0_7500000, - util: 0_7777777, - max_util: 0_9500000, - r_one: 0_0500000, - r_two: 0_5000000, - r_three: 1_5000000, - reactivity: 105, - }; + let mut new_metadata = reserve_config.clone(); + new_metadata.index = 123; + new_metadata.c_factor += 1; + new_metadata.l_factor += 1; + new_metadata.max_util += 1; + new_metadata.reactivity += 1; e.ledger().set(LedgerInfo { timestamp: 10000, @@ -671,7 +710,6 @@ mod tests { e.as_contract(&pool, || { storage::set_pool_config(&e, &pool_config); - let res_config_old = storage::get_res_config(&e, &underlying); storage::set_queued_reserve_set( &e, &QueuedReserveInit { @@ -680,30 +718,31 @@ mod tests { }, &underlying, ); - execute_set_queued_reserve(&e, &underlying); + execute_set_reserve(&e, &underlying); let res_config_updated = storage::get_res_config(&e, &underlying); assert_eq!(res_config_updated.decimals, new_metadata.decimals); assert_eq!(res_config_updated.c_factor, new_metadata.c_factor); assert_eq!(res_config_updated.l_factor, new_metadata.l_factor); assert_eq!(res_config_updated.util, new_metadata.util); assert_eq!(res_config_updated.max_util, new_metadata.max_util); + assert_eq!(res_config_updated.r_base, new_metadata.r_base); assert_eq!(res_config_updated.r_one, new_metadata.r_one); assert_eq!(res_config_updated.r_two, new_metadata.r_two); assert_eq!(res_config_updated.r_three, new_metadata.r_three); assert_eq!(res_config_updated.reactivity, new_metadata.reactivity); - assert_eq!(res_config_updated.index, res_config_old.index); + assert_eq!(res_config_updated.index, reserve_config.index); // validate interest was accrued let res_data = storage::get_res_data(&e, &underlying); assert!(res_data.d_rate > 1_000_000_000); assert!(res_data.backstop_credit > 0); assert_eq!(res_data.last_time, 10000); + assert!(res_data.ir_mod != 1_000_000_000); }); } #[test] - #[should_panic(expected = "Error(Contract, #1202)")] - fn test_execute_update_reserve_validates_decimals() { + fn test_execute_set_reserve_update_resets_ir_mod() { let e = Env::default(); e.mock_all_auths(); e.ledger().set(LedgerInfo { @@ -721,21 +760,23 @@ mod tests { let bombadil = Address::generate(&e); let (underlying, _) = testutils::create_token_contract(&e, &bombadil); - let (reserve_config, reserve_data) = testutils::default_reserve_meta(); + let (reserve_config, mut reserve_data) = testutils::default_reserve_meta(); + reserve_data.ir_mod = 1_100_000_000; testutils::create_reserve(&e, &pool, &underlying, &reserve_config, &reserve_data); - let new_metadata = ReserveConfig { - index: 99, - decimals: 8, - c_factor: 0_7500000, - l_factor: 0_7500000, - util: 1_0777777, - max_util: 0_9500000, - r_one: 0_0500000, - r_two: 0_5000000, - r_three: 1_5000000, - reactivity: 105, - }; + let mut new_metadata = reserve_config.clone(); + new_metadata.r_base += 1; + + e.ledger().set(LedgerInfo { + timestamp: 10000, + protocol_version: 20, + sequence_number: 100, + network_id: Default::default(), + base_reserve: 10, + min_temp_entry_ttl: 10, + min_persistent_entry_ttl: 10, + max_entry_ttl: 2000000, + }); let pool_config = PoolConfig { oracle: Address::generate(&e), @@ -754,12 +795,31 @@ mod tests { }, &underlying, ); - execute_set_queued_reserve(&e, &underlying); + execute_set_reserve(&e, &underlying); + let res_config_updated = storage::get_res_config(&e, &underlying); + assert_eq!(res_config_updated.decimals, new_metadata.decimals); + assert_eq!(res_config_updated.c_factor, new_metadata.c_factor); + assert_eq!(res_config_updated.l_factor, new_metadata.l_factor); + assert_eq!(res_config_updated.util, new_metadata.util); + assert_eq!(res_config_updated.max_util, new_metadata.max_util); + assert_eq!(res_config_updated.r_base, new_metadata.r_base); + assert_eq!(res_config_updated.r_one, new_metadata.r_one); + assert_eq!(res_config_updated.r_two, new_metadata.r_two); + assert_eq!(res_config_updated.r_three, new_metadata.r_three); + assert_eq!(res_config_updated.reactivity, new_metadata.reactivity); + assert_eq!(res_config_updated.index, reserve_config.index); + + let res_data = storage::get_res_data(&e, &underlying); + assert!(res_data.d_rate > 1_000_000_000); + assert!(res_data.backstop_credit > 0); + assert_eq!(res_data.last_time, 10000); + assert_eq!(res_data.ir_mod, 1_000_000_000); }); } #[test] - fn test_execute_update_reserve_resets_ir_mod() { + #[should_panic(expected = "Error(Contract, #1202)")] + fn test_execute_set_reserve_validates_decimals_stay_same() { let e = Env::default(); e.mock_all_auths(); e.ledger().set(LedgerInfo { @@ -782,13 +842,14 @@ mod tests { let new_metadata = ReserveConfig { index: 99, - decimals: 7, + decimals: 8, // started at 18 c_factor: 0_7500000, l_factor: 0_7500000, - util: 1_0777777, + util: 0_0777777, max_util: 0_9500000, + r_base: 0_0100000, r_one: 0_0500000, - r_two: 0_7500000, + r_two: 0_5000000, r_three: 1_5000000, reactivity: 105, }; @@ -810,9 +871,49 @@ mod tests { }, &underlying, ); - execute_set_queued_reserve(&e, &underlying); - let res_data = storage::get_res_data(&e, &underlying); - assert_eq!(res_data.ir_mod, 1_000_000_000); + execute_set_reserve(&e, &underlying); + }); + } + + #[test] + fn test_initialize_reserve_sets_index() { + let e = Env::default(); + let pool = testutils::create_pool(&e); + let bombadil = Address::generate(&e); + + let (asset_id_0, _) = testutils::create_token_contract(&e, &bombadil); + let (asset_id_1, _) = testutils::create_token_contract(&e, &bombadil); + + let metadata = ReserveConfig { + index: 0, + decimals: 7, + c_factor: 0_7500000, + l_factor: 0_7500000, + util: 0_5000000, + max_util: 0_9500000, + r_base: 0_0100000, + r_one: 0_0500000, + r_two: 0_5000000, + r_three: 1_5000000, + reactivity: 100, + }; + e.as_contract(&pool, || { + initialize_reserve(&e, &asset_id_0, &metadata); + + initialize_reserve(&e, &asset_id_1, &metadata); + let res_config_0 = storage::get_res_config(&e, &asset_id_0); + let res_config_1 = storage::get_res_config(&e, &asset_id_1); + assert_eq!(res_config_0.decimals, metadata.decimals); + assert_eq!(res_config_0.c_factor, metadata.c_factor); + assert_eq!(res_config_0.l_factor, metadata.l_factor); + assert_eq!(res_config_0.util, metadata.util); + assert_eq!(res_config_0.max_util, metadata.max_util); + assert_eq!(res_config_0.r_one, metadata.r_one); + assert_eq!(res_config_0.r_two, metadata.r_two); + assert_eq!(res_config_0.r_three, metadata.r_three); + assert_eq!(res_config_0.reactivity, metadata.reactivity); + assert_eq!(res_config_0.index, 0); + assert_eq!(res_config_1.index, 1); }); } @@ -828,6 +929,7 @@ mod tests { l_factor: 0_7500000, util: 0_5000000, max_util: 0_9500000, + r_base: 0_0001000, r_one: 0_0500000, r_two: 0_5000000, r_three: 1_5000000, @@ -850,6 +952,7 @@ mod tests { l_factor: 0_7500000, util: 0_5000000, max_util: 0_9500000, + r_base: 0_0001000, r_one: 0_0500000, r_two: 0_5000000, r_three: 1_5000000, @@ -870,6 +973,7 @@ mod tests { l_factor: 0_7500000, util: 0_5000000, max_util: 0_9500000, + r_base: 0_0001000, r_one: 0_0500000, r_two: 0_5000000, r_three: 1_5000000, @@ -890,6 +994,7 @@ mod tests { l_factor: 1_0000001, util: 0_5000000, max_util: 0_9500000, + r_base: 0_0001000, r_one: 0_0500000, r_two: 0_5000000, r_three: 1_5000000, @@ -910,6 +1015,7 @@ mod tests { l_factor: 0_7500000, util: 1_0000000, max_util: 0_9500000, + r_base: 0_0001000, r_one: 0_0500000, r_two: 0_5000000, r_three: 1_5000000, @@ -930,6 +1036,49 @@ mod tests { l_factor: 0_7500000, util: 0_5000000, max_util: 1_0000001, + r_base: 0_0001000, + r_one: 0_0500000, + r_two: 0_5000000, + r_three: 1_5000000, + reactivity: 100, + }; + require_valid_reserve_metadata(&e, &metadata); + } + + #[test] + #[should_panic(expected = "Error(Contract, #1202)")] + fn test_validate_reserve_metadata_validates_r_base_too_high() { + let e = Env::default(); + + let metadata = ReserveConfig { + index: 0, + decimals: 18, + c_factor: 0_7500000, + l_factor: 0_7500000, + util: 0_5000000, + max_util: 0_9500000, + r_base: 1_0000000, + r_one: 0_0500000, + r_two: 0_5000000, + r_three: 1_5000000, + reactivity: 100, + }; + require_valid_reserve_metadata(&e, &metadata); + } + + #[test] + #[should_panic(expected = "Error(Contract, #1202)")] + fn test_validate_reserve_metadata_validates_r_base_too_low() { + let e = Env::default(); + + let metadata = ReserveConfig { + index: 0, + decimals: 18, + c_factor: 0_7500000, + l_factor: 0_7500000, + util: 0_5000000, + max_util: 0_9500000, + r_base: 0_0000999, r_one: 0_0500000, r_two: 0_5000000, r_three: 1_5000000, @@ -950,6 +1099,7 @@ mod tests { l_factor: 0_7500000, util: 0_5000000, max_util: 0_9500000, + r_base: 0_0000100, r_one: 0_5000001, r_two: 0_5000000, r_three: 1_5000000, @@ -970,6 +1120,7 @@ mod tests { l_factor: 0_7500000, util: 0_5000000, max_util: 0_9500000, + r_base: 0_0100000, r_one: 0_0500000, r_two: 0_5000000, r_three: 1_5000000, diff --git a/pool/src/pool/health_factor.rs b/pool/src/pool/health_factor.rs index b8f58e7f..e90fc504 100644 --- a/pool/src/pool/health_factor.rs +++ b/pool/src/pool/health_factor.rs @@ -1,7 +1,7 @@ use soroban_fixed_point_math::FixedPoint; -use soroban_sdk::{panic_with_error, unwrap::UnwrapOptimized, Env}; +use soroban_sdk::{unwrap::UnwrapOptimized, Env}; -use crate::{constants::SCALAR_7, errors::PoolError, storage}; +use crate::{constants::SCALAR_7, storage}; use super::{pool::Pool, Positions}; @@ -59,10 +59,10 @@ impl PositionData { // append users effective liability to liability_base let asset_liability = reserve.to_effective_asset_from_d_token(d_token_balance); liability_base += asset_to_base - .fixed_mul_floor(asset_liability, reserve.scalar) + .fixed_mul_ceil(asset_liability, reserve.scalar) .unwrap_optimized(); liability_raw += asset_to_base - .fixed_mul_floor( + .fixed_mul_ceil( reserve.to_asset_from_d_token(d_token_balance), reserve.scalar, ) @@ -84,24 +84,37 @@ impl PositionData { /// Return the health factor as a ratio pub fn as_health_factor(&self) -> i128 { self.collateral_base - .fixed_div_ceil(self.liability_base, self.scalar) + .fixed_div_floor(self.liability_base, self.scalar) .unwrap_optimized() } - /// Check if the position data meets the minimum health factor, panic if not - pub fn require_healthy(&self, e: &Env) { + // Check if the position data is over a maximum health factor + // Note: max must be 7 decimals + pub fn is_hf_over(&self, max: i128) -> bool { if self.liability_base == 0 { - return; + return true; + } + let min_health_factor = self.scalar.fixed_mul_ceil(max, SCALAR_7).unwrap_optimized(); + if self.as_health_factor() > min_health_factor { + return true; } + false + } - // force user to have slightly more collateral than liabilities to prevent rounding errors + /// Check if the position data is under a minimum health factor + /// Note: min must be 7 decimals + pub fn is_hf_under(&self, min: i128) -> bool { + if self.liability_base == 0 { + return false; + } let min_health_factor = self .scalar - .fixed_mul_floor(1_0000100, SCALAR_7) + .fixed_mul_floor(min, SCALAR_7) .unwrap_optimized(); if self.as_health_factor() < min_health_factor { - panic_with_error!(e, PoolError::InvalidHf); + return true; } + false } } @@ -193,17 +206,30 @@ mod tests { let mut pool = Pool::load(&e); let position_data = PositionData::calculate_from_positions(&e, &mut pool, &positions); assert_eq!(position_data.collateral_base, 262_7985925); - assert_eq!(position_data.liability_base, 185_2368827); + assert_eq!(position_data.liability_base, 185_2368828); assert_eq!(position_data.collateral_raw, 350_3984567); - assert_eq!(position_data.liability_raw, 148_0895061); + assert_eq!(position_data.liability_raw, 148_0895062); assert_eq!(position_data.scalar, SCALAR_7); }); } #[test] - fn test_require_healthy() { - let e = Env::default(); + fn test_as_health_factor_rounds_floor() { + let position_data = PositionData { + collateral_base: 9_1234567, + collateral_raw: 0, + liability_base: 9_1000000, + liability_raw: 0, + scalar: 1_0000000, + }; + // actual: 1.002577659 + let result = position_data.as_health_factor(); + assert_eq!(result, 1_0025776); + } + + #[test] + fn test_is_hf_under() { let position_data = PositionData { collateral_base: 9_1234567, collateral_raw: 12_0000000, @@ -212,15 +238,28 @@ mod tests { scalar: 1_0000000, }; - position_data.require_healthy(&e); + let result = position_data.is_hf_under(1_0000100); // no panic - assert!(true); + assert_eq!(result, false); } #[test] - fn test_require_healthy_no_liabilites() { - let e = Env::default(); + fn test_is_hf_under_odd_scalar() { + let position_data = PositionData { + collateral_base: 9_12345, + collateral_raw: 12_00000, + liability_base: 9_12333, + liability_raw: 10_00000, + scalar: 1_00000, + }; + + let result = position_data.is_hf_under(1_0000100); + // no panic + assert_eq!(result, false); + } + #[test] + fn test_is_hf_under_no_liabilites() { let position_data = PositionData { collateral_base: 9_1234567, collateral_raw: 12_0000000, @@ -229,16 +268,13 @@ mod tests { scalar: 1_0000000, }; - position_data.require_healthy(&e); + let result = position_data.is_hf_under(1_0000100); // no panic - assert!(true); + assert_eq!(result, false); } #[test] - #[should_panic(expected = "Error(Contract, #1205)")] - fn test_require_healthy_panics() { - let e = Env::default(); - + fn test_is_hf_under_true() { let position_data = PositionData { collateral_base: 9_1234567, collateral_raw: 12_0000000, @@ -247,8 +283,67 @@ mod tests { scalar: 1_0000000, }; - position_data.require_healthy(&e); + let result = position_data.is_hf_under(1_0000100); + // panic + assert!(result); + } + + #[test] + fn test_is_hf_over() { + let position_data = PositionData { + collateral_base: 9_1234567, + collateral_raw: 12_0000000, + liability_base: 9_1233333, + liability_raw: 10_0000000, + scalar: 1_0000000, + }; + + let result = position_data.is_hf_over(1_1000000); // no panic - assert!(true); + assert_eq!(result, false); + } + + #[test] + fn test_is_hf_over_odd_scalar() { + let position_data = PositionData { + collateral_base: 9_1234567_000, + collateral_raw: 12_0000000_000, + liability_base: 9_1233333_000, + liability_raw: 10_0000000_000, + scalar: 1_0000000_000, + }; + + let result = position_data.is_hf_over(1_1000000); + // no panic + assert_eq!(result, false); + } + + #[test] + fn test_is_hf_over_no_liabilites() { + let position_data = PositionData { + collateral_base: 9_1234567, + collateral_raw: 12_0000000, + liability_base: 0, + liability_raw: 0, + scalar: 1_0000000, + }; + + let result = position_data.is_hf_over(1_0000100); + // panic + assert!(result); + } + #[test] + fn test_is_hf_over_true() { + let position_data = PositionData { + collateral_base: 19_1234567, + collateral_raw: 22_0000000, + liability_base: 9_1234567, + liability_raw: 10_0000000, + scalar: 1_0000000, + }; + + let result = position_data.is_hf_over(1_0000100); + // panic + assert!(result); } } diff --git a/pool/src/pool/interest.rs b/pool/src/pool/interest.rs index e770c30b..186144f9 100644 --- a/pool/src/pool/interest.rs +++ b/pool/src/pool/interest.rs @@ -35,7 +35,7 @@ pub fn calc_accrual( let base_rate = util_scalar .fixed_mul_ceil(i128(config.r_one), SCALAR_7) .unwrap_optimized() - + 0_0100000; + + i128(config.r_base); cur_ir = base_rate .fixed_mul_ceil(ir_mod, SCALAR_9) @@ -48,7 +48,7 @@ pub fn calc_accrual( .fixed_mul_ceil(i128(config.r_two), SCALAR_7) .unwrap_optimized() + i128(config.r_one) - + 0_0100000; + + i128(config.r_base); cur_ir = base_rate .fixed_mul_ceil(ir_mod, SCALAR_9) @@ -62,7 +62,7 @@ pub fn calc_accrual( .unwrap_optimized(); let intersection = ir_mod - .fixed_mul_ceil(i128(config.r_two + config.r_one + 0_0100000), SCALAR_9) + .fixed_mul_ceil(i128(config.r_two + config.r_one + config.r_base), SCALAR_9) .unwrap_optimized(); cur_ir = extra_rate + intersection; } @@ -130,6 +130,7 @@ mod tests { l_factor: 0_7500000, util: 0_7500000, max_util: 0_9500000, + r_base: 0_0100000, r_one: 0_0500000, r_two: 0_5000000, r_three: 1_5000000, @@ -165,6 +166,7 @@ mod tests { l_factor: 0_7500000, util: 0_7500000, max_util: 0_9500000, + r_base: 0_0100000, r_one: 0_0500000, r_two: 0_5000000, r_three: 1_5000000, @@ -200,6 +202,7 @@ mod tests { l_factor: 0_7500000, util: 0_7500000, max_util: 0_9500000, + r_base: 0_0100000, r_one: 0_0500000, r_two: 0_5000000, r_three: 1_5000000, @@ -235,6 +238,7 @@ mod tests { l_factor: 0_7500000, util: 0_7500000, max_util: 0_9500000, + r_base: 0_0100000, r_one: 0_0500000, r_two: 0_5000000, r_three: 1_5000000, @@ -269,6 +273,7 @@ mod tests { l_factor: 0_7500000, util: 0_7500000, max_util: 0_9500000, + r_base: 0_0100000, r_one: 0_0500000, r_two: 0_5000000, r_three: 1_5000000, @@ -303,6 +308,7 @@ mod tests { l_factor: 0_7500000, util: 0_7500000, max_util: 0_9500000, + r_base: 0_0100000, r_one: 0_0500000, r_two: 0_5000000, r_three: 1_5000000, @@ -327,4 +333,49 @@ mod tests { assert_eq!(accrual, 1_000_000_001); assert_eq!(ir_mod, 0_100_000_000); } + + #[test] + fn test_calc_accrual_fixed_rate() { + let e = Env::default(); + + let reserve_config = ReserveConfig { + decimals: 7, + c_factor: 0_7500000, + l_factor: 0_7500000, + util: 0_7500000, + max_util: 0_9500000, + r_base: 0_2500000, + r_one: 0, + r_two: 0, + r_three: 0, + reactivity: 0_0000020, + index: 0, + }; + let ir_mod: i128 = 1_000_000_000; + + e.ledger().set(LedgerInfo { + timestamp: 500, + protocol_version: 20, + sequence_number: 100, + network_id: Default::default(), + base_reserve: 10, + min_temp_entry_ttl: 10, + min_persistent_entry_ttl: 10, + max_entry_ttl: 2000000, + }); + + let (accrual_0, ir_mod_0) = calc_accrual(&e, &reserve_config, 0, ir_mod, 0); + let (accrual_1, ir_mod_1) = calc_accrual(&e, &reserve_config, 0_6565656, ir_mod, 0); + let (accrual_2, ir_mod_2) = calc_accrual(&e, &reserve_config, 0_7565656, ir_mod, 0); + let (accrual_3, ir_mod_3) = calc_accrual(&e, &reserve_config, 0_9565656, ir_mod, 0); + + assert_eq!(accrual_0, 1_000_003_964); + assert_eq!(ir_mod_0, 0_999_250_000); + assert_eq!(accrual_1, 1_000_003_964); + assert_eq!(ir_mod_1, 0_999_906_566); + assert_eq!(accrual_2, 1_000_003_964); + assert_eq!(ir_mod_2, 1_000_006_565); + assert_eq!(accrual_3, 1_000_003_964); + assert_eq!(ir_mod_3, 1_000_206_565); + } } diff --git a/pool/src/pool/mod.rs b/pool/src/pool/mod.rs index b07cbe64..6e6da98a 100644 --- a/pool/src/pool/mod.rs +++ b/pool/src/pool/mod.rs @@ -7,7 +7,7 @@ pub use bad_debt::transfer_bad_debt_to_backstop; mod config; pub use config::{ execute_cancel_queued_set_reserve, execute_initialize, execute_queue_set_reserve, - execute_set_queued_reserve, execute_update_pool, + execute_set_reserve, execute_update_pool, }; mod health_factor; diff --git a/pool/src/pool/reserve.rs b/pool/src/pool/reserve.rs index 7fa915a6..f24512eb 100644 --- a/pool/src/pool/reserve.rs +++ b/pool/src/pool/reserve.rs @@ -1,5 +1,4 @@ use cast::i128; -use sep_41_token::TokenClient; use soroban_fixed_point_math::FixedPoint; use soroban_sdk::{contracttype, panic_with_error, unwrap::UnwrapOptimized, Address, Env}; @@ -71,6 +70,12 @@ impl Reserve { } let cur_util = reserve.utilization(); + if cur_util == 0 { + // if there are no assets borrowed, we don't need to update the reserve + reserve.last_time = e.ledger().timestamp(); + return reserve; + } + let (loan_accrual, new_ir_mod) = calc_accrual( e, &reserve_config, @@ -80,28 +85,29 @@ impl Reserve { ); reserve.ir_mod = new_ir_mod; + let pre_update_supply = reserve.total_supply(); + let pre_update_liabilities = reserve.total_liabilities(); + reserve.d_rate = loan_accrual .fixed_mul_ceil(reserve.d_rate, SCALAR_9) .unwrap_optimized(); - // TODO: Is it safe to calculate b_rate from accrual? If any unexpected token loss occurs - // the transfer rate will become unrecoverable. - let pre_update_supply = reserve.total_supply(); - let token_bal = TokenClient::new(e, asset).balance(&e.current_contract_address()); - - // credit the backstop underlying from the accrued interest based on the backstop rate - let accrued_supply = - reserve.total_liabilities() + token_bal - reserve.backstop_credit - pre_update_supply; - if pool_config.bstop_rate > 0 && accrued_supply > 0 { - let new_backstop_credit = accrued_supply - .fixed_mul_floor(i128(pool_config.bstop_rate), SCALAR_7) + let accrued_interest = reserve.total_liabilities() - pre_update_liabilities; + if accrued_interest > 0 { + // credit the backstop underlying from the accrued interest based on the backstop rate + // update the accrued interest to reflect the amount the pool accrued + let mut new_backstop_credit: i128 = 0; + if pool_config.bstop_rate > 0 { + new_backstop_credit = accrued_interest + .fixed_mul_floor(i128(pool_config.bstop_rate), SCALAR_7) + .unwrap_optimized(); + reserve.backstop_credit += new_backstop_credit; + } + reserve.b_rate = (pre_update_supply + accrued_interest - new_backstop_credit) + .fixed_div_floor(reserve.b_supply, SCALAR_9) .unwrap_optimized(); - reserve.backstop_credit += new_backstop_credit; } - reserve.b_rate = (reserve.total_liabilities() + token_bal - reserve.backstop_credit) - .fixed_div_floor(reserve.b_supply, SCALAR_9) - .unwrap_optimized(); reserve.last_time = e.ledger().timestamp(); reserve } @@ -123,7 +129,7 @@ impl Reserve { /// Fetch the current utilization rate for the reserve normalized to 7 decimals pub fn utilization(&self) -> i128 { self.total_liabilities() - .fixed_div_floor(self.total_supply(), SCALAR_7) + .fixed_div_ceil(self.total_supply(), SCALAR_7) .unwrap_optimized() } @@ -275,13 +281,13 @@ mod tests { storage::set_pool_config(&e, &pool_config); let reserve = Reserve::load(&e, &pool_config, &underlying); - // (accrual: 1_002_957_369, util: .7864352) - assert_eq!(reserve.d_rate, 1_349_657_792); - assert_eq!(reserve.b_rate, 1_125_547_121); - assert_eq!(reserve.ir_mod, 1_044_981_440); + // (accrual: 1_002_957_369, util: .7864353) + assert_eq!(reserve.d_rate, 1_349_657_800); + assert_eq!(reserve.b_rate, 1_125_547_124); + assert_eq!(reserve.ir_mod, 1_044_981_563); assert_eq!(reserve.d_supply, 65_0000000); assert_eq!(reserve.b_supply, 99_0000000); - assert_eq!(reserve.backstop_credit, 0_0517357); + assert_eq!(reserve.backstop_credit, 0_0517358); assert_eq!(reserve.last_time, 617280); }); } @@ -335,6 +341,52 @@ mod tests { }); } + #[test] + fn test_load_reserve_zero_util() { + let e = Env::default(); + e.mock_all_auths(); + + e.ledger().set(LedgerInfo { + timestamp: 123456 * 5, + protocol_version: 20, + sequence_number: 123456, + network_id: Default::default(), + base_reserve: 10, + min_temp_entry_ttl: 10, + min_persistent_entry_ttl: 10, + max_entry_ttl: 2000000, + }); + + let bombadil = Address::generate(&e); + let pool = testutils::create_pool(&e); + let oracle = Address::generate(&e); + + let (underlying, _) = testutils::create_token_contract(&e, &bombadil); + let (reserve_config, mut reserve_data) = testutils::default_reserve_meta(); + reserve_data.d_rate = 0; + reserve_data.d_supply = 0; + testutils::create_reserve(&e, &pool, &underlying, &reserve_config, &reserve_data); + + let pool_config = PoolConfig { + oracle, + bstop_rate: 0_2000000, + status: 0, + max_positions: 4, + }; + e.as_contract(&pool, || { + storage::set_pool_config(&e, &pool_config); + let reserve = Reserve::load(&e, &pool_config, &underlying); + + assert_eq!(reserve.d_rate, 0); + assert_eq!(reserve.b_rate, reserve_data.b_rate); + assert_eq!(reserve.ir_mod, reserve_data.ir_mod); + assert_eq!(reserve.d_supply, 0); + assert_eq!(reserve.b_supply, reserve_data.b_supply); + assert_eq!(reserve.backstop_credit, 0); + assert_eq!(reserve.last_time, 617280); + }); + } + #[test] fn test_load_reserve_zero_bstop_rate() { let e = Env::default(); @@ -373,10 +425,10 @@ mod tests { storage::set_pool_config(&e, &pool_config); let reserve = Reserve::load(&e, &pool_config, &underlying); - // (accrual: 1_002_957_369, util: .7864352) - assert_eq!(reserve.d_rate, 1_349_657_792); - assert_eq!(reserve.b_rate, 1_126_069_704); - assert_eq!(reserve.ir_mod, 1_044_981_440); + // (accrual: 1_002_957_369, util: .7864353) + assert_eq!(reserve.d_rate, 1_349_657_800); + assert_eq!(reserve.b_rate, 1_126_069_708); + assert_eq!(reserve.ir_mod, 1_044_981_563); assert_eq!(reserve.d_supply, 65_0000000); assert_eq!(reserve.b_supply, 99_0000000); assert_eq!(reserve.backstop_credit, 0); @@ -425,13 +477,13 @@ mod tests { let reserve_data = storage::get_res_data(&e, &underlying); - // (accrual: 1_002_957_369, util: .7864352) - assert_eq!(reserve_data.d_rate, 1_349_657_792); - assert_eq!(reserve_data.b_rate, 1_125_547_121); - assert_eq!(reserve_data.ir_mod, 1_044_981_440); + // (accrual: 1_002_957_369, util: .7864353) + assert_eq!(reserve_data.d_rate, 1_349_657_800); + assert_eq!(reserve_data.b_rate, 1_125_547_124); + assert_eq!(reserve_data.ir_mod, 1_044_981_563); assert_eq!(reserve_data.d_supply, 65_0000000); assert_eq!(reserve_data.b_supply, 99_0000000); - assert_eq!(reserve_data.backstop_credit, 0_0517357); + assert_eq!(reserve_data.backstop_credit, 0_0517358); assert_eq!(reserve_data.last_time, 617280); }); } @@ -448,7 +500,7 @@ mod tests { let result = reserve.utilization(); - assert_eq!(result, 0_7864352); + assert_eq!(result, 0_7864353); } #[test] diff --git a/pool/src/pool/status.rs b/pool/src/pool/status.rs index 0a3b040b..1b939e27 100644 --- a/pool/src/pool/status.rs +++ b/pool/src/pool/status.rs @@ -133,8 +133,9 @@ pub fn calc_pool_backstop_threshold(pool_backstop_data: &PoolBackstopData) -> i1 // The calculation is: // - Threshold % = (bal_blnd^4 * bal_usdc) / PC^5 such that PC is 200k let threshold_pc = 320_000_000_000_000_000_000_000_000i128; // 3.2e26 (200k^5) - // floor balances to nearest full unit and calculate saturated pool product constant - // and scale to SCALAR_7 to get final division result in SCALAR_7 points + + // floor balances to nearest full unit and calculate saturated pool product constant + // and scale to SCALAR_7 to get final division result in SCALAR_7 points let bal_blnd = pool_backstop_data.blnd / SCALAR_7; let bal_usdc = pool_backstop_data.usdc / SCALAR_7; let saturating_pool_pc = bal_blnd diff --git a/pool/src/pool/submit.rs b/pool/src/pool/submit.rs index 684e0588..455792de 100644 --- a/pool/src/pool/submit.rs +++ b/pool/src/pool/submit.rs @@ -38,10 +38,13 @@ pub fn execute_submit( let (actions, new_from_state, check_health) = build_actions_from_request(e, &mut pool, from, requests); - if check_health { - // panics if the new positions set does not meet the health factor requirement - PositionData::calculate_from_positions(e, &mut pool, &new_from_state.positions) - .require_healthy(e); + // panics if the new positions set does not meet the health factor requirement + // min is 1.0000100 to prevent rounding errors + if check_health + && PositionData::calculate_from_positions(e, &mut pool, &new_from_state.positions) + .is_hf_under(1_0000100) + { + panic_with_error!(e, PoolError::InvalidHf); } // transfer tokens from sender to pool diff --git a/pool/src/pool/user.rs b/pool/src/pool/user.rs index a89ccaf9..3e4298c5 100644 --- a/pool/src/pool/user.rs +++ b/pool/src/pool/user.rs @@ -1,6 +1,6 @@ -use soroban_sdk::{contracttype, Address, Env, Map}; +use soroban_sdk::{contracttype, panic_with_error, Address, Env, Map}; -use crate::{emissions, storage, validator::require_nonnegative}; +use crate::{emissions, storage, validator::require_nonnegative, PoolError}; use super::{Pool, Reserve}; @@ -61,6 +61,9 @@ impl User { /// Add liabilities to the position expressed in debtTokens. Accrues emissions /// against the balance if necessary and updates the reserve's d_supply. pub fn add_liabilities(&mut self, e: &Env, reserve: &mut Reserve, amount: i128) { + if amount == 0 { + panic_with_error!(e, PoolError::InvalidDTokenMintAmount) + } let balance = self.get_liabilities(reserve.index); self.update_d_emissions(e, reserve, balance); self.positions @@ -72,6 +75,9 @@ impl User { /// Remove liabilities from the position expressed in debtTokens. Accrues emissions /// against the balance if necessary and updates the reserve's d_supply. pub fn remove_liabilities(&mut self, e: &Env, reserve: &mut Reserve, amount: i128) { + if amount == 0 { + panic_with_error!(e, PoolError::InvalidDTokenBurnAmount) + } let balance = self.get_liabilities(reserve.index); self.update_d_emissions(e, reserve, balance); let new_balance = balance - amount; @@ -92,6 +98,9 @@ impl User { /// Add collateral to the position expressed in blendTokens. Accrues emissions /// against the balance if necessary and updates the reserve's b_supply. pub fn add_collateral(&mut self, e: &Env, reserve: &mut Reserve, amount: i128) { + if amount == 0 { + panic_with_error!(e, PoolError::InvalidBTokenMintAmount) + } let balance = self.get_collateral(reserve.index); self.update_b_emissions(e, reserve, self.get_total_supply(reserve.index)); self.positions @@ -103,6 +112,9 @@ impl User { /// Remove collateral from the position expressed in blendTokens. Accrues emissions /// against the balance if necessary and updates the reserve's d_supply. pub fn remove_collateral(&mut self, e: &Env, reserve: &mut Reserve, amount: i128) { + if amount == 0 { + panic_with_error!(e, PoolError::InvalidBTokenBurnAmount) + } let balance = self.get_collateral(reserve.index); self.update_b_emissions(e, reserve, self.get_total_supply(reserve.index)); let new_balance = balance - amount; @@ -123,6 +135,9 @@ impl User { /// Add supply to the position expressed in blendTokens. Accrues emissions /// against the balance if necessary and updates the reserve's b_supply. pub fn add_supply(&mut self, e: &Env, reserve: &mut Reserve, amount: i128) { + if amount == 0 { + panic_with_error!(e, PoolError::InvalidBTokenMintAmount) + } let balance = self.get_supply(reserve.index); self.update_b_emissions(e, reserve, self.get_total_supply(reserve.index)); self.positions.supply.set(reserve.index, balance + amount); @@ -132,6 +147,9 @@ impl User { /// Remove supply from the position expressed in blendTokens. Accrues emissions /// against the balance if necessary and updates the reserve's b_supply. pub fn remove_supply(&mut self, e: &Env, reserve: &mut Reserve, amount: i128) { + if amount == 0 { + panic_with_error!(e, PoolError::InvalidBTokenBurnAmount) + } let balance = self.get_supply(reserve.index); self.update_b_emissions(e, reserve, self.get_total_supply(reserve.index)); let new_balance = balance - amount; @@ -290,6 +308,26 @@ mod tests { }); } + #[test] + #[should_panic(expected = "Error(Contract, #1218)")] + fn test_add_liabilities_zero_mint() { + let e = Env::default(); + let samwise = Address::generate(&e); + let pool = testutils::create_pool(&e); + + let mut reserve_0 = testutils::default_reserve(&e); + + let mut user = User { + address: samwise.clone(), + positions: Positions::env_default(&e), + }; + e.as_contract(&pool, || { + assert_eq!(user.get_liabilities(0), 0); + + user.add_liabilities(&e, &mut reserve_0, 0); + }); + } + #[test] fn test_add_liabilities_accrues_emissions() { let e = Env::default(); @@ -359,6 +397,29 @@ mod tests { }); } + #[test] + #[should_panic(expected = "Error(Contract, #1219)")] + fn test_remove_liabilities_zero_burn() { + let e = Env::default(); + let samwise = Address::generate(&e); + let pool = testutils::create_pool(&e); + + let mut reserve_0 = testutils::default_reserve(&e); + + let mut user = User { + address: samwise.clone(), + positions: Positions::env_default(&e), + }; + e.as_contract(&pool, || { + assert_eq!(user.get_liabilities(0), 0); + + user.add_liabilities(&e, &mut reserve_0, 123); + assert_eq!(user.get_liabilities(0), 123); + + user.remove_liabilities(&e, &mut reserve_0, 0); + }); + } + #[test] fn test_remove_liabilities_accrues_emissions() { let e = Env::default(); @@ -486,6 +547,26 @@ mod tests { }); } + #[test] + #[should_panic(expected = "Error(Contract, #1216)")] + fn test_add_collateral_zero_mint() { + let e = Env::default(); + let samwise = Address::generate(&e); + let pool = testutils::create_pool(&e); + + let mut reserve_0 = testutils::default_reserve(&e); + + let mut user = User { + address: samwise.clone(), + positions: Positions::env_default(&e), + }; + e.as_contract(&pool, || { + assert_eq!(user.get_collateral(0), 0); + + user.add_collateral(&e, &mut reserve_0, 0); + }); + } + #[test] fn test_add_collateral_accrues_emissions() { let e = Env::default(); @@ -554,6 +635,29 @@ mod tests { }); } + #[test] + #[should_panic(expected = "Error(Contract, #1217)")] + fn test_remove_collateral_zero_burn() { + let e = Env::default(); + let samwise = Address::generate(&e); + let pool = testutils::create_pool(&e); + + let mut reserve_0 = testutils::default_reserve(&e); + + let mut user = User { + address: samwise.clone(), + positions: Positions::env_default(&e), + }; + e.as_contract(&pool, || { + assert_eq!(user.get_collateral(0), 0); + + user.add_collateral(&e, &mut reserve_0, 123); + assert_eq!(user.get_collateral(0), 123); + + user.remove_collateral(&e, &mut reserve_0, 0); + }); + } + #[test] fn test_remove_collateral_accrues_emissions() { let e = Env::default(); @@ -683,6 +787,26 @@ mod tests { }); } + #[test] + #[should_panic(expected = "Error(Contract, #1216)")] + fn test_add_supply_zero_mint() { + let e = Env::default(); + let samwise = Address::generate(&e); + let pool = testutils::create_pool(&e); + + let mut reserve_0 = testutils::default_reserve(&e); + + let mut user = User { + address: samwise.clone(), + positions: Positions::env_default(&e), + }; + e.as_contract(&pool, || { + assert_eq!(user.get_supply(0), 0); + + user.add_supply(&e, &mut reserve_0, 0); + }); + } + #[test] fn test_add_supply_accrues_emissions() { let e = Env::default(); @@ -751,6 +875,29 @@ mod tests { }); } + #[test] + #[should_panic(expected = "Error(Contract, #1217)")] + fn test_remove_supply_zero_burn() { + let e = Env::default(); + let samwise = Address::generate(&e); + let pool = testutils::create_pool(&e); + + let mut reserve_0 = testutils::default_reserve(&e); + + let mut user = User { + address: samwise.clone(), + positions: Positions::env_default(&e), + }; + e.as_contract(&pool, || { + assert_eq!(user.get_supply(0), 0); + + user.add_supply(&e, &mut reserve_0, 123); + assert_eq!(user.get_supply(0), 123); + + user.remove_supply(&e, &mut reserve_0, 0); + }); + } + #[test] fn test_remove_supply_accrues_emissions() { let e = Env::default(); diff --git a/pool/src/storage.rs b/pool/src/storage.rs index 915e1e37..ae5d4515 100644 --- a/pool/src/storage.rs +++ b/pool/src/storage.rs @@ -41,9 +41,10 @@ pub struct ReserveConfig { pub l_factor: u32, // the liability factor for the reserve scaled expressed in 7 decimals pub util: u32, // the target utilization rate scaled expressed in 7 decimals pub max_util: u32, // the maximum allowed utilization rate scaled expressed in 7 decimals - pub r_one: u32, // the R1 value in the interest rate formula scaled expressed in 7 decimals - pub r_two: u32, // the R2 value in the interest rate formula scaled expressed in 7 decimals - pub r_three: u32, // the R3 value in the interest rate formula scaled expressed in 7 decimals + pub r_base: u32, // the R0 value (base rate) in the interest rate formula scaled expressed in 7 decimals + pub r_one: u32, // the R1 value in the interest rate formula scaled expressed in 7 decimals + pub r_two: u32, // the R2 value in the interest rate formula scaled expressed in 7 decimals + pub r_three: u32, // the R3 value in the interest rate formula scaled expressed in 7 decimals pub reactivity: u32, // the reactivity constant for the reserve scaled expressed in 7 decimals } @@ -153,10 +154,10 @@ pub fn extend_instance(e: &Env) { } /// Fetch an entry in persistent storage that has a default value if it doesn't exist -fn get_persistent_default, V: TryFromVal>( +fn get_persistent_default, V: TryFromVal, F: FnOnce() -> V>( e: &Env, key: &K, - default: V, + default: F, bump_threshold: u32, bump_amount: u32, ) -> V { @@ -166,7 +167,7 @@ fn get_persistent_default, V: TryFromVal>( .extend_ttl(key, bump_threshold, bump_amount); result } else { - default + default() } } @@ -195,7 +196,7 @@ pub fn get_user_positions(e: &Env, user: &Address) -> Positions { get_persistent_default( e, &key, - Positions::env_default(e), + || Positions::env_default(e), LEDGER_THRESHOLD_USER, LEDGER_BUMP_USER, ) @@ -388,15 +389,21 @@ pub fn has_res(e: &Env, asset: &Address) -> bool { /// If the reserve set has not been queued pub fn get_queued_reserve_set(e: &Env, asset: &Address) -> QueuedReserveInit { let key = PoolDataKey::ResInit(asset.clone()); - e.storage() - .temporary() - .extend_ttl(&key, LEDGER_THRESHOLD_USER, LEDGER_BUMP_USER); e.storage() .temporary() .get::(&key) .unwrap_optimized() } +/// Check if a reserve is actively queued +/// +/// ### Arguments +/// * `asset` - The contract address of the asset +pub fn has_queued_reserve_set(e: &Env, asset: &Address) -> bool { + let key = PoolDataKey::ResInit(asset.clone()); + e.storage().temporary().has(&key) +} + /// Set a new queued reserve set /// /// ### Arguments @@ -466,7 +473,7 @@ pub fn get_res_list(e: &Env) -> Vec
{ get_persistent_default( e, &Symbol::new(e, RES_LIST_KEY), - vec![e], + || vec![e], LEDGER_THRESHOLD_SHARED, LEDGER_BUMP_SHARED, ) @@ -507,10 +514,10 @@ pub fn push_res_list(e: &Env, asset: &Address) -> u32 { /// * `res_token_index` - The d/bToken index for the reserve pub fn get_res_emis_config(e: &Env, res_token_index: &u32) -> Option { let key = PoolDataKey::EmisConfig(*res_token_index); - get_persistent_default::>( + get_persistent_default( e, &key, - None, + || None, LEDGER_THRESHOLD_SHARED, LEDGER_BUMP_SHARED, ) @@ -541,10 +548,10 @@ pub fn set_res_emis_config( /// * `res_token_index` - The d/bToken index for the reserve pub fn get_res_emis_data(e: &Env, res_token_index: &u32) -> Option { let key = PoolDataKey::EmisData(*res_token_index); - get_persistent_default::>( + get_persistent_default( e, &key, - None, + || None, LEDGER_THRESHOLD_SHARED, LEDGER_BUMP_SHARED, ) @@ -581,13 +588,7 @@ pub fn get_user_emissions( user: user.clone(), reserve_id: *res_token_index, }); - get_persistent_default::>( - e, - &key, - None, - LEDGER_THRESHOLD_USER, - LEDGER_BUMP_USER, - ) + get_persistent_default(e, &key, || None, LEDGER_THRESHOLD_USER, LEDGER_BUMP_USER) } /// Set the users emission data for a reserve's d or d token @@ -610,10 +611,10 @@ pub fn set_user_emissions(e: &Env, user: &Address, res_token_index: &u32, data: /// Fetch the pool reserve emissions pub fn get_pool_emissions(e: &Env) -> Map { - get_persistent_default::>( + get_persistent_default( e, &Symbol::new(e, POOL_EMIS_KEY), - map![e], + || map![e], LEDGER_THRESHOLD_SHARED, LEDGER_BUMP_SHARED, ) diff --git a/pool/src/testutils.rs b/pool/src/testutils.rs index b03d65f0..3c55b22f 100644 --- a/pool/src/testutils.rs +++ b/pool/src/testutils.rs @@ -208,6 +208,7 @@ pub(crate) fn default_reserve_meta() -> (ReserveConfig, ReserveData) { l_factor: 0_7500000, util: 0_7500000, max_util: 0_9500000, + r_base: 0_0100000, r_one: 0_0500000, r_two: 0_5000000, r_three: 1_5000000, diff --git a/test-suites/src/pool.rs b/test-suites/src/pool.rs index 98e46096..d52fad5f 100644 --- a/test-suites/src/pool.rs +++ b/test-suites/src/pool.rs @@ -12,6 +12,7 @@ pub fn default_reserve_metadata() -> ReserveConfig { l_factor: 0_7500000, util: 0_7500000, max_util: 0_9500000, + r_base: 0_0100000, r_one: 0_0500000, r_two: 0_5000000, r_three: 1_5000000, diff --git a/test-suites/tests/test_backstop.rs b/test-suites/tests/test_backstop.rs index 31b0e252..aa465327 100644 --- a/test-suites/tests/test_backstop.rs +++ b/test-suites/tests/test_backstop.rs @@ -173,6 +173,25 @@ fn test_backstop() { } ) ); + assert_eq!( + fixture.env.auths()[1], + ( + pool.address.clone(), + AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + fixture.backstop.address.clone(), + Symbol::new(&fixture.env, "donate"), + vec![ + &fixture.env, + frodo.to_val(), + pool.address.to_val(), + amount.into_val(&fixture.env) + ] + )), + sub_invocations: std::vec![] + } + ) + ); assert_eq!(bstop_token.balance(&frodo), frodo_bstop_token_balance); assert_eq!( bstop_token.balance(&fixture.backstop.address), diff --git a/test-suites/tests/test_backstop_inflation_attack.rs b/test-suites/tests/test_backstop_inflation_attack.rs new file mode 100644 index 00000000..799aa601 --- /dev/null +++ b/test-suites/tests/test_backstop_inflation_attack.rs @@ -0,0 +1,73 @@ +#![cfg(test)] + +use soroban_sdk::{testutils::Address as _, vec, Address, Error, Symbol}; +use test_suites::{ + pool::default_reserve_metadata, + test_fixture::{TestFixture, TokenIndex, SCALAR_7}, +}; + +#[test] +fn test_backstop_inflation_attack() { + let mut fixture = TestFixture::create(false); + + let whale = Address::generate(&fixture.env); + let sauron = Address::generate(&fixture.env); + let pippen = Address::generate(&fixture.env); + + // create pool with 1 new reserve + fixture.create_pool(Symbol::new(&fixture.env, "Teapot"), 0, 6); + + let xlm_config = default_reserve_metadata(); + fixture.create_pool_reserve(0, TokenIndex::XLM, &xlm_config); + let pool_address = fixture.pools[0].pool.address.clone(); + + // setup backstop and update pool status + fixture.tokens[TokenIndex::BLND].mint(&whale, &(5_001_000 * SCALAR_7)); + fixture.tokens[TokenIndex::USDC].mint(&whale, &(121_000 * SCALAR_7)); + fixture.lp.join_pool( + &(400_000 * SCALAR_7), + &vec![&fixture.env, 5_001_000 * SCALAR_7, 121_000 * SCALAR_7], + &whale, + ); + + // execute inflation attack against pippen + let starting_balance = 200_000 * SCALAR_7; + fixture.lp.transfer(&whale, &sauron, &starting_balance); + fixture.lp.transfer(&whale, &pippen, &starting_balance); + + // 1. Attacker deposits a small amount as the initial depositor + let sauron_deposit_amount = 100; + let sauron_shares = fixture + .backstop + .deposit(&sauron, &pool_address, &sauron_deposit_amount); + + // 2. Attacker tries to send a large amount to the backstop before the victim can perform a deposit + let inflation_amount = 10_000 * SCALAR_7; + fixture + .lp + .transfer(&sauron, &pool_address, &inflation_amount); + + // contract correctly mints share amounts regardless of the token balance + let deposit_amount = 100; + let pippen_shares = fixture + .backstop + .deposit(&pippen, &pool_address, &deposit_amount); + assert_eq!(pippen_shares, 100); + assert_eq!(sauron_shares, pippen_shares); + + // 2b. Attacker tries to donate a large amount to the backstop before the victim can perform a deposit + // #! NOTE - Contract will stop a random address from donating. This can ONLY come from the pool. + // However, authorizations are mocked during intergation tests, so this will succeed. + fixture + .backstop + .donate(&sauron, &pool_address, &inflation_amount); + + // contracts stop any zero share deposits + let bad_deposit_result = fixture + .backstop + .try_deposit(&pippen, &pool_address, &deposit_amount); + assert_eq!( + bad_deposit_result.err(), + Some(Ok(Error::from_contract_error(1005))) + ); +} diff --git a/test-suites/tests/test_liquidation.rs b/test-suites/tests/test_liquidation.rs index 975a2b5a..ae22cc0c 100644 --- a/test-suites/tests/test_liquidation.rs +++ b/test-suites/tests/test_liquidation.rs @@ -18,38 +18,38 @@ fn test_liquidations() { let frodo = fixture.users.get(0).unwrap(); let pool_fixture = &fixture.pools[0]; - //accrue interest + // accrue interest let requests: Vec = vec![ &fixture.env, Request { request_type: RequestType::Borrow as u32, address: fixture.tokens[TokenIndex::STABLE].address.clone(), - amount: 1, + amount: 10, }, Request { request_type: RequestType::Repay as u32, address: fixture.tokens[TokenIndex::STABLE].address.clone(), - amount: 1, + amount: 10, }, Request { request_type: RequestType::Borrow as u32, address: fixture.tokens[TokenIndex::XLM].address.clone(), - amount: 1, + amount: 10, }, Request { request_type: RequestType::Repay as u32, address: fixture.tokens[TokenIndex::XLM].address.clone(), - amount: 1, + amount: 10, }, Request { request_type: RequestType::Borrow as u32, address: fixture.tokens[TokenIndex::WETH].address.clone(), - amount: 1, + amount: 10, }, Request { request_type: RequestType::Repay as u32, address: fixture.tokens[TokenIndex::WETH].address.clone(), - amount: 1, + amount: 10, }, ]; pool_fixture.pool.submit(&frodo, &frodo, &frodo, &requests); @@ -869,7 +869,7 @@ fn test_liquidations() { let events = fixture.env.events().all(); let event = vec![&fixture.env, events.get_unchecked(events.len() - 1)]; - let bad_debt: i128 = 92903018; + let bad_debt: i128 = 92903008; assert_eq!( event, vec![ @@ -914,7 +914,7 @@ fn test_liquidations() { assert_eq!(positions.liabilities.get(0).unwrap(), bad_debt); }); // check d_supply - let d_supply = 19104604034; + let d_supply = 19104605847; fixture.env.as_contract(&pool_fixture.pool.address, || { let key = PoolDataKey::ResData(fixture.tokens[TokenIndex::STABLE].address.clone()); let data = fixture diff --git a/test-suites/tests/test_pool_inflation_attack.rs b/test-suites/tests/test_pool_inflation_attack.rs new file mode 100644 index 00000000..efa2a142 --- /dev/null +++ b/test-suites/tests/test_pool_inflation_attack.rs @@ -0,0 +1,123 @@ +#![cfg(test)] + +use pool::{Request, RequestType}; +use soroban_sdk::{testutils::Address as _, vec, Address, Symbol}; +use test_suites::{ + pool::default_reserve_metadata, + test_fixture::{TestFixture, TokenIndex, SCALAR_7}, +}; + +#[test] +fn test_pool_inflation_attack() { + let mut fixture = TestFixture::create(false); + + let whale = Address::generate(&fixture.env); + let sauron = Address::generate(&fixture.env); + let pippen = Address::generate(&fixture.env); + + // create pool with 1 new reserve + fixture.create_pool(Symbol::new(&fixture.env, "Teapot"), 0, 6); + + let xlm_config = default_reserve_metadata(); + fixture.create_pool_reserve(0, TokenIndex::XLM, &xlm_config); + + // setup backstop and update pool status + fixture.tokens[TokenIndex::BLND].mint(&whale, &(500_100 * SCALAR_7)); + fixture.tokens[TokenIndex::USDC].mint(&whale, &(12_600 * SCALAR_7)); + fixture.lp.join_pool( + &(50_000 * SCALAR_7), + &vec![&fixture.env, 500_100 * SCALAR_7, 12_600 * SCALAR_7], + &whale, + ); + fixture + .backstop + .deposit(&whale, &fixture.pools[0].pool.address, &(50_000 * SCALAR_7)); + fixture.backstop.update_tkn_val(); + fixture.pools[0].pool.set_status(&0); + fixture.jump_with_sequence(60); + + // execute inflation attack against pippen + let starting_balance = 1_000_000 * SCALAR_7; + fixture.tokens[TokenIndex::XLM].mint(&sauron, &starting_balance); + fixture.tokens[TokenIndex::XLM].mint(&pippen, &starting_balance); + + // 1. Attacker deposits a single stroop as the initial depositor + let requests = vec![ + &fixture.env, + Request { + request_type: RequestType::Supply as u32, + address: fixture.tokens[TokenIndex::XLM].address.clone(), + amount: 1, + }, + ]; + fixture.pools[0] + .pool + .submit(&sauron, &sauron, &sauron, &requests); + + // skip a ledger to force pool to refresh reserve data + fixture.jump_with_sequence(5); + + // 2. Attacker frontruns victim's deposit by depositing a large amount of underlying + // to try and force an error in minting B tokens + let inflation_amount = 100 * SCALAR_7; + fixture.tokens[TokenIndex::XLM].transfer( + &sauron, + &fixture.pools[0].pool.address, + &inflation_amount, + ); + + let attack_amount = 42 * SCALAR_7; + let requests = vec![ + &fixture.env, + Request { + request_type: RequestType::Supply as u32, + address: fixture.tokens[TokenIndex::XLM].address.clone(), + amount: attack_amount, + }, + ]; + fixture.pools[0] + .pool + .submit(&pippen, &pippen, &pippen, &requests); + + // skip a ledger to force pool to refresh reserve data + fixture.jump_with_sequence(5); + + // 3. Attacker withdraws all funds and victim withdraws all funds + let requests = vec![ + &fixture.env, + Request { + request_type: RequestType::Withdraw as u32, + address: fixture.tokens[TokenIndex::XLM].address.clone(), + amount: attack_amount + inflation_amount, + }, + ]; + fixture.pools[0] + .pool + .submit(&sauron, &sauron, &sauron, &requests); + + let requests = vec![ + &fixture.env, + Request { + request_type: RequestType::Withdraw as u32, + address: fixture.tokens[TokenIndex::XLM].address.clone(), + amount: attack_amount + inflation_amount, + }, + ]; + fixture.pools[0] + .pool + .submit(&pippen, &pippen, &pippen, &requests); + + // Verify the attack was unnsuccessul and victim did not lose their funds + assert_eq!( + fixture.tokens[TokenIndex::XLM].balance(&pippen), + starting_balance + ); + assert_eq!( + fixture.tokens[TokenIndex::XLM].balance(&sauron), + starting_balance - inflation_amount + ); + assert_eq!( + fixture.tokens[TokenIndex::XLM].balance(&fixture.pools[0].pool.address), + inflation_amount + ); +} diff --git a/test-suites/tests/test_wasm_happy_path.rs b/test-suites/tests/test_wasm_happy_path.rs index 85820807..ad9fc1bc 100644 --- a/test-suites/tests/test_wasm_happy_path.rs +++ b/test-suites/tests/test_wasm_happy_path.rs @@ -404,7 +404,7 @@ fn test_wasm_happy_path() { .pool .claim(&sam, &vec![&fixture.env, 0, 3], &sam); backstop_blnd_balance -= claim_amount; - assert_eq!(claim_amount, 90908_8243315); + assert_eq!(claim_amount, 90908_8243725); assert_eq!( fixture.tokens[TokenIndex::BLND].balance(&fixture.backstop.address), backstop_blnd_balance @@ -445,7 +445,7 @@ fn test_wasm_happy_path() { .pool .claim(&frodo, &vec![&fixture.env, 0, 3], &frodo); backstop_blnd_balance -= claim_amount; - assert_eq!(claim_amount, 1073628_1728000); + assert_eq!(claim_amount, 1073628_1628000); assert_eq!( fixture.tokens[TokenIndex::BLND].balance(&fixture.backstop.address), backstop_blnd_balance @@ -456,7 +456,7 @@ fn test_wasm_happy_path() { .pool .claim(&sam, &vec![&fixture.env, 0, 3], &sam); backstop_blnd_balance -= claim_amount; - assert_eq!(claim_amount, 8361251_7312500); + assert_eq!(claim_amount, 8361251_6449409); assert_eq!( fixture.tokens[TokenIndex::BLND].balance(&fixture.backstop.address), backstop_blnd_balance