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] 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