From 8f41dffb0e4380b38a5a9488ee69b9b4fae8424d Mon Sep 17 00:00:00 2001 From: markuspluna <59978114+markuspluna@users.noreply.github.com> Date: Thu, 21 Dec 2023 17:16:48 -0600 Subject: [PATCH 1/4] Pool: added max position management --- pool/src/contract.rs | 20 +++++++++++ pool/src/errors.rs | 1 + pool/src/pool/submit.rs | 76 ++++++++++++++++++++++++++++++++++++++++ pool/src/pool/user.rs | 66 ++++++++++++++++++++++++++++++++-- pool/src/storage.rs | 21 +++++++++++ test-suites/src/setup.rs | 3 ++ 6 files changed, 185 insertions(+), 2 deletions(-) diff --git a/pool/src/contract.rs b/pool/src/contract.rs index ba30d477..8876d7e7 100644 --- a/pool/src/contract.rs +++ b/pool/src/contract.rs @@ -48,6 +48,15 @@ pub trait Pool { /// If the caller is not the admin fn set_admin(e: Env, new_admin: Address); + /// (Admin only) Set a max number of positions a single user can have + /// + /// ### Arguments + /// * `max` - Max number of positions a single user can have + /// + /// ### Panics + /// If the caller is not the admin + fn set_max_positions(e: Env, max: u32); + /// (Admin only) Update the pool /// /// ### Arguments @@ -271,6 +280,17 @@ impl Pool for PoolContract { .publish((Symbol::new(&e, "update_pool"), admin), backstop_take_rate); } + fn set_max_positions(e: Env, max: u32) { + storage::extend_instance(&e); + let admin = storage::get_admin(&e); + admin.require_auth(); + + storage::set_max_positions(&e, &max); + + e.events() + .publish((Symbol::new(&e, "set_max_positions"), admin), max); + } + fn queue_set_reserve(e: Env, asset: Address, metadata: ReserveConfig) { storage::extend_instance(&e); let admin = storage::get_admin(&e); diff --git a/pool/src/errors.rs b/pool/src/errors.rs index dc71732d..645ca2bc 100644 --- a/pool/src/errors.rs +++ b/pool/src/errors.rs @@ -18,6 +18,7 @@ pub enum PoolError { InvalidHf = 10, InvalidPoolStatus = 11, InvalidUtilRate = 12, + MaxPositionsExceeded = 13, // Emission Errors (20-29) EmissionFailure = 20, // Oracle Errors (30-39) diff --git a/pool/src/pool/submit.rs b/pool/src/pool/submit.rs index bd8464a8..a809bbb8 100644 --- a/pool/src/pool/submit.rs +++ b/pool/src/pool/submit.rs @@ -41,6 +41,9 @@ pub fn execute_submit( TokenClient::new(e, &address).transfer(spender, &e.current_contract_address(), &amount); } + // ensure user is under max positions + new_from_state.positions.require_under_max(e); + // store updated info to ledger pool.store_cached_reserves(e); new_from_state.store(e); @@ -121,6 +124,7 @@ mod tests { }; e.as_contract(&pool, || { e.mock_all_auths_allowing_non_root_auth(); + storage::set_max_positions(&e, &2); storage::set_pool_config(&e, &pool_config); let pre_pool_balance_0 = underlying_0_client.balance(&pool); @@ -160,7 +164,79 @@ mod tests { assert_eq!(underlying_1_client.balance(&merry), 1_5000000); }); } + #[test] + #[should_panic(expected = "Error(Contract, #13)")] + fn test_submit_requires_positions_under_max() { + let e = Env::default(); + e.budget().reset_unlimited(); + e.mock_all_auths_allowing_non_root_auth(); + + e.ledger().set(LedgerInfo { + timestamp: 600, + protocol_version: 20, + sequence_number: 1234, + 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 frodo = Address::generate(&e); + let merry = Address::generate(&e); + let pool = testutils::create_pool(&e); + let (oracle, oracle_client) = testutils::create_mock_oracle(&e); + + let (underlying_0, underlying_0_client) = testutils::create_token_contract(&e, &bombadil); + let (reserve_config, reserve_data) = testutils::default_reserve_meta(); + testutils::create_reserve(&e, &pool, &underlying_0, &reserve_config, &reserve_data); + let (underlying_1, underlying_1_client) = testutils::create_token_contract(&e, &bombadil); + let (reserve_config, reserve_data) = testutils::default_reserve_meta(); + testutils::create_reserve(&e, &pool, &underlying_1, &reserve_config, &reserve_data); + + underlying_0_client.mint(&frodo, &16_0000000); + + oracle_client.set_data( + &bombadil, + &Asset::Other(Symbol::new(&e, "USD")), + &vec![ + &e, + Asset::Stellar(underlying_0.clone()), + Asset::Stellar(underlying_1.clone()), + ], + &7, + &300, + ); + oracle_client.set_price_stable(&vec![&e, 1_0000000, 5_0000000]); + + let pool_config = PoolConfig { + oracle, + bstop_rate: 0_100_000_000, + status: 0, + }; + e.as_contract(&pool, || { + e.mock_all_auths_allowing_non_root_auth(); + storage::set_pool_config(&e, &pool_config); + + let requests = vec![ + &e, + Request { + request_type: 2, + address: underlying_0, + amount: 15_0000000, + }, + Request { + request_type: 4, + address: underlying_1, + amount: 1_5000000, + }, + ]; + execute_submit(&e, &samwise, &frodo, &merry, requests); + }); + } #[test] #[should_panic(expected = "Error(Contract, #10)")] fn test_submit_requires_healhty() { diff --git a/pool/src/pool/user.rs b/pool/src/pool/user.rs index d755ce2f..2d969a18 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}; @@ -22,6 +22,11 @@ impl Positions { supply: Map::new(e), } } + pub fn require_under_max(&self, e: &Env) { + if storage::get_max_positions(e) < self.liabilities.len() + self.collateral.len() as u32 { + panic_with_error!(e, PoolError::MaxPositionsExceeded) + } + } } /// A user / contracts position's with the pool @@ -855,4 +860,61 @@ mod tests { assert_eq!(user.get_total_supply(1), 456 + 789); }); } + + #[test] + fn test_require_under_max_passes() { + 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, || { + storage::set_max_positions(&e, &1); + user.add_collateral(&e, &mut reserve_0, 123); + user.positions.require_under_max(&e); + }); + } + #[test] + #[should_panic(expected = "Error(Contract, #13)")] + fn test_require_under_max_fails() { + 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, || { + storage::set_max_positions(&e, &1); + user.add_collateral(&e, &mut reserve_0, 123); + user.add_liabilities(&e, &mut reserve_0, 789); + user.positions.require_under_max(&e); + }); + } + #[test] + #[should_panic(expected = "Error(Contract, #13)")] + fn test_require_under_max_fails_unset() { + 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, || { + user.add_collateral(&e, &mut reserve_0, 123); + user.positions.require_under_max(&e); + }); + } } diff --git a/pool/src/storage.rs b/pool/src/storage.rs index bbe742c5..dbdb3f2a 100644 --- a/pool/src/storage.rs +++ b/pool/src/storage.rs @@ -94,6 +94,7 @@ pub struct UserEmissionData { /********** Storage Key Types **********/ const ADMIN_KEY: &str = "Admin"; +const MAX_POSITIONS_KEY: &str = "MaxPos"; const NAME_KEY: &str = "Name"; const BACKSTOP_KEY: &str = "Backstop"; const BLND_TOKEN_KEY: &str = "BLNDTkn"; @@ -227,6 +228,26 @@ pub fn has_admin(e: &Env) -> bool { e.storage().instance().has(&Symbol::new(e, ADMIN_KEY)) } +/*********** Max Positions ***********/ +// Fetch the current max posoitions +/// +pub fn get_max_positions(e: &Env) -> u32 { + e.storage() + .instance() + .get(&Symbol::new(e, MAX_POSITIONS_KEY)) + .unwrap_or(0) +} + +/// Set a new admin +/// +/// ### Arguments +/// * `max_positions` - The max positions for the pool +pub fn set_max_positions(e: &Env, max_positions: &u32) { + e.storage() + .instance() + .set::(&Symbol::new(e, MAX_POSITIONS_KEY), max_positions); +} + /********** Metadata **********/ /// Set a pool name diff --git a/test-suites/src/setup.rs b/test-suites/src/setup.rs index 1e98c32f..288e9785 100644 --- a/test-suites/src/setup.rs +++ b/test-suites/src/setup.rs @@ -73,6 +73,9 @@ pub fn create_fixture_with_data<'a>(wasm: bool) -> TestFixture<'a> { ]; pool_fixture.pool.set_emissions_config(&reserve_emissions); + // set max positions for pool + pool_fixture.pool.set_max_positions(&6); + // deposit into backstop, add to reward zone fixture .backstop From 71d980dd82e9bcbbd483d1ec13abf0965f18bd49 Mon Sep 17 00:00:00 2001 From: markuspluna <59978114+markuspluna@users.noreply.github.com> Date: Thu, 21 Dec 2023 17:31:43 -0600 Subject: [PATCH 2/4] Pool: added new_admin auth requirement for admin swaps --- pool/src/contract.rs | 1 + pool/src/pool/submit.rs | 2 +- test-suites/tests/test_pool.rs | 14 ++++++++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/pool/src/contract.rs b/pool/src/contract.rs index 8876d7e7..eaa561f4 100644 --- a/pool/src/contract.rs +++ b/pool/src/contract.rs @@ -262,6 +262,7 @@ impl Pool for PoolContract { storage::extend_instance(&e); let admin = storage::get_admin(&e); admin.require_auth(); + new_admin.require_auth(); storage::set_admin(&e, &new_admin); diff --git a/pool/src/pool/submit.rs b/pool/src/pool/submit.rs index a809bbb8..4b93a4ec 100644 --- a/pool/src/pool/submit.rs +++ b/pool/src/pool/submit.rs @@ -193,7 +193,7 @@ mod tests { let (reserve_config, reserve_data) = testutils::default_reserve_meta(); testutils::create_reserve(&e, &pool, &underlying_0, &reserve_config, &reserve_data); - let (underlying_1, underlying_1_client) = testutils::create_token_contract(&e, &bombadil); + let (underlying_1, _) = testutils::create_token_contract(&e, &bombadil); let (reserve_config, reserve_data) = testutils::default_reserve_meta(); testutils::create_reserve(&e, &pool, &underlying_1, &reserve_config, &reserve_data); diff --git a/test-suites/tests/test_pool.rs b/test-suites/tests/test_pool.rs index 61d0cc95..9f76c99d 100644 --- a/test-suites/tests/test_pool.rs +++ b/test-suites/tests/test_pool.rs @@ -707,6 +707,20 @@ fn test_pool_config() { } ) ); + assert_eq!( + fixture.env.auths()[1], + ( + new_admin.clone(), + AuthorizedInvocation { + function: AuthorizedFunction::Contract(( + pool_fixture.pool.address.clone(), + Symbol::new(&fixture.env, "set_admin"), + vec![&fixture.env, new_admin.to_val(),] + )), + sub_invocations: std::vec![] + } + ) + ); let event = vec![&fixture.env, fixture.env.events().all().last_unchecked()]; assert_eq!( event, From eea02dc1ac7733d6d3644420f8a238021a2abd38 Mon Sep 17 00:00:00 2001 From: markuspluna <59978114+markuspluna@users.noreply.github.com> Date: Thu, 28 Dec 2023 17:02:54 -0600 Subject: [PATCH 3/4] Pool: position cap refactor --- mocks/mock-pool-factory/src/pool_factory.rs | 3 + pool-factory/src/pool_factory.rs | 4 + pool-factory/src/test.rs | 24 ++- pool/src/auctions/auction.rs | 8 + .../src/auctions/backstop_interest_auction.rs | 4 + pool/src/auctions/bad_debt_auction.rs | 6 + pool/src/auctions/user_liquidation_auction.rs | 7 + pool/src/contract.rs | 37 ++-- pool/src/pool/actions.rs | 61 +++++++ pool/src/pool/bad_debt.rs | 5 + pool/src/pool/config.rs | 20 ++- pool/src/pool/health_factor.rs | 1 + pool/src/pool/pool.rs | 167 +++++++++++++++++- pool/src/pool/reserve.rs | 4 + pool/src/pool/status.rs | 20 +++ pool/src/pool/submit.rs | 80 +-------- pool/src/pool/user.rs | 66 +------ pool/src/storage.rs | 22 +-- test-suites/src/setup.rs | 6 +- test-suites/src/test_fixture.rs | 3 +- test-suites/tests/test_pool.rs | 13 +- 21 files changed, 356 insertions(+), 205 deletions(-) diff --git a/mocks/mock-pool-factory/src/pool_factory.rs b/mocks/mock-pool-factory/src/pool_factory.rs index dc7e2a11..9fff4a20 100644 --- a/mocks/mock-pool-factory/src/pool_factory.rs +++ b/mocks/mock-pool-factory/src/pool_factory.rs @@ -32,6 +32,7 @@ pub trait MockPoolFactoryTrait { salt: BytesN<32>, oracle: Address, backstop_take_rate: u64, + max_positions: u32, ) -> Address; /// Checks if contract address was deployed by the factory @@ -65,6 +66,7 @@ impl MockPoolFactoryTrait for MockPoolFactory { _salt: BytesN<32>, oracle: Address, backstop_take_rate: u64, + max_positions: u32, ) -> Address { storage::extend_instance(&e); let pool_init_meta = storage::get_pool_init_meta(&e); @@ -79,6 +81,7 @@ impl MockPoolFactoryTrait for MockPoolFactory { init_args.push_back(name.to_val()); init_args.push_back(oracle.to_val()); init_args.push_back(backstop_take_rate.into_val(&e)); + init_args.push_back(max_positions.into_val(&e)); init_args.push_back(pool_init_meta.backstop.to_val()); init_args.push_back(pool_init_meta.blnd_id.to_val()); init_args.push_back(pool_init_meta.usdc_id.to_val()); diff --git a/pool-factory/src/pool_factory.rs b/pool-factory/src/pool_factory.rs index f8fbdea8..12d6bedd 100644 --- a/pool-factory/src/pool_factory.rs +++ b/pool-factory/src/pool_factory.rs @@ -25,6 +25,7 @@ pub trait PoolFactory { /// * `name` - The name of the pool /// * `oracle` - The oracle address for the pool /// * `backstop_take_rate` - The backstop take rate for the pool + /// * `max_positions` - The maximum user positions supported by the pool fn deploy( e: Env, admin: Address, @@ -32,6 +33,7 @@ pub trait PoolFactory { salt: BytesN<32>, oracle: Address, backstop_take_rate: u64, + max_positions: u32, ) -> Address; /// Checks if contract address was deployed by the factory @@ -60,6 +62,7 @@ impl PoolFactory for PoolFactoryContract { salt: BytesN<32>, oracle: Address, backstop_take_rate: u64, + max_positions: u32, ) -> Address { admin.require_auth(); storage::extend_instance(&e); @@ -75,6 +78,7 @@ impl PoolFactory for PoolFactoryContract { init_args.push_back(name.to_val()); init_args.push_back(oracle.to_val()); init_args.push_back(backstop_take_rate.into_val(&e)); + init_args.push_back(max_positions.into_val(&e)); init_args.push_back(pool_init_meta.backstop.to_val()); init_args.push_back(pool_init_meta.blnd_id.to_val()); init_args.push_back(pool_init_meta.usdc_id.to_val()); diff --git a/pool-factory/src/test.rs b/pool-factory/src/test.rs index ced86f29..e4dc5a89 100644 --- a/pool-factory/src/test.rs +++ b/pool-factory/src/test.rs @@ -30,6 +30,7 @@ fn test_pool_factory() { let oracle = Address::generate(&e); let backstop_id = Address::generate(&e); let backstop_rate: u64 = 100000; + let max_positions: u32 = 6; let blnd_id = Address::generate(&e); let usdc_id = Address::generate(&e); @@ -49,8 +50,14 @@ fn test_pool_factory() { let name2 = Symbol::new(&e, "pool2"); let salt = BytesN::<32>::random(&e); - let deployed_pool_address_1 = - pool_factory_client.deploy(&bombadil, &name1, &salt, &oracle, &backstop_rate); + let deployed_pool_address_1 = pool_factory_client.deploy( + &bombadil, + &name1, + &salt, + &oracle, + &backstop_rate, + &max_positions, + ); let event = vec![&e, e.events().all().last_unchecked()]; assert_eq!( @@ -66,8 +73,14 @@ fn test_pool_factory() { ); let salt = BytesN::<32>::random(&e); - let deployed_pool_address_2 = - pool_factory_client.deploy(&bombadil, &name2, &salt, &oracle, &backstop_rate); + let deployed_pool_address_2 = pool_factory_client.deploy( + &bombadil, + &name2, + &salt, + &oracle, + &backstop_rate, + &max_positions, + ); e.as_contract(&deployed_pool_address_1, || { assert_eq!( @@ -92,7 +105,8 @@ fn test_pool_factory() { pool::PoolConfig { oracle: oracle, bstop_rate: backstop_rate, - status: 6 + status: 6, + max_positions: 6 } ); assert_eq!( diff --git a/pool/src/auctions/auction.rs b/pool/src/auctions/auction.rs index 605cb2fa..15a8d952 100644 --- a/pool/src/auctions/auction.rs +++ b/pool/src/auctions/auction.rs @@ -387,6 +387,7 @@ mod tests { oracle: oracle_id, bstop_rate: 0_100_000_000, status: 0, + max_positions: 4, }; e.as_contract(&pool_address, || { storage::set_pool_config(&e, &pool_config); @@ -487,6 +488,7 @@ mod tests { oracle: oracle_id, bstop_rate: 0_100_000_000, status: 0, + max_positions: 4, }; e.as_contract(&pool_address, || { storage::set_pool_config(&e, &pool_config); @@ -597,6 +599,7 @@ mod tests { oracle: oracle_address, bstop_rate: 0_100_000_000, status: 0, + max_positions: 4, }; e.as_contract(&pool_address, || { storage::set_user_positions(&e, &samwise, &positions); @@ -739,6 +742,7 @@ mod tests { oracle: oracle_address, bstop_rate: 0_100_000_000, status: 0, + max_positions: 4, }; let positions: Positions = Positions { collateral: map![ @@ -846,6 +850,7 @@ mod tests { oracle: oracle_address, bstop_rate: 0_100_000_000, status: 0, + max_positions: 4, }; let positions: Positions = Positions { collateral: map![ @@ -966,6 +971,7 @@ mod tests { oracle: oracle_address, bstop_rate: 0_100_000_000, status: 0, + max_positions: 4, }; let positions: Positions = Positions { collateral: map![ @@ -1154,6 +1160,7 @@ mod tests { oracle: oracle_address, bstop_rate: 0_100_000_000, status: 0, + max_positions: 4, }; let positions: Positions = Positions { collateral: map![ @@ -1275,6 +1282,7 @@ mod tests { oracle: oracle_address, bstop_rate: 0_100_000_000, status: 0, + max_positions: 4, }; let positions: Positions = Positions { collateral: map![ diff --git a/pool/src/auctions/backstop_interest_auction.rs b/pool/src/auctions/backstop_interest_auction.rs index 1245264c..962062a1 100644 --- a/pool/src/auctions/backstop_interest_auction.rs +++ b/pool/src/auctions/backstop_interest_auction.rs @@ -229,6 +229,7 @@ mod tests { oracle: oracle_id, bstop_rate: 0_100_000_000, status: 0, + max_positions: 4, }; e.as_contract(&pool_address, || { storage::set_pool_config(&e, &pool_config); @@ -340,6 +341,7 @@ mod tests { oracle: oracle_id, bstop_rate: 0_100_000_000, status: 0, + max_positions: 4, }; e.as_contract(&pool_address, || { storage::set_pool_config(&e, &pool_config); @@ -451,6 +453,7 @@ mod tests { oracle: oracle_id, bstop_rate: 0_100_000_000, status: 0, + max_positions: 4, }; e.as_contract(&pool_address, || { storage::set_pool_config(&e, &pool_config); @@ -545,6 +548,7 @@ mod tests { oracle: Address::generate(&e), bstop_rate: 0_100_000_000, status: 0, + max_positions: 4, }; let mut auction_data = AuctionData { bid: map![&e, (usdc_id.clone(), 95_0000000)], diff --git a/pool/src/auctions/bad_debt_auction.rs b/pool/src/auctions/bad_debt_auction.rs index af541f9f..bc45e685 100644 --- a/pool/src/auctions/bad_debt_auction.rs +++ b/pool/src/auctions/bad_debt_auction.rs @@ -305,6 +305,7 @@ mod tests { oracle: oracle_id, bstop_rate: 0_100_000_000, status: 0, + max_positions: 4, }; e.as_contract(&pool_address, || { storage::set_pool_config(&e, &pool_config); @@ -438,6 +439,7 @@ mod tests { oracle: oracle_id, bstop_rate: 0_100_000_000, status: 0, + max_positions: 4, }; e.as_contract(&pool_address, || { storage::set_pool_config(&e, &pool_config); @@ -572,6 +574,7 @@ mod tests { oracle: oracle_id, bstop_rate: 0_100_000_000, status: 0, + max_positions: 4, }; e.as_contract(&pool_address, || { storage::set_pool_config(&e, &pool_config); @@ -679,6 +682,7 @@ mod tests { oracle: Address::generate(&e), bstop_rate: 0_100_000_000, status: 0, + max_positions: 4, }; let mut auction_data = AuctionData { bid: map![&e, (underlying_0, 10_0000000), (underlying_1, 2_5000000)], @@ -822,6 +826,7 @@ mod tests { oracle: Address::generate(&e), bstop_rate: 0_100_000_000, status: 0, + max_positions: 4, }; let mut auction_data = AuctionData { bid: map![ @@ -972,6 +977,7 @@ mod tests { oracle: Address::generate(&e), bstop_rate: 0_100_000_000, status: 0, + max_positions: 4, }; let mut auction_data = AuctionData { bid: map![ diff --git a/pool/src/auctions/user_liquidation_auction.rs b/pool/src/auctions/user_liquidation_auction.rs index e3c04944..5a866bbf 100644 --- a/pool/src/auctions/user_liquidation_auction.rs +++ b/pool/src/auctions/user_liquidation_auction.rs @@ -181,6 +181,7 @@ mod tests { oracle, bstop_rate: 0_100_000_000, status: 0, + max_positions: 4, }; e.as_contract(&pool_address, || { storage::set_pool_config(&e, &pool_config); @@ -289,6 +290,7 @@ mod tests { oracle: oracle_address, bstop_rate: 0_100_000_000, status: 0, + max_positions: 4, }; e.as_contract(&pool_address, || { storage::set_user_positions(&e, &samwise, &positions); @@ -394,6 +396,7 @@ mod tests { oracle: oracle_address, bstop_rate: 0_100_000_000, status: 0, + max_positions: 4, }; let positions: Positions = Positions { collateral: map![ @@ -501,6 +504,7 @@ mod tests { oracle: oracle_address, bstop_rate: 0_100_000_000, status: 0, + max_positions: 4, }; let positions: Positions = Positions { collateral: map![ @@ -609,6 +613,7 @@ mod tests { oracle: oracle_address, bstop_rate: 0_100_000_000, status: 0, + max_positions: 4, }; let positions: Positions = Positions { collateral: map![ @@ -728,6 +733,7 @@ mod tests { oracle: oracle_address, bstop_rate: 0_100_000_000, status: 0, + max_positions: 4, }; let positions: Positions = Positions { collateral: map![ @@ -903,6 +909,7 @@ mod tests { oracle: oracle_address, bstop_rate: 0_100_000_000, status: 0, + max_positions: 4, }; let positions: Positions = Positions { collateral: map![ diff --git a/pool/src/contract.rs b/pool/src/contract.rs index eaa561f4..6b2f8a5d 100644 --- a/pool/src/contract.rs +++ b/pool/src/contract.rs @@ -22,6 +22,7 @@ pub trait Pool { /// * `name` - The name of the pool /// * `oracle` - The contract address of the oracle /// * `backstop_take_rate` - The take rate for the backstop in stroops + /// * `max_positions` - The maximum number of positions a user is permitted to have /// /// Pool Factory supplied: /// * `backstop_id` - The contract address of the pool's backstop module @@ -34,6 +35,7 @@ pub trait Pool { name: Symbol, oracle: Address, bstop_rate: u64, + max_positions: u32, backstop_id: Address, blnd_id: Address, usdc_id: Address, @@ -48,23 +50,15 @@ pub trait Pool { /// If the caller is not the admin fn set_admin(e: Env, new_admin: Address); - /// (Admin only) Set a max number of positions a single user can have - /// - /// ### Arguments - /// * `max` - Max number of positions a single user can have - /// - /// ### Panics - /// If the caller is not the admin - fn set_max_positions(e: Env, max: u32); - /// (Admin only) Update the pool /// /// ### Arguments /// * `backstop_take_rate` - The new take rate for the backstop + /// * `max_positions` - The new maximum number of allowed positions for a single user's account /// /// ### Panics /// If the caller is not the admin - fn update_pool(e: Env, backstop_take_rate: u64); + fn update_pool(e: Env, backstop_take_rate: u64, max_positions: u32); /// (Admin only) Queues setting data for a reserve in the pool /// @@ -240,6 +234,7 @@ impl Pool for PoolContract { name: Symbol, oracle: Address, bstop_rate: u64, + max_postions: u32, backstop_id: Address, blnd_id: Address, usdc_id: Address, @@ -252,6 +247,7 @@ impl Pool for PoolContract { &name, &oracle, &bstop_rate, + &max_postions, &backstop_id, &blnd_id, &usdc_id, @@ -270,26 +266,17 @@ impl Pool for PoolContract { .publish((Symbol::new(&e, "set_admin"), admin), new_admin); } - fn update_pool(e: Env, backstop_take_rate: u64) { + fn update_pool(e: Env, backstop_take_rate: u64, max_positions: u32) { storage::extend_instance(&e); let admin = storage::get_admin(&e); admin.require_auth(); - pool::execute_update_pool(&e, backstop_take_rate); - - e.events() - .publish((Symbol::new(&e, "update_pool"), admin), backstop_take_rate); - } + pool::execute_update_pool(&e, backstop_take_rate, max_positions); - fn set_max_positions(e: Env, max: u32) { - storage::extend_instance(&e); - let admin = storage::get_admin(&e); - admin.require_auth(); - - storage::set_max_positions(&e, &max); - - e.events() - .publish((Symbol::new(&e, "set_max_positions"), admin), max); + e.events().publish( + (Symbol::new(&e, "update_pool"), admin), + (backstop_take_rate, max_positions), + ); } fn queue_set_reserve(e: Env, asset: Address, metadata: ReserveConfig) { diff --git a/pool/src/pool/actions.rs b/pool/src/pool/actions.rs index deeec137..d9933da5 100644 --- a/pool/src/pool/actions.rs +++ b/pool/src/pool/actions.rs @@ -280,6 +280,8 @@ pub fn build_actions_from_request( } _ => panic_with_error!(e, PoolError::BadRequest), } + // Verify max positions haven't been exceeded + pool.require_under_max(e, from_state.positions.clone(), request.request_type) } (actions, from_state, check_health) } @@ -332,6 +334,7 @@ mod tests { oracle: Address::generate(&e), bstop_rate: 0_100_000_000, status: 0, + max_positions: 2, }; e.as_contract(&pool, || { storage::set_pool_config(&e, &pool_config); @@ -400,6 +403,7 @@ mod tests { oracle: Address::generate(&e), bstop_rate: 0_200_000_000, status: 0, + max_positions: 2, }; let user_positions = Positions { @@ -473,6 +477,7 @@ mod tests { oracle: Address::generate(&e), bstop_rate: 0_200_000_000, status: 0, + max_positions: 2, }; let user_positions = Positions { liabilities: map![&e], @@ -543,6 +548,7 @@ mod tests { oracle: Address::generate(&e), bstop_rate: 0_100_000_000, status: 0, + max_positions: 2, }; e.as_contract(&pool, || { storage::set_pool_config(&e, &pool_config); @@ -614,6 +620,7 @@ mod tests { oracle: Address::generate(&e), bstop_rate: 0_200_000_000, status: 0, + max_positions: 2, }; let user_positions = Positions { liabilities: map![&e], @@ -686,6 +693,7 @@ mod tests { oracle: Address::generate(&e), bstop_rate: 0_200_000_000, status: 0, + max_positions: 2, }; let user_positions = Positions { liabilities: map![&e], @@ -755,6 +763,7 @@ mod tests { oracle: Address::generate(&e), bstop_rate: 0_200_000_000, status: 0, + max_positions: 2, }; e.as_contract(&pool, || { storage::set_pool_config(&e, &pool_config); @@ -819,6 +828,7 @@ mod tests { oracle: Address::generate(&e), bstop_rate: 0_200_000_000, status: 0, + max_positions: 2, }; let user_positions = Positions { liabilities: map![&e, (0, 20_0000000)], @@ -892,6 +902,7 @@ mod tests { oracle: Address::generate(&e), bstop_rate: 0_200_000_000, status: 0, + max_positions: 2, }; let user_positions = Positions { liabilities: map![&e, (0, 20_0000000)], @@ -971,6 +982,7 @@ mod tests { oracle: Address::generate(&e), bstop_rate: 0_200_000_000, status: 0, + max_positions: 2, }; let user_positions = Positions::env_default(&e); e.as_contract(&pool, || { @@ -1121,6 +1133,7 @@ mod tests { oracle: oracle_address, bstop_rate: 0_100_000_000, status: 0, + max_positions: 2, }; let positions: Positions = Positions { collateral: map![ @@ -1244,6 +1257,7 @@ mod tests { oracle: oracle_address, bstop_rate: 0_100_000_000, status: 0, + max_positions: 2, }; let auction_data = AuctionData { bid: map![&e, (underlying_0, 10_0000000), (underlying_1, 2_5000000)], @@ -1373,6 +1387,7 @@ mod tests { oracle: Address::generate(&e), bstop_rate: 0_100_000_000, status: 0, + max_positions: 2, }; let auction_data = AuctionData { bid: map![&e, (usdc_id.clone(), 952_0000000)], @@ -1456,6 +1471,7 @@ mod tests { oracle: Address::generate(&e), bstop_rate: 0_100_000_000, status: 0, + max_positions: 2, }; let auction_data = AuctionData { bid: map![&e, (underlying_0.clone(), 952_0000000)], @@ -1499,4 +1515,49 @@ mod tests { assert_eq!(actions.spender_transfer.len(), 0); }); } + #[test] + #[should_panic(expected = "Error(Contract, #13)")] + fn test_actions_requires_positions_under_max() { + let e = Env::default(); + e.mock_all_auths(); + + let bombadil = Address::generate(&e); + let samwise = Address::generate(&e); + let pool = testutils::create_pool(&e); + + let (underlying, _) = testutils::create_token_contract(&e, &bombadil); + let (reserve_config, reserve_data) = testutils::default_reserve_meta(); + testutils::create_reserve(&e, &pool, &underlying, &reserve_config, &reserve_data); + e.ledger().set(LedgerInfo { + timestamp: 600, + protocol_version: 20, + sequence_number: 1234, + 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), + bstop_rate: 0_200_000_000, + status: 0, + max_positions: 0, + }; + e.as_contract(&pool, || { + storage::set_pool_config(&e, &pool_config); + + let mut pool = Pool::load(&e); + + let requests = vec![ + &e, + Request { + request_type: 4, + address: underlying.clone(), + amount: 10_1234567, + }, + ]; + build_actions_from_request(&e, &mut pool, &samwise, requests); + }); + } } diff --git a/pool/src/pool/bad_debt.rs b/pool/src/pool/bad_debt.rs index 0e1279a7..0af51e7d 100644 --- a/pool/src/pool/bad_debt.rs +++ b/pool/src/pool/bad_debt.rs @@ -116,6 +116,7 @@ mod tests { oracle: Address::generate(&e), bstop_rate: 0_100_000_000, status: 0, + max_positions: 2, }; let user_positions = Positions { liabilities: map![&e, (0, 24_0000000), (1, 25_0000000)], @@ -182,6 +183,7 @@ mod tests { oracle: Address::generate(&e), bstop_rate: 0_100_000_000, status: 0, + max_positions: 2, }; let user_positions = Positions { liabilities: map![&e, (0, 24_0000000), (1, 25_0000000)], @@ -234,6 +236,7 @@ mod tests { oracle: Address::generate(&e), bstop_rate: 0_100_000_000, status: 0, + max_positions: 2, }; let user_positions = Positions::env_default(&e); e.as_contract(&pool, || { @@ -282,6 +285,7 @@ mod tests { oracle: Address::generate(&e), bstop_rate: 0_100_000_000, status: 0, + max_positions: 2, }; let user_positions = Positions { liabilities: map![&e, (0, 24_0000000), (1, 25_0000000)], @@ -337,6 +341,7 @@ mod tests { oracle: Address::generate(&e), bstop_rate: 0_100_000_000, status: 0, + max_positions: 2, }; let backstop_positions = Positions { diff --git a/pool/src/pool/config.rs b/pool/src/pool/config.rs index f0966f14..828d026e 100644 --- a/pool/src/pool/config.rs +++ b/pool/src/pool/config.rs @@ -17,6 +17,7 @@ pub fn execute_initialize( name: &Symbol, oracle: &Address, bstop_rate: &u64, + max_positions: &u32, backstop_address: &Address, blnd_id: &Address, usdc_id: &Address, @@ -39,6 +40,7 @@ pub fn execute_initialize( oracle: oracle.clone(), bstop_rate: *bstop_rate, status: 6, + max_positions: *max_positions, }, ); storage::set_blnd_token(e, blnd_id); @@ -46,13 +48,14 @@ pub fn execute_initialize( } /// Update the pool -pub fn execute_update_pool(e: &Env, backstop_take_rate: u64) { +pub fn execute_update_pool(e: &Env, backstop_take_rate: u64, max_positions: u32) { // ensure backstop is [0,1) if backstop_take_rate >= 1_000_000_000 { panic_with_error!(e, PoolError::BadRequest); } let mut pool_config = storage::get_pool_config(e); pool_config.bstop_rate = backstop_take_rate; + pool_config.max_positions = max_positions; storage::set_pool_config(e, &pool_config); } @@ -179,6 +182,7 @@ mod tests { let name = Symbol::new(&e, "pool_name"); let oracle = Address::generate(&e); let bstop_rate = 0_100_000_000u64; + let max_postions = 2; let backstop_address = Address::generate(&e); let blnd_id = Address::generate(&e); let usdc_id = Address::generate(&e); @@ -190,6 +194,7 @@ mod tests { &name, &oracle, &bstop_rate, + &max_postions, &backstop_address, &blnd_id, &usdc_id, @@ -215,16 +220,18 @@ mod tests { oracle: Address::generate(&e), bstop_rate: 0_100_000_000, status: 0, + max_positions: 2, }; e.as_contract(&pool, || { storage::set_pool_config(&e, &pool_config); // happy path - execute_update_pool(&e, 0_200_000_000u64); + execute_update_pool(&e, 0_200_000_000u64, 4u32); let new_pool_config = storage::get_pool_config(&e); assert_eq!(new_pool_config.bstop_rate, 0_200_000_000u64); assert_eq!(new_pool_config.oracle, pool_config.oracle); assert_eq!(new_pool_config.status, pool_config.status); + assert_eq!(new_pool_config.max_positions, 4u32) }); } @@ -238,11 +245,12 @@ mod tests { oracle: Address::generate(&e), bstop_rate: 0_100_000_000, status: 0, + max_positions: 2, }; e.as_contract(&pool, || { storage::set_pool_config(&e, &pool_config); - execute_update_pool(&e, 1_000_000_000u64); + execute_update_pool(&e, 1_000_000_000u64, 4u32); }); } #[test] @@ -269,6 +277,7 @@ mod tests { oracle: Address::generate(&e), bstop_rate: 0_100_000_000, status: 6, + max_positions: 2, }; e.as_contract(&pool, || { storage::set_pool_config(&e, &pool_config); @@ -313,6 +322,7 @@ mod tests { oracle: Address::generate(&e), bstop_rate: 0_100_000_000, status: 0, + max_positions: 2, }; e.as_contract(&pool, || { storage::set_pool_config(&e, &pool_config); @@ -509,6 +519,7 @@ mod tests { oracle: Address::generate(&e), bstop_rate: 0_100_000_000, status: 0, + max_positions: 2, }; e.as_contract(&pool, || { storage::set_pool_config(&e, &pool_config); @@ -566,6 +577,7 @@ mod tests { oracle: Address::generate(&e), bstop_rate: 0_100_000_000, status: 0, + max_positions: 2, }; e.as_contract(&pool, || { storage::set_pool_config(&e, &pool_config); @@ -640,6 +652,7 @@ mod tests { oracle: Address::generate(&e), bstop_rate: 0_100_000_000, status: 0, + max_positions: 2, }; e.as_contract(&pool, || { storage::set_pool_config(&e, &pool_config); @@ -695,6 +708,7 @@ mod tests { oracle: Address::generate(&e), bstop_rate: 0_100_000_000, status: 0, + max_positions: 2, }; e.as_contract(&pool, || { storage::set_pool_config(&e, &pool_config); diff --git a/pool/src/pool/health_factor.rs b/pool/src/pool/health_factor.rs index c2f5c37d..4d129f1e 100644 --- a/pool/src/pool/health_factor.rs +++ b/pool/src/pool/health_factor.rs @@ -180,6 +180,7 @@ mod tests { oracle, bstop_rate: 0_200_000_000, status: 0, + max_positions: 5, }; let positions = Positions { diff --git a/pool/src/pool/pool.rs b/pool/src/pool/pool.rs index f1cef37d..02c0319d 100644 --- a/pool/src/pool/pool.rs +++ b/pool/src/pool/pool.rs @@ -5,6 +5,7 @@ use sep_40_oracle::{Asset, PriceFeedClient}; use crate::{ errors::PoolError, storage::{self, PoolConfig}, + Positions, }; use super::reserve::Reserve; @@ -75,6 +76,20 @@ impl Pool { } } + /// Require that an action does not violate the maximum number of positions, or panic. + /// + /// ### Arguments + /// * `positions` - The user's positions + /// * `action_type` - The type of action being performed + pub fn require_under_max(&self, e: &Env, positions: Positions, action_type: u32) { + if self.config.max_positions + < positions.liabilities.len() + positions.collateral.len() as u32 + && (action_type == 2 || action_type == 4) + { + panic_with_error!(e, PoolError::MaxPositionsExceeded) + } + } + /// Load the decimals of the prices for the Pool's oracle. Returns a cached version if one /// already exists. pub fn load_price_decimals(&mut self, e: &Env) -> u32 { @@ -117,7 +132,7 @@ mod tests { Symbol, }; - use crate::{storage::ReserveData, testutils}; + use crate::{pool::User, storage::ReserveData, testutils}; use super::*; @@ -149,6 +164,7 @@ mod tests { oracle, bstop_rate: 0_200_000_000, status: 0, + max_positions: 2, }; e.as_contract(&pool, || { storage::set_pool_config(&e, &pool_config); @@ -214,6 +230,7 @@ mod tests { oracle, bstop_rate: 0_200_000_000, status: 0, + max_positions: 2, }; e.as_contract(&pool, || { storage::set_pool_config(&e, &pool_config); @@ -268,6 +285,7 @@ mod tests { oracle, bstop_rate: 0_200_000_000, status: 2, + max_positions: 2, }; e.as_contract(&pool, || { storage::set_pool_config(&e, &pool_config); @@ -287,6 +305,7 @@ mod tests { oracle, bstop_rate: 0_200_000_000, status: 1, + max_positions: 2, }; e.as_contract(&pool, || { storage::set_pool_config(&e, &pool_config); @@ -307,6 +326,7 @@ mod tests { oracle, bstop_rate: 0_200_000_000, status: 2, + max_positions: 2, }; e.as_contract(&pool, || { storage::set_pool_config(&e, &pool_config); @@ -326,6 +346,7 @@ mod tests { oracle, bstop_rate: 0_200_000_000, status: 1, + max_positions: 2, }; e.as_contract(&pool, || { storage::set_pool_config(&e, &pool_config); @@ -346,6 +367,7 @@ mod tests { oracle, bstop_rate: 0_200_000_000, status: 4, + max_positions: 2, }; e.as_contract(&pool, || { storage::set_pool_config(&e, &pool_config); @@ -366,6 +388,7 @@ mod tests { oracle, bstop_rate: 0_200_000_000, status: 4, + max_positions: 2, }; e.as_contract(&pool, || { storage::set_pool_config(&e, &pool_config); @@ -385,6 +408,7 @@ mod tests { oracle, bstop_rate: 0_200_000_000, status: 4, + max_positions: 2, }; e.as_contract(&pool, || { storage::set_pool_config(&e, &pool_config); @@ -416,6 +440,7 @@ mod tests { oracle, bstop_rate: 0_200_000_000, status: 0, + max_positions: 2, }; e.as_contract(&pool, || { storage::set_pool_config(&e, &pool_config); @@ -454,6 +479,7 @@ mod tests { oracle, bstop_rate: 0_200_000_000, status: 0, + max_positions: 2, }; e.as_contract(&pool, || { storage::set_pool_config(&e, &pool_config); @@ -505,6 +531,7 @@ mod tests { oracle, bstop_rate: 0_200_000_000, status: 0, + max_positions: 2, }; e.as_contract(&pool, || { storage::set_pool_config(&e, &pool_config); @@ -514,4 +541,142 @@ mod tests { assert!(false); }); } + #[test] + fn test_require_under_max_passes() { + let e = Env::default(); + let samwise = Address::generate(&e); + let pool = testutils::create_pool(&e); + + let mut reserve_0 = testutils::default_reserve(&e); + let (oracle, _) = testutils::create_mock_oracle(&e); + let mut user = User { + address: samwise.clone(), + positions: Positions::env_default(&e), + }; + let pool_config = PoolConfig { + oracle, + bstop_rate: 0_200_000_000, + status: 0, + max_positions: 2, + }; + e.as_contract(&pool, || { + user.add_collateral(&e, &mut reserve_0, 123); + storage::set_pool_config(&e, &pool_config); + let pool = Pool::load(&e); + + pool.require_under_max(&e, user.positions, 4); + }); + } + #[test] + #[should_panic(expected = "Error(Contract, #13)")] + fn test_require_under_max_fails_borrow() { + 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), + }; + let (oracle, _) = testutils::create_mock_oracle(&e); + let pool_config = PoolConfig { + oracle, + bstop_rate: 0_200_000_000, + status: 0, + max_positions: 1, + }; + e.as_contract(&pool, || { + storage::set_pool_config(&e, &pool_config); + user.add_collateral(&e, &mut reserve_0, 123); + user.add_liabilities(&e, &mut reserve_0, 789); + let pool = Pool::load(&e); + + pool.require_under_max(&e, user.positions, 4); + }); + } + #[test] + #[should_panic(expected = "Error(Contract, #13)")] + fn test_require_under_max_fails_deposit() { + 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), + }; + let (oracle, _) = testutils::create_mock_oracle(&e); + let pool_config = PoolConfig { + oracle, + bstop_rate: 0_200_000_000, + status: 0, + max_positions: 1, + }; + e.as_contract(&pool, || { + storage::set_pool_config(&e, &pool_config); + let pool = Pool::load(&e); + user.add_collateral(&e, &mut reserve_0, 123); + user.add_liabilities(&e, &mut reserve_0, 789); + pool.require_under_max(&e, user.positions, 2); + }); + } + #[test] + fn test_require_under_max_passes_withdraw() { + 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), + }; + let (oracle, _) = testutils::create_mock_oracle(&e); + let pool_config = PoolConfig { + oracle, + bstop_rate: 0_200_000_000, + status: 0, + max_positions: 1, + }; + e.as_contract(&pool, || { + storage::set_pool_config(&e, &pool_config); + let pool = Pool::load(&e); + user.add_collateral(&e, &mut reserve_0, 123); + user.add_liabilities(&e, &mut reserve_0, 789); + + pool.require_under_max(&e, user.positions, 3); + }); + } + #[test] + fn test_require_under_max_passes_repay() { + 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), + }; + let (oracle, _) = testutils::create_mock_oracle(&e); + let pool_config = PoolConfig { + oracle, + bstop_rate: 0_200_000_000, + status: 0, + max_positions: 1, + }; + e.as_contract(&pool, || { + storage::set_pool_config(&e, &pool_config); + let pool = Pool::load(&e); + user.add_collateral(&e, &mut reserve_0, 123); + user.add_liabilities(&e, &mut reserve_0, 789); + pool.require_under_max(&e, user.positions, 5); + }); + } } diff --git a/pool/src/pool/reserve.rs b/pool/src/pool/reserve.rs index 6533a41c..01553293 100644 --- a/pool/src/pool/reserve.rs +++ b/pool/src/pool/reserve.rs @@ -269,6 +269,7 @@ mod tests { oracle, bstop_rate: 0_200_000_000, status: 0, + max_positions: 5, }; e.as_contract(&pool, || { storage::set_pool_config(&e, &pool_config); @@ -317,6 +318,7 @@ mod tests { oracle, bstop_rate: 0_200_000_000, status: 0, + max_positions: 4, }; e.as_contract(&pool, || { storage::set_pool_config(&e, &pool_config); @@ -365,6 +367,7 @@ mod tests { oracle, bstop_rate: 0, status: 0, + max_positions: 4, }; e.as_contract(&pool, || { storage::set_pool_config(&e, &pool_config); @@ -413,6 +416,7 @@ mod tests { oracle, bstop_rate: 0_200_000_000, status: 0, + max_positions: 4, }; e.as_contract(&pool, || { storage::set_pool_config(&e, &pool_config); diff --git a/pool/src/pool/status.rs b/pool/src/pool/status.rs index 8b0dd0c5..fca8df59 100644 --- a/pool/src/pool/status.rs +++ b/pool/src/pool/status.rs @@ -193,6 +193,7 @@ mod tests { oracle: oracle_id, bstop_rate: 0, status: 1, + max_positions: 4, }; e.as_contract(&pool_id, || { storage::set_admin(&e, &bombadil); @@ -240,6 +241,7 @@ mod tests { oracle: oracle_id, bstop_rate: 0, status: 1, + max_positions: 4, }; e.as_contract(&pool_id, || { storage::set_admin(&e, &bombadil); @@ -285,6 +287,7 @@ mod tests { oracle: oracle_id, bstop_rate: 0, status: 2, + max_positions: 4, }; e.as_contract(&pool_id, || { storage::set_admin(&e, &bombadil); @@ -327,6 +330,7 @@ mod tests { oracle: oracle_id, bstop_rate: 0, status: 1, + max_positions: 4, }; e.as_contract(&pool_id, || { storage::set_admin(&e, &bombadil); @@ -375,6 +379,7 @@ mod tests { oracle: oracle_id, bstop_rate: 0, status: 5, + max_positions: 4, }; e.as_contract(&pool_id, || { storage::set_admin(&e, &bombadil); @@ -419,6 +424,7 @@ mod tests { oracle: oracle_id, bstop_rate: 0, status: 6, + max_positions: 4, }; e.as_contract(&pool_id, || { storage::set_admin(&e, &bombadil); @@ -461,6 +467,7 @@ mod tests { oracle: oracle_id, bstop_rate: 0, status: 1, + max_positions: 4, }; e.as_contract(&pool_id, || { storage::set_admin(&e, &bombadil); @@ -507,6 +514,7 @@ mod tests { oracle: oracle_id, bstop_rate: 0, status: 2, + max_positions: 4, }; e.as_contract(&pool_id, || { storage::set_admin(&e, &bombadil); @@ -550,6 +558,7 @@ mod tests { oracle: oracle_id, bstop_rate: 0, status: 3, + max_positions: 4, }; e.as_contract(&pool_id, || { storage::set_admin(&e, &bombadil); @@ -597,6 +606,7 @@ mod tests { oracle: oracle_id, bstop_rate: 0, status: 0, + max_positions: 4, }; e.as_contract(&pool_id, || { storage::set_admin(&e, &bombadil); @@ -644,6 +654,7 @@ mod tests { oracle: oracle_id, bstop_rate: 0, status: 1, + max_positions: 4, }; e.as_contract(&pool_id, || { storage::set_admin(&e, &bombadil); @@ -692,6 +703,7 @@ mod tests { oracle: oracle_id, bstop_rate: 0, status: 1, + max_positions: 4, }; e.as_contract(&pool_id, || { storage::set_admin(&e, &bombadil); @@ -740,6 +752,7 @@ mod tests { oracle: oracle_id, bstop_rate: 0, status: 0, + max_positions: 4, }; e.as_contract(&pool_id, || { storage::set_admin(&e, &bombadil); @@ -788,6 +801,7 @@ mod tests { oracle: oracle_id, bstop_rate: 0, status: 0, + max_positions: 4, }; e.as_contract(&pool_id, || { storage::set_admin(&e, &bombadil); @@ -836,6 +850,7 @@ mod tests { oracle: oracle_id, bstop_rate: 0, status: 1, + max_positions: 4, }; e.as_contract(&pool_id, || { storage::set_admin(&e, &bombadil); @@ -883,6 +898,7 @@ mod tests { oracle: oracle_id, bstop_rate: 0, status: 2, + max_positions: 4, }; e.as_contract(&pool_id, || { storage::set_admin(&e, &bombadil); @@ -931,6 +947,7 @@ mod tests { oracle: oracle_id, bstop_rate: 0, status: 2, + max_positions: 4, }; e.as_contract(&pool_id, || { storage::set_admin(&e, &bombadil); @@ -979,6 +996,7 @@ mod tests { oracle: oracle_id, bstop_rate: 0, status: 4, + max_positions: 4, }; e.as_contract(&pool_id, || { storage::set_admin(&e, &bombadil); @@ -1023,6 +1041,7 @@ mod tests { oracle: oracle_id, bstop_rate: 0, status: 6, + max_positions: 4, }; e.as_contract(&pool_id, || { storage::set_admin(&e, &bombadil); @@ -1068,6 +1087,7 @@ mod tests { oracle: oracle_id, bstop_rate: 0, status: 5, + max_positions: 4, }; e.as_contract(&pool_id, || { storage::set_admin(&e, &bombadil); diff --git a/pool/src/pool/submit.rs b/pool/src/pool/submit.rs index 4b93a4ec..810ea590 100644 --- a/pool/src/pool/submit.rs +++ b/pool/src/pool/submit.rs @@ -41,9 +41,6 @@ pub fn execute_submit( TokenClient::new(e, &address).transfer(spender, &e.current_contract_address(), &amount); } - // ensure user is under max positions - new_from_state.positions.require_under_max(e); - // store updated info to ledger pool.store_cached_reserves(e); new_from_state.store(e); @@ -121,10 +118,10 @@ mod tests { oracle, bstop_rate: 0_100_000_000, status: 0, + max_positions: 2, }; e.as_contract(&pool, || { e.mock_all_auths_allowing_non_root_auth(); - storage::set_max_positions(&e, &2); storage::set_pool_config(&e, &pool_config); let pre_pool_balance_0 = underlying_0_client.balance(&pool); @@ -164,79 +161,7 @@ mod tests { assert_eq!(underlying_1_client.balance(&merry), 1_5000000); }); } - #[test] - #[should_panic(expected = "Error(Contract, #13)")] - fn test_submit_requires_positions_under_max() { - let e = Env::default(); - e.budget().reset_unlimited(); - e.mock_all_auths_allowing_non_root_auth(); - - e.ledger().set(LedgerInfo { - timestamp: 600, - protocol_version: 20, - sequence_number: 1234, - 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 frodo = Address::generate(&e); - let merry = Address::generate(&e); - let pool = testutils::create_pool(&e); - let (oracle, oracle_client) = testutils::create_mock_oracle(&e); - - let (underlying_0, underlying_0_client) = testutils::create_token_contract(&e, &bombadil); - let (reserve_config, reserve_data) = testutils::default_reserve_meta(); - testutils::create_reserve(&e, &pool, &underlying_0, &reserve_config, &reserve_data); - - let (underlying_1, _) = testutils::create_token_contract(&e, &bombadil); - let (reserve_config, reserve_data) = testutils::default_reserve_meta(); - testutils::create_reserve(&e, &pool, &underlying_1, &reserve_config, &reserve_data); - - underlying_0_client.mint(&frodo, &16_0000000); - - oracle_client.set_data( - &bombadil, - &Asset::Other(Symbol::new(&e, "USD")), - &vec![ - &e, - Asset::Stellar(underlying_0.clone()), - Asset::Stellar(underlying_1.clone()), - ], - &7, - &300, - ); - oracle_client.set_price_stable(&vec![&e, 1_0000000, 5_0000000]); - - let pool_config = PoolConfig { - oracle, - bstop_rate: 0_100_000_000, - status: 0, - }; - e.as_contract(&pool, || { - e.mock_all_auths_allowing_non_root_auth(); - storage::set_pool_config(&e, &pool_config); - - let requests = vec![ - &e, - Request { - request_type: 2, - address: underlying_0, - amount: 15_0000000, - }, - Request { - request_type: 4, - address: underlying_1, - amount: 1_5000000, - }, - ]; - execute_submit(&e, &samwise, &frodo, &merry, requests); - }); - } + #[test] #[should_panic(expected = "Error(Contract, #10)")] fn test_submit_requires_healhty() { @@ -287,6 +212,7 @@ mod tests { oracle, bstop_rate: 0_100_000_000, status: 0, + max_positions: 2, }; e.as_contract(&pool, || { storage::set_pool_config(&e, &pool_config); diff --git a/pool/src/pool/user.rs b/pool/src/pool/user.rs index 2d969a18..d755ce2f 100644 --- a/pool/src/pool/user.rs +++ b/pool/src/pool/user.rs @@ -1,6 +1,6 @@ -use soroban_sdk::{contracttype, panic_with_error, Address, Env, Map}; +use soroban_sdk::{contracttype, Address, Env, Map}; -use crate::{emissions, storage, validator::require_nonnegative, PoolError}; +use crate::{emissions, storage, validator::require_nonnegative}; use super::{Pool, Reserve}; @@ -22,11 +22,6 @@ impl Positions { supply: Map::new(e), } } - pub fn require_under_max(&self, e: &Env) { - if storage::get_max_positions(e) < self.liabilities.len() + self.collateral.len() as u32 { - panic_with_error!(e, PoolError::MaxPositionsExceeded) - } - } } /// A user / contracts position's with the pool @@ -860,61 +855,4 @@ mod tests { assert_eq!(user.get_total_supply(1), 456 + 789); }); } - - #[test] - fn test_require_under_max_passes() { - 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, || { - storage::set_max_positions(&e, &1); - user.add_collateral(&e, &mut reserve_0, 123); - user.positions.require_under_max(&e); - }); - } - #[test] - #[should_panic(expected = "Error(Contract, #13)")] - fn test_require_under_max_fails() { - 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, || { - storage::set_max_positions(&e, &1); - user.add_collateral(&e, &mut reserve_0, 123); - user.add_liabilities(&e, &mut reserve_0, 789); - user.positions.require_under_max(&e); - }); - } - #[test] - #[should_panic(expected = "Error(Contract, #13)")] - fn test_require_under_max_fails_unset() { - 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, || { - user.add_collateral(&e, &mut reserve_0, 123); - user.positions.require_under_max(&e); - }); - } } diff --git a/pool/src/storage.rs b/pool/src/storage.rs index dbdb3f2a..b3bceb4f 100644 --- a/pool/src/storage.rs +++ b/pool/src/storage.rs @@ -20,6 +20,7 @@ pub struct PoolConfig { pub oracle: Address, pub bstop_rate: u64, // the rate the backstop takes on accrued debt interest, expressed in 9 decimals pub status: u32, + pub max_positions: u32, } /// The pool's emission config @@ -94,7 +95,6 @@ pub struct UserEmissionData { /********** Storage Key Types **********/ const ADMIN_KEY: &str = "Admin"; -const MAX_POSITIONS_KEY: &str = "MaxPos"; const NAME_KEY: &str = "Name"; const BACKSTOP_KEY: &str = "Backstop"; const BLND_TOKEN_KEY: &str = "BLNDTkn"; @@ -228,26 +228,6 @@ pub fn has_admin(e: &Env) -> bool { e.storage().instance().has(&Symbol::new(e, ADMIN_KEY)) } -/*********** Max Positions ***********/ -// Fetch the current max posoitions -/// -pub fn get_max_positions(e: &Env) -> u32 { - e.storage() - .instance() - .get(&Symbol::new(e, MAX_POSITIONS_KEY)) - .unwrap_or(0) -} - -/// Set a new admin -/// -/// ### Arguments -/// * `max_positions` - The max positions for the pool -pub fn set_max_positions(e: &Env, max_positions: &u32) { - e.storage() - .instance() - .set::(&Symbol::new(e, MAX_POSITIONS_KEY), max_positions); -} - /********** Metadata **********/ /// Set a pool name diff --git a/test-suites/src/setup.rs b/test-suites/src/setup.rs index 288e9785..b26ca669 100644 --- a/test-suites/src/setup.rs +++ b/test-suites/src/setup.rs @@ -33,7 +33,7 @@ pub fn create_fixture_with_data<'a>(wasm: bool) -> TestFixture<'a> { ); // create pool - fixture.create_pool(Symbol::new(&fixture.env, "Teapot"), 0_100_000_000); + fixture.create_pool(Symbol::new(&fixture.env, "Teapot"), 0_100_000_000, 6); let mut stable_config = default_reserve_metadata(); stable_config.decimals = 6; @@ -73,9 +73,6 @@ pub fn create_fixture_with_data<'a>(wasm: bool) -> TestFixture<'a> { ]; pool_fixture.pool.set_emissions_config(&reserve_emissions); - // set max positions for pool - pool_fixture.pool.set_max_positions(&6); - // deposit into backstop, add to reward zone fixture .backstop @@ -227,7 +224,6 @@ mod tests { let fixture = create_fixture_with_data(false); let frodo = fixture.users.get(0).unwrap(); let pool_fixture: &PoolFixture = fixture.pools.get(0).unwrap(); - // validate backstop deposit assert_eq!( 50_000 * SCALAR_7, diff --git a/test-suites/src/test_fixture.rs b/test-suites/src/test_fixture.rs index 8cc4a87b..15282558 100644 --- a/test-suites/src/test_fixture.rs +++ b/test-suites/src/test_fixture.rs @@ -167,13 +167,14 @@ impl TestFixture<'_> { fixture } - pub fn create_pool(&mut self, name: Symbol, backstop_take_rate: u64) { + pub fn create_pool(&mut self, name: Symbol, backstop_take_rate: u64, max_positions: u32) { let pool_id = self.pool_factory.deploy( &self.bombadil, &name, &BytesN::<32>::random(&self.env), &self.oracle.address, &backstop_take_rate, + &max_positions, ); self.pools.push(PoolFixture { pool: PoolClient::new(&self.env, &pool_id), diff --git a/test-suites/tests/test_pool.rs b/test-suites/tests/test_pool.rs index 9f76c99d..9bd8866e 100644 --- a/test-suites/tests/test_pool.rs +++ b/test-suites/tests/test_pool.rs @@ -545,6 +545,7 @@ fn test_pool_config() { &Symbol::new(&fixture.env, "teapot"), &Address::generate(&fixture.env), &10000, + &4, &Address::generate(&fixture.env), &Address::generate(&fixture.env), &Address::generate(&fixture.env), @@ -552,7 +553,13 @@ fn test_pool_config() { assert!(result.is_err()); // Update pool config (admin only) - pool_fixture.pool.update_pool(&0_050_000_000); + pool_fixture.pool.update_pool(&0_050_000_000, &6); + let backstop_take_rate: u64 = 0_050_000_000u64; + let event_data: soroban_sdk::Vec = vec![ + &fixture.env, + backstop_take_rate.into_val(&fixture.env), + 6u32.into_val(&fixture.env), + ]; assert_eq!( fixture.env.auths()[0], ( @@ -561,7 +568,7 @@ fn test_pool_config() { function: AuthorizedFunction::Contract(( pool_fixture.pool.address.clone(), Symbol::new(&fixture.env, "update_pool"), - vec![&fixture.env, 0_050_000_000u64.into_val(&fixture.env)] + event_data.into_val(&fixture.env) )), sub_invocations: std::vec![] } @@ -581,7 +588,7 @@ fn test_pool_config() { fixture.bombadil.clone() ) .into_val(&fixture.env), - 0_050_000_000u64.into_val(&fixture.env) + event_data.into_val(&fixture.env) ) ] ); From f37f3823103ea4a88d1d2e164f19cddc7f352a24 Mon Sep 17 00:00:00 2001 From: mootz12 Date: Fri, 29 Dec 2023 14:10:38 -0500 Subject: [PATCH 4/4] pool: chore: move max positions check logic to once per request process --- pool/src/pool/actions.rs | 86 +++++++++++++++++++++++++++++-- pool/src/pool/pool.rs | 107 +++++++++++++++++++-------------------- pool/src/pool/submit.rs | 2 +- pool/src/pool/user.rs | 8 +++ 4 files changed, 143 insertions(+), 60 deletions(-) diff --git a/pool/src/pool/actions.rs b/pool/src/pool/actions.rs index d9933da5..17cc192a 100644 --- a/pool/src/pool/actions.rs +++ b/pool/src/pool/actions.rs @@ -71,6 +71,7 @@ pub fn build_actions_from_request( ) -> (Actions, User, bool) { let mut actions = Actions::new(e); let mut from_state = User::load(e, from); + let prev_positions_count = from_state.positions.effective_count(); let mut check_health = false; for request in requests.iter() { // verify the request is allowed @@ -280,9 +281,11 @@ pub fn build_actions_from_request( } _ => panic_with_error!(e, PoolError::BadRequest), } - // Verify max positions haven't been exceeded - pool.require_under_max(e, from_state.positions.clone(), request.request_type) } + + // Verify max positions haven't been exceeded + pool.require_under_max(e, &from_state.positions, prev_positions_count); + (actions, from_state, check_health) } @@ -1133,7 +1136,7 @@ mod tests { oracle: oracle_address, bstop_rate: 0_100_000_000, status: 0, - max_positions: 2, + max_positions: 4, }; let positions: Positions = Positions { collateral: map![ @@ -1515,6 +1518,69 @@ mod tests { assert_eq!(actions.spender_transfer.len(), 0); }); } + + /********** positions_under_max **********/ + + #[test] + fn test_actions_requires_positions_under_max_with_decrease() { + let e = Env::default(); + e.mock_all_auths(); + + let bombadil = Address::generate(&e); + let samwise = Address::generate(&e); + let pool = testutils::create_pool(&e); + + let (underlying, _) = testutils::create_token_contract(&e, &bombadil); + let (reserve_config, reserve_data) = testutils::default_reserve_meta(); + testutils::create_reserve(&e, &pool, &underlying, &reserve_config, &reserve_data); + + let (underlying_1, _) = testutils::create_token_contract(&e, &bombadil); + let (reserve_config, reserve_data) = testutils::default_reserve_meta(); + testutils::create_reserve(&e, &pool, &underlying_1, &reserve_config, &reserve_data); + + e.ledger().set(LedgerInfo { + timestamp: 600, + protocol_version: 20, + sequence_number: 1234, + 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), + bstop_rate: 0_200_000_000, + status: 0, + max_positions: 2, + }; + + let user_positions = Positions { + liabilities: map![&e, (0, 5_0000000), (1, 1_0000000)], + collateral: map![&e, (0, 20_0000000), (1, 10)], + supply: map![&e], + }; + e.as_contract(&pool, || { + storage::set_pool_config(&e, &pool_config); + storage::set_user_positions(&e, &samwise, &user_positions); + + let mut pool = Pool::load(&e); + + let requests = vec![ + &e, + Request { + request_type: 3, + address: underlying_1.clone(), + amount: 20, + }, + ]; + + let (_, user, _) = build_actions_from_request(&e, &mut pool, &samwise, requests); + assert_eq!(user.positions.effective_count(), 3) + }); + } + #[test] #[should_panic(expected = "Error(Contract, #13)")] fn test_actions_requires_positions_under_max() { @@ -1528,6 +1594,7 @@ mod tests { let (underlying, _) = testutils::create_token_contract(&e, &bombadil); let (reserve_config, reserve_data) = testutils::default_reserve_meta(); testutils::create_reserve(&e, &pool, &underlying, &reserve_config, &reserve_data); + e.ledger().set(LedgerInfo { timestamp: 600, protocol_version: 20, @@ -1538,14 +1605,22 @@ mod tests { min_persistent_entry_ttl: 10, max_entry_ttl: 2000000, }); + let pool_config = PoolConfig { oracle: Address::generate(&e), bstop_rate: 0_200_000_000, status: 0, - max_positions: 0, + max_positions: 1, + }; + + let user_positions = Positions { + liabilities: map![&e], + collateral: map![&e, (0, 20_0000000)], + supply: map![&e], }; e.as_contract(&pool, || { storage::set_pool_config(&e, &pool_config); + storage::set_user_positions(&e, &samwise, &user_positions); let mut pool = Pool::load(&e); @@ -1554,9 +1629,10 @@ mod tests { Request { request_type: 4, address: underlying.clone(), - amount: 10_1234567, + amount: 1_0000000, }, ]; + build_actions_from_request(&e, &mut pool, &samwise, requests); }); } diff --git a/pool/src/pool/pool.rs b/pool/src/pool/pool.rs index 02c0319d..bdb8eb11 100644 --- a/pool/src/pool/pool.rs +++ b/pool/src/pool/pool.rs @@ -76,16 +76,18 @@ impl Pool { } } - /// Require that an action does not violate the maximum number of positions, or panic. + /// Require that a position does not violate the maximum number of positions, or panic. /// /// ### Arguments /// * `positions` - The user's positions - /// * `action_type` - The type of action being performed - pub fn require_under_max(&self, e: &Env, positions: Positions, action_type: u32) { - if self.config.max_positions - < positions.liabilities.len() + positions.collateral.len() as u32 - && (action_type == 2 || action_type == 4) - { + /// * `previous_num` - The number of positions the user previously had + /// + /// ### Panics + /// If the user has more positions than the maximum allowed and they are not + /// decreasing their number of positions + pub fn require_under_max(&self, e: &Env, positions: &Positions, previous_num: u32) { + let new_num = positions.effective_count(); + if new_num > previous_num && self.config.max_positions < new_num { panic_with_error!(e, PoolError::MaxPositionsExceeded) } } @@ -541,8 +543,9 @@ mod tests { assert!(false); }); } + #[test] - fn test_require_under_max_passes() { + fn test_require_under_max_empty() { let e = Env::default(); let samwise = Address::generate(&e); let pool = testutils::create_pool(&e); @@ -560,105 +563,97 @@ mod tests { max_positions: 2, }; e.as_contract(&pool, || { - user.add_collateral(&e, &mut reserve_0, 123); storage::set_pool_config(&e, &pool_config); + let prev_positions = user.positions.effective_count(); + let pool = Pool::load(&e); + user.add_collateral(&e, &mut reserve_0, 1); - pool.require_under_max(&e, user.positions, 4); + pool.require_under_max(&e, &user.positions, prev_positions); }); } + #[test] - #[should_panic(expected = "Error(Contract, #13)")] - fn test_require_under_max_fails_borrow() { + fn test_require_under_max_ignores_supply() { 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 reserve_1 = testutils::default_reserve(&e); + reserve_1.index = 1; + let (oracle, _) = testutils::create_mock_oracle(&e); let mut user = User { address: samwise.clone(), positions: Positions::env_default(&e), }; - let (oracle, _) = testutils::create_mock_oracle(&e); let pool_config = PoolConfig { oracle, bstop_rate: 0_200_000_000, status: 0, - max_positions: 1, + max_positions: 2, }; e.as_contract(&pool, || { storage::set_pool_config(&e, &pool_config); - user.add_collateral(&e, &mut reserve_0, 123); - user.add_liabilities(&e, &mut reserve_0, 789); + user.add_supply(&e, &mut reserve_0, 42); + user.add_supply(&e, &mut reserve_1, 42); + user.add_collateral(&e, &mut reserve_1, 1); + let prev_positions = user.positions.effective_count(); + let pool = Pool::load(&e); + user.add_liabilities(&e, &mut reserve_1, 2); - pool.require_under_max(&e, user.positions, 4); + pool.require_under_max(&e, &user.positions, prev_positions); }); } - #[test] - #[should_panic(expected = "Error(Contract, #13)")] - fn test_require_under_max_fails_deposit() { - 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), - }; - let (oracle, _) = testutils::create_mock_oracle(&e); - let pool_config = PoolConfig { - oracle, - bstop_rate: 0_200_000_000, - status: 0, - max_positions: 1, - }; - e.as_contract(&pool, || { - storage::set_pool_config(&e, &pool_config); - let pool = Pool::load(&e); - user.add_collateral(&e, &mut reserve_0, 123); - user.add_liabilities(&e, &mut reserve_0, 789); - pool.require_under_max(&e, user.positions, 2); - }); - } #[test] - fn test_require_under_max_passes_withdraw() { + fn test_require_under_max_allows_decreasing_change() { 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 reserve_1 = testutils::default_reserve(&e); + reserve_1.index = 1; + let (oracle, _) = testutils::create_mock_oracle(&e); let mut user = User { address: samwise.clone(), positions: Positions::env_default(&e), }; - let (oracle, _) = testutils::create_mock_oracle(&e); let pool_config = PoolConfig { oracle, bstop_rate: 0_200_000_000, status: 0, - max_positions: 1, + max_positions: 2, }; e.as_contract(&pool, || { storage::set_pool_config(&e, &pool_config); + user.add_collateral(&e, &mut reserve_0, 42); + user.add_collateral(&e, &mut reserve_1, 42); + user.add_liabilities(&e, &mut reserve_0, 123); + user.add_liabilities(&e, &mut reserve_1, 123); + let prev_positions = user.positions.effective_count(); + let pool = Pool::load(&e); - user.add_collateral(&e, &mut reserve_0, 123); - user.add_liabilities(&e, &mut reserve_0, 789); + user.remove_collateral(&e, &mut reserve_1, 42); - pool.require_under_max(&e, user.positions, 3); + pool.require_under_max(&e, &user.positions, prev_positions); }); } + #[test] - fn test_require_under_max_passes_repay() { + #[should_panic(expected = "Error(Contract, #13)")] + fn test_require_under_max_panics_if_over() { 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 reserve_1 = testutils::default_reserve(&e); + reserve_1.index = 1; let mut user = User { address: samwise.clone(), @@ -669,14 +664,18 @@ mod tests { oracle, bstop_rate: 0_200_000_000, status: 0, - max_positions: 1, + max_positions: 2, }; e.as_contract(&pool, || { storage::set_pool_config(&e, &pool_config); - let pool = Pool::load(&e); user.add_collateral(&e, &mut reserve_0, 123); user.add_liabilities(&e, &mut reserve_0, 789); - pool.require_under_max(&e, user.positions, 5); + let prev_positions = user.positions.effective_count(); + + let pool = Pool::load(&e); + user.add_liabilities(&e, &mut reserve_1, 42); + + pool.require_under_max(&e, &user.positions, prev_positions); }); } } diff --git a/pool/src/pool/submit.rs b/pool/src/pool/submit.rs index 810ea590..f28dbe75 100644 --- a/pool/src/pool/submit.rs +++ b/pool/src/pool/submit.rs @@ -161,7 +161,7 @@ mod tests { assert_eq!(underlying_1_client.balance(&merry), 1_5000000); }); } - + #[test] #[should_panic(expected = "Error(Contract, #10)")] fn test_submit_requires_healhty() { diff --git a/pool/src/pool/user.rs b/pool/src/pool/user.rs index d755ce2f..0244150b 100644 --- a/pool/src/pool/user.rs +++ b/pool/src/pool/user.rs @@ -22,6 +22,14 @@ impl Positions { supply: Map::new(e), } } + + /// Get the number of effective (impacts health factor) posiitons the user holds. + /// + /// This function ignores non-collateralized supply positions, as they are not relevant to the + /// max number of allowed positions by the pool. + pub fn effective_count(&self) -> u32 { + self.liabilities.len() + self.collateral.len() + } } /// A user / contracts position's with the pool