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 ba30d477..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, @@ -52,10 +54,11 @@ pub trait 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 /// @@ -231,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, @@ -243,6 +247,7 @@ impl Pool for PoolContract { &name, &oracle, &bstop_rate, + &max_postions, &backstop_id, &blnd_id, &usdc_id, @@ -253,6 +258,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); @@ -260,15 +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); + pool::execute_update_pool(&e, backstop_take_rate, max_positions); - e.events() - .publish((Symbol::new(&e, "update_pool"), admin), backstop_take_rate); + 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/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/actions.rs b/pool/src/pool/actions.rs index deeec137..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 @@ -281,6 +282,10 @@ 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, prev_positions_count); + (actions, from_state, check_health) } @@ -332,6 +337,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 +406,7 @@ mod tests { oracle: Address::generate(&e), bstop_rate: 0_200_000_000, status: 0, + max_positions: 2, }; let user_positions = Positions { @@ -473,6 +480,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 +551,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 +623,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 +696,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 +766,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 +831,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 +905,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 +985,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 +1136,7 @@ mod tests { oracle: oracle_address, bstop_rate: 0_100_000_000, status: 0, + max_positions: 4, }; let positions: Positions = Positions { collateral: map![ @@ -1244,6 +1260,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 +1390,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 +1474,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 +1518,122 @@ 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() { + 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: 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); + + let requests = vec![ + &e, + Request { + request_type: 4, + address: underlying.clone(), + amount: 1_0000000, + }, + ]; + + 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..bdb8eb11 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,22 @@ impl Pool { } } + /// Require that a position does not violate the maximum number of positions, or panic. + /// + /// ### Arguments + /// * `positions` - The user's positions + /// * `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) + } + } + /// 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 +134,7 @@ mod tests { Symbol, }; - use crate::{storage::ReserveData, testutils}; + use crate::{pool::User, storage::ReserveData, testutils}; use super::*; @@ -149,6 +166,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 +232,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 +287,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 +307,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 +328,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 +348,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 +369,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 +390,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 +410,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 +442,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 +481,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 +533,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 +543,139 @@ mod tests { assert!(false); }); } + + #[test] + fn test_require_under_max_empty() { + 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, || { + 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, prev_positions); + }); + } + + #[test] + 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 pool_config = PoolConfig { + oracle, + bstop_rate: 0_200_000_000, + status: 0, + max_positions: 2, + }; + e.as_contract(&pool, || { + storage::set_pool_config(&e, &pool_config); + 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, prev_positions); + }); + } + + #[test] + 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 pool_config = PoolConfig { + oracle, + bstop_rate: 0_200_000_000, + status: 0, + 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.remove_collateral(&e, &mut reserve_1, 42); + + pool.require_under_max(&e, &user.positions, prev_positions); + }); + } + + #[test] + #[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(), + 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: 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); + 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/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 bd8464a8..f28dbe75 100644 --- a/pool/src/pool/submit.rs +++ b/pool/src/pool/submit.rs @@ -118,6 +118,7 @@ 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(); @@ -211,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 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 diff --git a/pool/src/storage.rs b/pool/src/storage.rs index bbe742c5..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 diff --git a/test-suites/src/setup.rs b/test-suites/src/setup.rs index 1e98c32f..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; @@ -224,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 61d0cc95..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) ) ] ); @@ -707,6 +714,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,