diff --git a/emitter/src/storage.rs b/emitter/src/storage.rs index 59c07249..1f68bfa6 100644 --- a/emitter/src/storage.rs +++ b/emitter/src/storage.rs @@ -101,7 +101,7 @@ pub fn set_queued_swap(e: &Env, swap: &Swap) { ); } -/// Fetch the current queued backstop swap, or None +/// Delete the current queued backstop swap pub fn del_queued_swap(e: &Env) { e.storage().persistent().remove(&Symbol::new(e, SWAP_KEY)); } diff --git a/pool-factory/src/test.rs b/pool-factory/src/test.rs index aabe5228..ced86f29 100644 --- a/pool-factory/src/test.rs +++ b/pool-factory/src/test.rs @@ -48,6 +48,7 @@ fn test_pool_factory() { let name1 = Symbol::new(&e, "pool1"); 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); @@ -91,7 +92,7 @@ fn test_pool_factory() { pool::PoolConfig { oracle: oracle, bstop_rate: backstop_rate, - status: 3 + status: 6 } ); assert_eq!( diff --git a/pool/src/constants.rs b/pool/src/constants.rs index 1eb11ab6..95ee29f3 100644 --- a/pool/src/constants.rs +++ b/pool/src/constants.rs @@ -8,3 +8,6 @@ pub const SCALAR_7: i128 = 1_0000000; // seconds per year pub const SECONDS_PER_YEAR: i128 = 31536000; + +// approximate week in blocks assuming 5 seconds per block +pub const SECONDS_PER_WEEK: u64 = 604800; diff --git a/pool/src/contract.rs b/pool/src/contract.rs index eb7f1dfc..ba30d477 100644 --- a/pool/src/contract.rs +++ b/pool/src/contract.rs @@ -57,25 +57,35 @@ pub trait Pool { /// If the caller is not the admin fn update_pool(e: Env, backstop_take_rate: u64); - /// (Admin only) Initialize a reserve in the pool + /// (Admin only) Queues setting data for a reserve in the pool /// /// ### Arguments /// * `asset` - The underlying asset to add as a reserve /// * `config` - The ReserveConfig for the reserve /// /// ### Panics - /// If the caller is not the admin or the reserve is already setup - fn init_reserve(e: Env, asset: Address, metadata: ReserveConfig) -> u32; + /// If the caller is not the admin + fn queue_set_reserve(e: Env, asset: Address, metadata: ReserveConfig); - /// (Admin only) Update a reserve in the pool + /// (Admin only) Cancels the queued set of a reserve in the pool /// /// ### Arguments /// * `asset` - The underlying asset to add as a reserve - /// * `config` - The ReserveConfig for the reserve /// /// ### Panics - /// If the caller is not the admin or the reserve does not exist - fn update_reserve(e: Env, asset: Address, config: ReserveConfig); + /// If the caller is not the admin or the reserve is not queued for initialization + fn cancel_set_reserve(e: Env, asset: Address); + + /// (Admin only) Executes the queued set of a reserve in the pool + /// + /// ### Arguments + /// * `asset` - The underlying asset to add as a reserve + /// + /// ### Panics + /// If the reserve is not queued for initialization + /// or is already setup + /// or has invalid metadata + fn set_reserve(e: Env, asset: Address) -> u32; /// Fetch the positions for an address /// @@ -261,27 +271,36 @@ impl Pool for PoolContract { .publish((Symbol::new(&e, "update_pool"), admin), backstop_take_rate); } - fn init_reserve(e: Env, asset: Address, config: ReserveConfig) -> u32 { + fn queue_set_reserve(e: Env, asset: Address, metadata: ReserveConfig) { storage::extend_instance(&e); let admin = storage::get_admin(&e); admin.require_auth(); - let index = pool::initialize_reserve(&e, &asset, &config); + pool::execute_queue_set_reserve(&e, &asset, &metadata); - e.events() - .publish((Symbol::new(&e, "init_reserve"), admin), (asset, index)); - index + e.events().publish( + (Symbol::new(&e, "queue_set_reserve"), admin), + (asset, metadata), + ); } - fn update_reserve(e: Env, asset: Address, config: ReserveConfig) { + fn cancel_set_reserve(e: Env, asset: Address) { storage::extend_instance(&e); let admin = storage::get_admin(&e); admin.require_auth(); - pool::execute_update_reserve(&e, &asset, &config); + pool::execute_cancel_queued_set_reserve(&e, &asset); + + e.events() + .publish((Symbol::new(&e, "cancel_set_reserve"), admin), asset); + } + + fn set_reserve(e: Env, asset: Address) -> u32 { + let index = pool::execute_set_queued_reserve(&e, &asset); e.events() - .publish((Symbol::new(&e, "update_reserve"), admin), asset); + .publish((Symbol::new(&e, "set_reserve"),), (asset, index)); + index } fn get_positions(e: Env, address: Address) -> Positions { diff --git a/pool/src/errors.rs b/pool/src/errors.rs index 6c89b029..dc71732d 100644 --- a/pool/src/errors.rs +++ b/pool/src/errors.rs @@ -12,6 +12,7 @@ pub enum PoolError { NegativeAmount = 4, InvalidPoolInitArgs = 5, InvalidReserveMetadata = 6, + InitNotUnlocked = 7, StatusNotAllowed = 8, // Pool State Errors (10-19) InvalidHf = 10, diff --git a/pool/src/pool/config.rs b/pool/src/pool/config.rs index b12a3005..f0966f14 100644 --- a/pool/src/pool/config.rs +++ b/pool/src/pool/config.rs @@ -1,6 +1,7 @@ use crate::{ + constants::SECONDS_PER_WEEK, errors::PoolError, - storage::{self, PoolConfig, ReserveConfig, ReserveData}, + storage::{self, PoolConfig, QueuedReserveInit, ReserveConfig, ReserveData}, }; use soroban_sdk::{panic_with_error, Address, Env, Symbol}; @@ -37,7 +38,7 @@ pub fn execute_initialize( &PoolConfig { oracle: oracle.clone(), bstop_rate: *bstop_rate, - status: 3, + status: 6, }, ); storage::set_blnd_token(e, blnd_id); @@ -55,14 +56,80 @@ pub fn execute_update_pool(e: &Env, backstop_take_rate: u64) { storage::set_pool_config(e, &pool_config); } -/// Initialize a reserve for the pool -pub fn initialize_reserve(e: &Env, asset: &Address, config: &ReserveConfig) -> u32 { - if storage::has_res(e, asset) { - panic_with_error!(e, PoolError::AlreadyInitialized); +/// Execute a queueing a reserve initialization for the pool +pub fn execute_queue_set_reserve(e: &Env, asset: &Address, metadata: &ReserveConfig) { + require_valid_reserve_metadata(e, metadata); + let mut unlock_time = e.ledger().timestamp(); + // require a timelock if pool status is not setup + if storage::get_pool_config(e).status != 6 { + unlock_time += SECONDS_PER_WEEK; } + storage::set_queued_reserve_set( + &e, + &QueuedReserveInit { + new_config: metadata.clone(), + unlock_time, + }, + &asset, + ); +} + +/// Execute cancelling a queueing a reserve initialization for the pool +pub fn execute_cancel_queued_set_reserve(e: &Env, asset: &Address) { + storage::del_queued_reserve_set(&e, &asset); +} - require_valid_reserve_metadata(e, config); - let index = storage::push_res_list(e, asset); +/// Execute a queued reserve initialization for the pool +pub fn execute_set_queued_reserve(e: &Env, asset: &Address) -> u32 { + let queued_init = storage::get_queued_reserve_set(e, asset); + + if queued_init.unlock_time > e.ledger().timestamp() { + panic_with_error!(e, PoolError::InitNotUnlocked); + } + + // remove queued reserve + storage::del_queued_reserve_set(e, asset); + + // initialize reserve + initialize_reserve(e, asset, &queued_init.new_config) +} + +/// sets reserve data for the pool +fn initialize_reserve(e: &Env, asset: &Address, config: &ReserveConfig) -> u32 { + let index: u32; + // if reserve already exists, ensure index and scalar do not change + if storage::has_res(e, asset) { + // accrue and store reserve data to the ledger + let pool = Pool::load(e); + let mut reserve = pool.load_reserve(e, asset); + index = reserve.index; + let reserve_config = storage::get_res_config(e, asset); + // decimals cannot change + if reserve_config.decimals != config.decimals { + panic_with_error!(e, PoolError::InvalidReserveMetadata); + } + // if any of the IR parameters were changed reset the IR modifier + if reserve_config.r_one != config.r_one + || reserve_config.r_two != config.r_two + || reserve_config.r_three != config.r_three + || reserve_config.util != config.util + { + reserve.ir_mod = 1_000_000_000; + } + reserve.store(e); + } else { + index = storage::push_res_list(e, asset); + let init_data = ReserveData { + b_rate: 1_000_000_000, + d_rate: 1_000_000_000, + ir_mod: 1_000_000_000, + d_supply: 0, + b_supply: 0, + last_time: e.ledger().timestamp(), + backstop_credit: 0, + }; + storage::set_res_data(e, asset, &init_data); + } let reserve_config = ReserveConfig { index, @@ -77,37 +144,8 @@ pub fn initialize_reserve(e: &Env, asset: &Address, config: &ReserveConfig) -> u reactivity: config.reactivity, }; storage::set_res_config(e, asset, &reserve_config); - let init_data = ReserveData { - b_rate: 1_000_000_000, - d_rate: 1_000_000_000, - ir_mod: 1_000_000_000, - d_supply: 0, - b_supply: 0, - last_time: e.ledger().timestamp(), - backstop_credit: 0, - }; - storage::set_res_data(e, asset, &init_data); - index -} - -/// Update a reserve in the pool -pub fn execute_update_reserve(e: &Env, asset: &Address, config: &ReserveConfig) { - require_valid_reserve_metadata(e, config); - - let pool = Pool::load(e); - if pool.config.status == 2 { - panic_with_error!(e, PoolError::InvalidPoolStatus); - } - // accrue and store reserve data to the ledger - let reserve = pool.load_reserve(e, asset); - reserve.store(e); - - // force index to remain constant and only allow metadata based changes - let mut new_config = config.clone(); - new_config.index = reserve.index; - - storage::set_res_config(e, asset, &new_config); + index } #[allow(clippy::zero_prefixed_literal)] @@ -126,6 +164,7 @@ fn require_valid_reserve_metadata(e: &Env, metadata: &ReserveConfig) { #[cfg(test)] mod tests { + use crate::storage::QueuedReserveInit; use crate::testutils; use super::*; @@ -160,7 +199,7 @@ mod tests { let pool_config = storage::get_pool_config(&e); assert_eq!(pool_config.oracle, oracle); assert_eq!(pool_config.bstop_rate, bstop_rate); - assert_eq!(pool_config.status, 3); + assert_eq!(pool_config.status, 6); assert_eq!(storage::get_backstop(&e), backstop_address); assert_eq!(storage::get_blnd_token(&e), blnd_id); assert_eq!(storage::get_usdc_token(&e), usdc_id); @@ -206,15 +245,57 @@ mod tests { execute_update_pool(&e, 1_000_000_000u64); }); } + #[test] + fn test_queue_initial_reserve() { + let e = Env::default(); + let pool = testutils::create_pool(&e); + let bombadil = Address::generate(&e); + + let (asset_id_0, _) = testutils::create_token_contract(&e, &bombadil); + + let metadata = ReserveConfig { + index: 0, + decimals: 7, + c_factor: 0_7500000, + l_factor: 0_7500000, + util: 0_5000000, + max_util: 0_9500000, + r_one: 0_0500000, + r_two: 0_5000000, + r_three: 1_5000000, + reactivity: 100, + }; + let pool_config = PoolConfig { + oracle: Address::generate(&e), + bstop_rate: 0_100_000_000, + status: 6, + }; + e.as_contract(&pool, || { + storage::set_pool_config(&e, &pool_config); + execute_queue_set_reserve(&e, &asset_id_0, &metadata); + let queued_res = storage::get_queued_reserve_set(&e, &asset_id_0); + let res_config_0 = queued_res.new_config; + assert_eq!(queued_res.unlock_time, e.ledger().timestamp()); + assert_eq!(res_config_0.decimals, metadata.decimals); + assert_eq!(res_config_0.c_factor, metadata.c_factor); + assert_eq!(res_config_0.l_factor, metadata.l_factor); + assert_eq!(res_config_0.util, metadata.util); + assert_eq!(res_config_0.max_util, metadata.max_util); + assert_eq!(res_config_0.r_one, metadata.r_one); + assert_eq!(res_config_0.r_two, metadata.r_two); + assert_eq!(res_config_0.r_three, metadata.r_three); + assert_eq!(res_config_0.reactivity, metadata.reactivity); + assert_eq!(res_config_0.index, 0); + }); + } #[test] - fn test_initialize_reserve() { + fn test_execute_queue_reserve_initialization() { let e = Env::default(); let pool = testutils::create_pool(&e); let bombadil = Address::generate(&e); let (asset_id_0, _) = testutils::create_token_contract(&e, &bombadil); - let (asset_id_1, _) = testutils::create_token_contract(&e, &bombadil); let metadata = ReserveConfig { index: 0, @@ -228,12 +309,96 @@ mod tests { r_three: 1_5000000, reactivity: 100, }; + let pool_config = PoolConfig { + oracle: Address::generate(&e), + bstop_rate: 0_100_000_000, + status: 0, + }; e.as_contract(&pool, || { - initialize_reserve(&e, &asset_id_0, &metadata); + storage::set_pool_config(&e, &pool_config); + execute_queue_set_reserve(&e, &asset_id_0, &metadata); + let queued_init = storage::get_queued_reserve_set(&e, &asset_id_0); + assert_eq!(queued_init.new_config.decimals, metadata.decimals); + assert_eq!(queued_init.new_config.c_factor, metadata.c_factor); + assert_eq!(queued_init.new_config.l_factor, metadata.l_factor); + assert_eq!(queued_init.new_config.util, metadata.util); + assert_eq!(queued_init.new_config.max_util, metadata.max_util); + assert_eq!(queued_init.new_config.r_one, metadata.r_one); + assert_eq!(queued_init.new_config.r_two, metadata.r_two); + assert_eq!(queued_init.new_config.r_three, metadata.r_three); + assert_eq!(queued_init.new_config.reactivity, metadata.reactivity); + assert_eq!(queued_init.new_config.index, 0); + assert_eq!( + queued_init.unlock_time, + e.ledger().timestamp() + SECONDS_PER_WEEK + ); + }); + } + #[test] + #[should_panic(expected = "Error(Storage, MissingValue)")] + fn test_execute_cancel_queued_reserve_initialization() { + let e = Env::default(); + let pool = testutils::create_pool(&e); + let bombadil = Address::generate(&e); - initialize_reserve(&e, &asset_id_1, &metadata); - let res_config_0 = storage::get_res_config(&e, &asset_id_0); - let res_config_1 = storage::get_res_config(&e, &asset_id_1); + let (asset_id_0, _) = testutils::create_token_contract(&e, &bombadil); + + let metadata = ReserveConfig { + index: 0, + decimals: 7, + c_factor: 0_7500000, + l_factor: 0_7500000, + util: 0_5000000, + max_util: 0_9500000, + r_one: 0_0500000, + r_two: 0_5000000, + r_three: 1_5000000, + reactivity: 100, + }; + e.as_contract(&pool, || { + storage::set_queued_reserve_set( + &e, + &QueuedReserveInit { + new_config: metadata.clone(), + unlock_time: e.ledger().timestamp(), + }, + &asset_id_0, + ); + execute_cancel_queued_set_reserve(&e, &asset_id_0); + storage::get_queued_reserve_set(&e, &asset_id_0); + }); + } + #[test] + fn test_execute_initialize_queued_reserve() { + let e = Env::default(); + let pool = testutils::create_pool(&e); + let bombadil = Address::generate(&e); + + let (asset_id_0, _) = testutils::create_token_contract(&e, &bombadil); + + let metadata = ReserveConfig { + index: 0, + decimals: 7, + c_factor: 0_7500000, + l_factor: 0_7500000, + util: 0_5000000, + max_util: 0_9500000, + r_one: 0_0500000, + r_two: 0_5000000, + r_three: 1_5000000, + reactivity: 100, + }; + e.as_contract(&pool, || { + storage::set_queued_reserve_set( + &e, + &QueuedReserveInit { + new_config: metadata.clone(), + unlock_time: e.ledger().timestamp(), + }, + &asset_id_0, + ); + execute_set_queued_reserve(&e, &asset_id_0); + let res_config_0: ReserveConfig = storage::get_res_config(&e, &asset_id_0); assert_eq!(res_config_0.decimals, metadata.decimals); assert_eq!(res_config_0.c_factor, metadata.c_factor); assert_eq!(res_config_0.l_factor, metadata.l_factor); @@ -244,17 +409,49 @@ mod tests { assert_eq!(res_config_0.r_three, metadata.r_three); assert_eq!(res_config_0.reactivity, metadata.reactivity); assert_eq!(res_config_0.index, 0); - assert_eq!(res_config_1.index, 1); }); } + #[test] + #[should_panic(expected = "Error(Contract, #7)")] + fn test_execute_queued_initialize_reserve_requires_block_passed() { + let e = Env::default(); + let pool = testutils::create_pool(&e); + let bombadil = Address::generate(&e); + + let (asset_id_0, _) = testutils::create_token_contract(&e, &bombadil); + let metadata = ReserveConfig { + index: 0, + decimals: 7, + c_factor: 0_7500000, + l_factor: 0_7500000, + util: 0_5000000, + max_util: 0_9500000, + r_one: 0_0500000, + r_two: 0_5000000, + r_three: 1_5000000, + reactivity: 100, + }; + e.as_contract(&pool, || { + storage::set_queued_reserve_set( + &e, + &QueuedReserveInit { + new_config: metadata.clone(), + unlock_time: e.ledger().timestamp() + 1, + }, + &asset_id_0, + ); + execute_set_queued_reserve(&e, &asset_id_0); + }); + } #[test] - #[should_panic(expected = "Error(Contract, #3)")] - fn test_initialize_reserve_blocks_duplicates() { + fn test_initialize_reserve() { let e = Env::default(); let pool = testutils::create_pool(&e); let bombadil = Address::generate(&e); - let (asset_id, _) = testutils::create_token_contract(&e, &bombadil); + + let (asset_id_0, _) = testutils::create_token_contract(&e, &bombadil); + let (asset_id_1, _) = testutils::create_token_contract(&e, &bombadil); let metadata = ReserveConfig { index: 0, @@ -269,16 +466,28 @@ mod tests { reactivity: 100, }; e.as_contract(&pool, || { - initialize_reserve(&e, &asset_id, &metadata); - let res_config = storage::get_res_config(&e, &asset_id); - assert_eq!(res_config.index, 0); - initialize_reserve(&e, &asset_id, &metadata); + initialize_reserve(&e, &asset_id_0, &metadata); + + initialize_reserve(&e, &asset_id_1, &metadata); + let res_config_0 = storage::get_res_config(&e, &asset_id_0); + let res_config_1 = storage::get_res_config(&e, &asset_id_1); + assert_eq!(res_config_0.decimals, metadata.decimals); + assert_eq!(res_config_0.c_factor, metadata.c_factor); + assert_eq!(res_config_0.l_factor, metadata.l_factor); + assert_eq!(res_config_0.util, metadata.util); + assert_eq!(res_config_0.max_util, metadata.max_util); + assert_eq!(res_config_0.r_one, metadata.r_one); + assert_eq!(res_config_0.r_two, metadata.r_two); + assert_eq!(res_config_0.r_three, metadata.r_three); + assert_eq!(res_config_0.reactivity, metadata.reactivity); + assert_eq!(res_config_0.index, 0); + assert_eq!(res_config_1.index, 1); }); } #[test] #[should_panic(expected = "Error(Contract, #6)")] - fn test_initialize_reserve_validates_metadata() { + fn test_queue_set_reserve_validates_metadata() { let e = Env::default(); let pool = testutils::create_pool(&e); let bombadil = Address::generate(&e); @@ -288,7 +497,7 @@ mod tests { index: 0, decimals: 7, c_factor: 0_7500000, - l_factor: 0_7500000, + l_factor: 1_7500000, util: 1_0000000, max_util: 0_9500000, r_one: 0_0500000, @@ -296,11 +505,14 @@ mod tests { r_three: 1_5000000, reactivity: 100, }; + let pool_config = PoolConfig { + oracle: Address::generate(&e), + bstop_rate: 0_100_000_000, + status: 0, + }; e.as_contract(&pool, || { - initialize_reserve(&e, &asset_id, &metadata); - let res_config = storage::get_res_config(&e, &asset_id); - assert_eq!(res_config.index, 0); - initialize_reserve(&e, &asset_id, &metadata); + storage::set_pool_config(&e, &pool_config); + execute_queue_set_reserve(&e, &asset_id, &metadata); }); } @@ -359,8 +571,15 @@ mod tests { storage::set_pool_config(&e, &pool_config); let res_config_old = storage::get_res_config(&e, &underlying); - - execute_update_reserve(&e, &underlying, &new_metadata); + storage::set_queued_reserve_set( + &e, + &QueuedReserveInit { + new_config: new_metadata.clone(), + unlock_time: e.ledger().timestamp(), + }, + &underlying, + ); + execute_set_queued_reserve(&e, &underlying); let res_config_updated = storage::get_res_config(&e, &underlying); assert_eq!(res_config_updated.decimals, new_metadata.decimals); assert_eq!(res_config_updated.c_factor, new_metadata.c_factor); @@ -383,7 +602,7 @@ mod tests { #[test] #[should_panic(expected = "Error(Contract, #6)")] - fn test_execute_update_reserve_validates_metadata() { + fn test_execute_update_reserve_validates_decimals() { let e = Env::default(); e.mock_all_auths(); e.ledger().set(LedgerInfo { @@ -406,7 +625,7 @@ mod tests { let new_metadata = ReserveConfig { index: 99, - decimals: 7, + decimals: 8, c_factor: 0_7500000, l_factor: 0_7500000, util: 1_0777777, @@ -425,7 +644,72 @@ mod tests { e.as_contract(&pool, || { storage::set_pool_config(&e, &pool_config); - execute_update_reserve(&e, &underlying, &new_metadata); + storage::set_queued_reserve_set( + &e, + &QueuedReserveInit { + new_config: new_metadata.clone(), + unlock_time: e.ledger().timestamp(), + }, + &underlying, + ); + execute_set_queued_reserve(&e, &underlying); + }); + } + + #[test] + fn test_execute_update_reserve_resets_ir_mod() { + let e = Env::default(); + e.mock_all_auths(); + e.ledger().set(LedgerInfo { + timestamp: 500, + protocol_version: 20, + sequence_number: 100, + network_id: Default::default(), + base_reserve: 10, + min_temp_entry_ttl: 10, + min_persistent_entry_ttl: 10, + max_entry_ttl: 2000000, + }); + + let pool = testutils::create_pool(&e); + let bombadil = Address::generate(&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 new_metadata = ReserveConfig { + index: 99, + decimals: 7, + c_factor: 0_7500000, + l_factor: 0_7500000, + util: 1_0777777, + max_util: 0_9500000, + r_one: 0_0500000, + r_two: 0_7500000, + r_three: 1_5000000, + reactivity: 105, + }; + + let pool_config = PoolConfig { + oracle: Address::generate(&e), + bstop_rate: 0_100_000_000, + status: 0, + }; + e.as_contract(&pool, || { + storage::set_pool_config(&e, &pool_config); + + storage::set_queued_reserve_set( + &e, + &QueuedReserveInit { + new_config: new_metadata.clone(), + unlock_time: e.ledger().timestamp(), + }, + &underlying, + ); + execute_set_queued_reserve(&e, &underlying); + let res_data = storage::get_res_data(&e, &underlying); + assert_eq!(res_data.ir_mod, 1_000_000_000); }); } diff --git a/pool/src/pool/mod.rs b/pool/src/pool/mod.rs index 83adf8e4..9097e5e1 100644 --- a/pool/src/pool/mod.rs +++ b/pool/src/pool/mod.rs @@ -6,7 +6,8 @@ pub use bad_debt::{burn_backstop_bad_debt, transfer_bad_debt_to_backstop}; mod config; pub use config::{ - execute_initialize, execute_update_pool, execute_update_reserve, initialize_reserve, + execute_cancel_queued_set_reserve, execute_initialize, execute_queue_set_reserve, + execute_set_queued_reserve, execute_update_pool, }; mod health_factor; diff --git a/pool/src/pool/status.rs b/pool/src/pool/status.rs index 283d8da0..8b0dd0c5 100644 --- a/pool/src/pool/status.rs +++ b/pool/src/pool/status.rs @@ -23,6 +23,11 @@ pub fn execute_update_pool_status(e: &Env) -> u32 { } match pool_config.status { + // Setup + 6 => { + // Setup supersedes all other statuses + panic_with_error!(e, PoolError::StatusNotAllowed); + } // Admin frozen 4 => { // Admin frozen supersedes all other statuses @@ -71,10 +76,7 @@ pub fn execute_set_pool_status(e: &Env, pool_status: u32) { let backstop_client = BackstopClient::new(e, &backstop_id); let pool_backstop_data = backstop_client.pool_data(&e.current_contract_address()); - // Admins cannot set non-admin status' - if pool_status % 2 != 0 { - panic_with_error!(e, PoolError::BadRequest); - } + match pool_status { 0 => { // Threshold must be met and q4w must be under 50% for the admin to set Active @@ -94,6 +96,14 @@ pub fn execute_set_pool_status(e: &Env, pool_status: u32) { // Admin On-Ice pool_config.status = 2; } + 3 => { + // Q4w must be under 75% for admin to set permissionless On-Ice + if pool_backstop_data.q4w_pct >= 0_7500000 { + panic_with_error!(e, PoolError::StatusNotAllowed); + } + // Admin On-Ice + pool_config.status = 3; + } 4 => { // Admin can always freeze the pool // Admin Frozen @@ -331,7 +341,7 @@ mod tests { #[test] #[should_panic(expected = "Error(Contract, #8)")] - fn test_set_pool_status_on_ice_blocks_with_too_high_q4w() { + fn test_set_pool_status_admin_on_ice_blocks_with_too_high_q4w() { let e = Env::default(); e.budget().reset_unlimited(); e.mock_all_auths_allowing_non_root_auth(); @@ -364,7 +374,7 @@ mod tests { let pool_config = PoolConfig { oracle: oracle_id, bstop_rate: 0, - status: 2, + status: 5, }; e.as_contract(&pool_id, || { storage::set_admin(&e, &bombadil); @@ -374,6 +384,50 @@ mod tests { }); } #[test] + #[should_panic(expected = "Error(Contract, #8)")] + fn test_set_pool_status_backstop_on_ice_blocks_with_too_high_q4w() { + let e = Env::default(); + e.budget().reset_unlimited(); + e.mock_all_auths_allowing_non_root_auth(); + let pool_id = create_pool(&e); + let oracle_id = Address::generate(&e); + + let bombadil = Address::generate(&e); + let samwise = Address::generate(&e); + + let (blnd, blnd_client) = create_token_contract(&e, &bombadil); + let (usdc, usdc_client) = create_token_contract(&e, &bombadil); + let (lp_token, lp_token_client) = create_comet_lp_pool(&e, &bombadil, &blnd, &usdc); + let (backstop_id, backstop_client) = create_backstop(&e); + setup_backstop(&e, &pool_id, &backstop_id, &lp_token, &usdc, &blnd); + + // mint lp tokens + blnd_client.mint(&samwise, &500_001_0000000); + blnd_client.approve(&samwise, &lp_token, &i128::MAX, &99999); + usdc_client.mint(&samwise, &12_501_0000000); + usdc_client.approve(&samwise, &lp_token, &i128::MAX, &99999); + lp_token_client.join_pool( + &50_000_0000000, + &vec![&e, 500_001_0000000, 12_501_0000000], + &samwise, + ); + backstop_client.deposit(&samwise, &pool_id, &50_000_0000000); + backstop_client.update_tkn_val(); + backstop_client.queue_withdrawal(&samwise, &pool_id, &40_000_0000000); + + let pool_config = PoolConfig { + oracle: oracle_id, + bstop_rate: 0, + status: 6, + }; + e.as_contract(&pool_id, || { + storage::set_admin(&e, &bombadil); + storage::set_pool_config(&e, &pool_config); + + execute_set_pool_status(&e, 3); + }); + } + #[test] fn test_set_pool_status_frozen() { let e = Env::default(); e.budget().reset_unlimited(); @@ -891,11 +945,11 @@ mod tests { } #[test] - #[should_panic(expected = "Error(Auth, InvalidAction)")] + #[should_panic(expected = "Error(Contract, #8)")] fn test_update_pool_status_admin_frozen() { let e = Env::default(); e.budget().reset_unlimited(); - // e.mock_all_auths_allowing_non_root_auth(); + e.mock_all_auths_allowing_non_root_auth(); let pool_id = create_pool(&e); let oracle_id = Address::generate(&e); @@ -934,6 +988,50 @@ mod tests { }); } + #[test] + #[should_panic(expected = "Error(Contract, #8)")] + fn test_update_pool_status_setup() { + let e = Env::default(); + e.budget().reset_unlimited(); + e.mock_all_auths_allowing_non_root_auth(); + let pool_id = create_pool(&e); + let oracle_id = Address::generate(&e); + + let bombadil = Address::generate(&e); + let samwise = Address::generate(&e); + + let (blnd, blnd_client) = create_token_contract(&e, &bombadil); + let (usdc, usdc_client) = create_token_contract(&e, &bombadil); + let (lp_token, lp_token_client) = create_comet_lp_pool(&e, &bombadil, &blnd, &usdc); + let (backstop_id, backstop_client) = create_backstop(&e); + setup_backstop(&e, &pool_id, &backstop_id, &lp_token, &usdc, &blnd); + + // mint lp tokens + blnd_client.mint(&samwise, &500_001_0000000); + blnd_client.approve(&samwise, &lp_token, &i128::MAX, &99999); + usdc_client.mint(&samwise, &12_501_0000000); + usdc_client.approve(&samwise, &lp_token, &i128::MAX, &99999); + lp_token_client.join_pool( + &50_000_0000000, + &vec![&e, 500_001_0000000, 12_501_0000000], + &samwise, + ); + backstop_client.deposit(&samwise, &pool_id, &50_000_0000000); + backstop_client.update_tkn_val(); + + let pool_config = PoolConfig { + oracle: oracle_id, + bstop_rate: 0, + status: 6, + }; + e.as_contract(&pool_id, || { + storage::set_admin(&e, &bombadil); + storage::set_pool_config(&e, &pool_config); + + execute_update_pool_status(&e); + }); + } + #[test] fn test_admin_update_pool_status_unfreeze() { let e = Env::default(); diff --git a/pool/src/storage.rs b/pool/src/storage.rs index bf4e8ac1..bbe742c5 100644 --- a/pool/src/storage.rs +++ b/pool/src/storage.rs @@ -45,6 +45,12 @@ pub struct ReserveConfig { pub r_three: u32, // the R3 value in the interest rate formula scaled expressed in 7 decimals pub reactivity: u32, // the reactivity constant for the reserve scaled expressed in 9 decimals } +#[derive(Clone)] +#[contracttype] +pub struct QueuedReserveInit { + pub new_config: ReserveConfig, + pub unlock_time: u64, +} /// The data for a reserve asset #[derive(Clone)] @@ -116,6 +122,8 @@ pub struct AuctionKey { pub enum PoolDataKey { // A map of underlying asset's contract address to reserve config ResConfig(Address), + // A map of underlying asset's contract address to queued reserve init + ResInit(Address), // A map of underlying asset's contract address to reserve data ResData(Address), // The reserve's emission config @@ -359,6 +367,51 @@ pub fn has_res(e: &Env, asset: &Address) -> bool { e.storage().persistent().has(&key) } +/// Fetch a queued reserve set +/// +/// ### Arguments +/// * `asset` - The contract address of the asset +/// +/// ### Panics +/// If the reserve set has not been queued +pub fn get_queued_reserve_set(e: &Env, asset: &Address) -> QueuedReserveInit { + let key = PoolDataKey::ResInit(asset.clone()); + e.storage() + .temporary() + .extend_ttl(&key, LEDGER_THRESHOLD_USER, LEDGER_BUMP_USER); + e.storage() + .temporary() + .get::(&key) + .unwrap_optimized() +} + +/// Set a new queued reserve set +/// +/// ### Arguments +/// * `asset` - The contract address of the asset +/// * `config` - The reserve configuration for the asset +pub fn set_queued_reserve_set(e: &Env, res_init: &QueuedReserveInit, asset: &Address) { + let key = PoolDataKey::ResInit(asset.clone()); + e.storage() + .temporary() + .set::(&key, res_init); + e.storage() + .temporary() + .extend_ttl(&key, LEDGER_THRESHOLD_USER, LEDGER_BUMP_USER); +} + +/// Delete a queued reserve set +/// +/// ### Arguments +/// * `asset` - The contract address of the asset +/// +/// ### Panics +/// If the reserve set has not been queued +pub fn del_queued_reserve_set(e: &Env, asset: &Address) { + let key = PoolDataKey::ResInit(asset.clone()); + e.storage().temporary().remove(&key); +} + /********** Reserve Data (ResData) **********/ /// Fetch the reserve data for an asset diff --git a/test-suites/src/setup.rs b/test-suites/src/setup.rs index 9c012c06..1e98c32f 100644 --- a/test-suites/src/setup.rs +++ b/test-suites/src/setup.rs @@ -1,5 +1,5 @@ use pool::{Request, ReserveEmissionMetadata}; -use soroban_sdk::{testutils::Address as _, vec, Address, Symbol, Vec}; +use soroban_sdk::{testutils::Address as _, vec as svec, Address, Symbol, Vec as SVec}; use crate::{ pool::default_reserve_metadata, @@ -10,6 +10,28 @@ use crate::{ pub fn create_fixture_with_data<'a>(wasm: bool) -> TestFixture<'a> { let mut fixture = TestFixture::create(wasm); + // mint whale tokens + let frodo = Address::generate(&fixture.env); + fixture.users.push(frodo.clone()); + fixture.tokens[TokenIndex::STABLE].mint(&frodo, &(100_000 * 10i128.pow(6))); + fixture.tokens[TokenIndex::XLM].mint(&frodo, &(1_000_000 * SCALAR_7)); + fixture.tokens[TokenIndex::WETH].mint(&frodo, &(100 * 10i128.pow(9))); + + // mint LP tokens with whale + fixture.tokens[TokenIndex::BLND].mint(&frodo, &(500_0010_000_0000_0000 * SCALAR_7)); + // fixture.tokens[TokenIndex::BLND].approve(&frodo, &fixture.lp.address, &i128::MAX, &99999); + fixture.tokens[TokenIndex::USDC].mint(&frodo, &(12_5010_000_0000_0000 * SCALAR_7)); + // fixture.tokens[TokenIndex::USDC].approve(&frodo, &fixture.lp.address, &i128::MAX, &99999); + fixture.lp.join_pool( + &(500_000_0000 * SCALAR_7), + &svec![ + &fixture.env, + 500_0010_000_0000_0000 * SCALAR_7, + 12_5010_000_0000_0000 * SCALAR_7, + ], + &frodo, + ); + // create pool fixture.create_pool(Symbol::new(&fixture.env, "Teapot"), 0_100_000_000); @@ -18,20 +40,20 @@ pub fn create_fixture_with_data<'a>(wasm: bool) -> TestFixture<'a> { stable_config.c_factor = 0_900_0000; stable_config.l_factor = 0_950_0000; stable_config.util = 0_850_0000; - fixture.create_pool_reserve(0, TokenIndex::STABLE, stable_config); + fixture.create_pool_reserve(0, TokenIndex::STABLE, &stable_config); let mut xlm_config = default_reserve_metadata(); xlm_config.c_factor = 0_750_0000; xlm_config.l_factor = 0_750_0000; xlm_config.util = 0_500_0000; - fixture.create_pool_reserve(0, TokenIndex::XLM, xlm_config); + fixture.create_pool_reserve(0, TokenIndex::XLM, &xlm_config); let mut weth_config = default_reserve_metadata(); weth_config.decimals = 9; weth_config.c_factor = 0_800_0000; weth_config.l_factor = 0_800_0000; weth_config.util = 0_700_0000; - fixture.create_pool_reserve(0, TokenIndex::WETH, weth_config); + fixture.create_pool_reserve(0, TokenIndex::WETH, &weth_config); // enable emissions for pool let pool_fixture = &fixture.pools[0]; @@ -51,28 +73,6 @@ pub fn create_fixture_with_data<'a>(wasm: bool) -> TestFixture<'a> { ]; pool_fixture.pool.set_emissions_config(&reserve_emissions); - // mint whale tokens - let frodo = Address::generate(&fixture.env); - fixture.users.push(frodo.clone()); - fixture.tokens[TokenIndex::STABLE].mint(&frodo, &(100_000 * 10i128.pow(6))); - fixture.tokens[TokenIndex::XLM].mint(&frodo, &(1_000_000 * SCALAR_7)); - fixture.tokens[TokenIndex::WETH].mint(&frodo, &(100 * 10i128.pow(9))); - - // mint LP tokens with whale - fixture.tokens[TokenIndex::BLND].mint(&frodo, &(500_0010_000_0000_0000 * SCALAR_7)); - // fixture.tokens[TokenIndex::BLND].approve(&frodo, &fixture.lp.address, &i128::MAX, &99999); - fixture.tokens[TokenIndex::USDC].mint(&frodo, &(12_5010_000_0000_0000 * SCALAR_7)); - // fixture.tokens[TokenIndex::USDC].approve(&frodo, &fixture.lp.address, &i128::MAX, &99999); - fixture.lp.join_pool( - &(500_000_0000 * SCALAR_7), - &vec![ - &fixture.env, - 500_0010_000_0000_0000 * SCALAR_7, - 12_5010_000_0000_0000 * SCALAR_7, - ], - &frodo, - ); - // deposit into backstop, add to reward zone fixture .backstop @@ -81,6 +81,7 @@ pub fn create_fixture_with_data<'a>(wasm: bool) -> TestFixture<'a> { fixture .backstop .add_reward(&pool_fixture.pool.address, &Address::generate(&fixture.env)); + pool_fixture.pool.set_status(&3); pool_fixture.pool.update_status(); // enable emissions @@ -105,7 +106,7 @@ pub fn create_fixture_with_data<'a>(wasm: bool) -> TestFixture<'a> { // fixture.tokens[TokenIndex::XLM].approve(&frodo, &pool_fixture.pool.address, &i128::MAX, &50000); // supply and borrow STABLE for 80% utilization (close to target) - let requests: Vec = vec![ + let requests: SVec = svec![ &fixture.env, Request { request_type: 2, @@ -121,7 +122,7 @@ pub fn create_fixture_with_data<'a>(wasm: bool) -> TestFixture<'a> { pool_fixture.pool.submit(&frodo, &frodo, &frodo, &requests); // supply and borrow WETH for 50% utilization (below target) - let requests: Vec = vec![ + let requests: SVec = svec![ &fixture.env, Request { request_type: 2, @@ -137,7 +138,7 @@ pub fn create_fixture_with_data<'a>(wasm: bool) -> TestFixture<'a> { pool_fixture.pool.submit(&frodo, &frodo, &frodo, &requests); // supply and borrow XLM for 65% utilization (above target) - let requests: Vec = vec![ + let requests: SVec = svec![ &fixture.env, Request { request_type: 2, @@ -167,7 +168,7 @@ mod tests { #[test] fn test_create_fixture_with_data_wasm() { - let fixture = create_fixture_with_data(true); + let fixture: TestFixture<'_> = create_fixture_with_data(true); let frodo = fixture.users.get(0).unwrap(); let pool_fixture: &PoolFixture = fixture.pools.get(0).unwrap(); diff --git a/test-suites/src/test_fixture.rs b/test-suites/src/test_fixture.rs index f53fef33..8cc4a87b 100644 --- a/test-suites/src/test_fixture.rs +++ b/test-suites/src/test_fixture.rs @@ -185,13 +185,14 @@ impl TestFixture<'_> { &mut self, pool_index: usize, asset_index: TokenIndex, - reserve_config: ReserveConfig, + reserve_config: &ReserveConfig, ) { let mut pool_fixture = self.pools.remove(pool_index); let token = &self.tokens[asset_index]; - let index = pool_fixture + pool_fixture .pool - .init_reserve(&token.address, &reserve_config); + .queue_set_reserve(&token.address, reserve_config); + let index = pool_fixture.pool.set_reserve(&token.address); pool_fixture.reserves.insert(asset_index, index); self.pools.insert(pool_index, pool_fixture); } diff --git a/test-suites/tests/test_liquidation.rs b/test-suites/tests/test_liquidation.rs index 705f2720..6a7ce85b 100644 --- a/test-suites/tests/test_liquidation.rs +++ b/test-suites/tests/test_liquidation.rs @@ -18,22 +18,71 @@ fn test_liquidations() { let frodo = fixture.users.get(0).unwrap(); let pool_fixture = &fixture.pools[0]; + //accrue interest + let requests: Vec = vec![ + &fixture.env, + Request { + request_type: 4, + address: fixture.tokens[TokenIndex::STABLE].address.clone(), + amount: 1, + }, + Request { + request_type: 5, + address: fixture.tokens[TokenIndex::STABLE].address.clone(), + amount: 1, + }, + Request { + request_type: 4, + address: fixture.tokens[TokenIndex::XLM].address.clone(), + amount: 1, + }, + Request { + request_type: 5, + address: fixture.tokens[TokenIndex::XLM].address.clone(), + amount: 1, + }, + Request { + request_type: 4, + address: fixture.tokens[TokenIndex::WETH].address.clone(), + amount: 1, + }, + Request { + request_type: 5, + address: fixture.tokens[TokenIndex::WETH].address.clone(), + amount: 1, + }, + ]; + pool_fixture.pool.submit(&frodo, &frodo, &frodo, &requests); + // Disable rate modifiers let mut usdc_config: ReserveConfig = fixture.read_reserve_config(0, TokenIndex::STABLE); usdc_config.reactivity = 0; - pool_fixture - .pool - .update_reserve(&fixture.tokens[TokenIndex::STABLE].address, &usdc_config); + let mut xlm_config: ReserveConfig = fixture.read_reserve_config(0, TokenIndex::XLM); xlm_config.reactivity = 0; - pool_fixture - .pool - .update_reserve(&fixture.tokens[TokenIndex::XLM].address, &xlm_config); let mut weth_config: ReserveConfig = fixture.read_reserve_config(0, TokenIndex::WETH); weth_config.reactivity = 0; - pool_fixture - .pool - .update_reserve(&fixture.tokens[TokenIndex::WETH].address, &weth_config); + + fixture.env.as_contract(&fixture.pools[0].pool.address, || { + let key = PoolDataKey::ResConfig(fixture.tokens[TokenIndex::STABLE].address.clone()); + fixture + .env + .storage() + .persistent() + .set::(&key, &usdc_config); + let key = PoolDataKey::ResConfig(fixture.tokens[TokenIndex::XLM].address.clone()); + fixture + .env + .storage() + .persistent() + .set::(&key, &xlm_config); + let key = PoolDataKey::ResConfig(fixture.tokens[TokenIndex::WETH].address.clone()); + fixture + .env + .storage() + .persistent() + .set::(&key, &weth_config); + }); // Create a user let samwise = Address::generate(&fixture.env); //sam will be supplying XLM and borrowing STABLE @@ -810,6 +859,11 @@ fn test_liquidations() { .pool .submit(&frodo, &frodo, &frodo, &bad_debt_fill_request); // transfer bad debt to backstop + + pool_fixture + .pool + .submit(&samwise, &samwise, &samwise, &blank_request); + pool_fixture.pool.bad_debt(&samwise); let events = fixture.env.events().all(); @@ -860,7 +914,7 @@ fn test_liquidations() { assert_eq!(positions.liabilities.get(0).unwrap(), bad_debt); }); // check d_supply - let d_supply = 19104604033; + let d_supply = 19104604034; fixture.env.as_contract(&pool_fixture.pool.address, || { let key = PoolDataKey::ResData(fixture.tokens[TokenIndex::STABLE].address.clone()); let data = fixture @@ -907,7 +961,6 @@ fn test_liquidations() { }); let events = fixture.env.events().all(); let event = vec![&fixture.env, events.get_unchecked(events.len() - 2)]; - let bad_debt: i128 = 92903018; assert_eq!( event, vec![ diff --git a/test-suites/tests/test_pool.rs b/test-suites/tests/test_pool.rs index 98a9b382..61d0cc95 100644 --- a/test-suites/tests/test_pool.rs +++ b/test-suites/tests/test_pool.rs @@ -593,7 +593,7 @@ fn test_pool_config() { reserve_config.c_factor = 0_200_0000; pool_fixture .pool - .init_reserve(&blnd.address, &reserve_config); + .queue_set_reserve(&blnd.address, &reserve_config); assert_eq!( fixture.env.auths()[0], ( @@ -601,7 +601,7 @@ fn test_pool_config() { AuthorizedInvocation { function: AuthorizedFunction::Contract(( pool_fixture.pool.address.clone(), - Symbol::new(&fixture.env, "init_reserve"), + Symbol::new(&fixture.env, "queue_set_reserve"), vec![ &fixture.env, blnd.address.to_val(), @@ -612,6 +612,9 @@ fn test_pool_config() { } ) ); + fixture.jump(604800); // 1 week + pool_fixture.pool.set_reserve(&blnd.address); + let new_reserve_config = fixture.read_reserve_config(0, TokenIndex::BLND); assert_eq!(new_reserve_config.l_factor, 0_500_0000); assert_eq!(new_reserve_config.c_factor, 0_200_0000); @@ -628,11 +631,7 @@ fn test_pool_config() { &fixture.env, ( pool_fixture.pool.address.clone(), - ( - Symbol::new(&fixture.env, "init_reserve"), - fixture.bombadil.clone() - ) - .into_val(&fixture.env), + (Symbol::new(&fixture.env, "set_reserve"),).into_val(&fixture.env), event_data.into_val(&fixture.env) ) ] @@ -642,7 +641,7 @@ fn test_pool_config() { reserve_config.c_factor = 0; pool_fixture .pool - .update_reserve(&blnd.address, &reserve_config); + .queue_set_reserve(&blnd.address, &reserve_config); assert_eq!( fixture.env.auths()[0], ( @@ -650,7 +649,7 @@ fn test_pool_config() { AuthorizedInvocation { function: AuthorizedFunction::Contract(( pool_fixture.pool.address.clone(), - Symbol::new(&fixture.env, "update_reserve"), + Symbol::new(&fixture.env, "queue_set_reserve"), vec![ &fixture.env, blnd.address.to_val(), @@ -661,6 +660,19 @@ fn test_pool_config() { } ) ); + fixture.jump(604800); // 1 week + pool_fixture.pool.set_reserve(&blnd.address); + assert_eq!( + event, + vec![ + &fixture.env, + ( + pool_fixture.pool.address.clone(), + (Symbol::new(&fixture.env, "set_reserve"),).into_val(&fixture.env), + event_data.into_val(&fixture.env) + ) + ] + ); let new_reserve_config = fixture.read_reserve_config(0, TokenIndex::BLND); assert_eq!(new_reserve_config.l_factor, 0_500_0000); assert_eq!(new_reserve_config.c_factor, 0); @@ -672,12 +684,8 @@ fn test_pool_config() { &fixture.env, ( pool_fixture.pool.address.clone(), - ( - Symbol::new(&fixture.env, "update_reserve"), - fixture.bombadil.clone() - ) - .into_val(&fixture.env), - blnd.address.to_val() + (Symbol::new(&fixture.env, "set_reserve"),).into_val(&fixture.env), + event_data.into_val(&fixture.env) ) ] );