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/pool_factory.rs b/pool-factory/src/pool_factory.rs index f8fbdea8..b97daa3e 100644 --- a/pool-factory/src/pool_factory.rs +++ b/pool-factory/src/pool_factory.rs @@ -1,6 +1,6 @@ use crate::{ errors::PoolFactoryError, - storage::{self, PoolInitMeta}, + storage::{self, PoolInitMeta, ReserveConfig}, }; use soroban_sdk::{ contract, contractclient, contractimpl, panic_with_error, vec, Address, BytesN, Env, IntoVal, @@ -32,6 +32,7 @@ pub trait PoolFactory { salt: BytesN<32>, oracle: Address, backstop_take_rate: u64, + reserves: Vec<(Address, ReserveConfig)>, ) -> Address; /// Checks if contract address was deployed by the factory @@ -60,6 +61,7 @@ impl PoolFactory for PoolFactoryContract { salt: BytesN<32>, oracle: Address, backstop_take_rate: u64, + reserves: Vec<(Address, ReserveConfig)>, ) -> Address { admin.require_auth(); storage::extend_instance(&e); @@ -78,6 +80,7 @@ impl PoolFactory for PoolFactoryContract { 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()); + init_args.push_back(reserves.to_val()); let pool_address = e .deployer() .with_current_contract(salt) diff --git a/pool-factory/src/storage.rs b/pool-factory/src/storage.rs index ad726e96..00dceb0d 100644 --- a/pool-factory/src/storage.rs +++ b/pool-factory/src/storage.rs @@ -4,6 +4,24 @@ use soroban_sdk::{contracttype, unwrap::UnwrapOptimized, Address, BytesN, Env, S pub(crate) const LEDGER_THRESHOLD: u32 = 518400; // TODO: Check on phase 1 max ledger entry bump pub(crate) const LEDGER_BUMP: u32 = 535670; // TODO: Check on phase 1 max ledger entry bump +/// The configuration information about a reserve asset +/// TODO: Duplicated from pool/storage.rs. Can this be moved to a common location? +#[derive(Clone)] +#[contracttype] +pub struct ReserveConfig { + pub index: u32, // the index of the reserve in the list + pub decimals: u32, // the decimals used in both the bToken and underlying contract + pub c_factor: u32, // the collateral factor for the reserve scaled expressed in 7 decimals + pub l_factor: u32, // the liability factor for the reserve scaled expressed in 7 decimals + pub util: u32, // the target utilization rate scaled expressed in 7 decimals + pub max_util: u32, // the maximum allowed utilization rate scaled expressed in 7 decimals + pub r_one: u32, // the R1 value in the interest rate formula scaled expressed in 7 decimals + pub r_two: u32, // the R2 value in the interest rate formula scaled expressed in 7 decimals + pub r_three: u32, // the R3 value in the interest rate formula scaled expressed in 7 decimals + pub reactivity: u32, // the reactivity constant for the reserve scaled expressed in 9 decimals +} + + #[derive(Clone)] #[contracttype] pub enum PoolFactoryDataKey { diff --git a/pool-factory/src/test.rs b/pool-factory/src/test.rs index aabe5228..917dd18e 100644 --- a/pool-factory/src/test.rs +++ b/pool-factory/src/test.rs @@ -5,7 +5,10 @@ use soroban_sdk::{ vec, Address, BytesN, Env, IntoVal, Symbol, }; -use crate::{PoolFactoryClient, PoolFactoryContract, PoolInitMeta}; +use crate::{ + storage::ReserveConfig, test::pool::PoolDataKey, PoolFactoryClient, PoolFactoryContract, + PoolInitMeta, +}; mod pool { soroban_sdk::contractimport!(file = "../target/wasm32-unknown-unknown/optimized/pool.wasm"); @@ -48,8 +51,27 @@ 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); + let asset_id_0 = Address::generate(&e); + 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 deployed_pool_address_1 = pool_factory_client.deploy( + &bombadil, + &name1, + &salt, + &oracle, + &backstop_rate, + &vec![&e, (asset_id_0.clone(), metadata.clone())], + ); let event = vec![&e, e.events().all().last_unchecked()]; assert_eq!( @@ -65,8 +87,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, + &vec![&e, (asset_id_0.clone(), metadata.clone())], + ); e.as_contract(&deployed_pool_address_1, || { assert_eq!( @@ -108,6 +136,22 @@ fn test_pool_factory() { .unwrap(), usdc_id.clone() ); + let key = PoolDataKey::ResConfig(asset_id_0.clone()); + let set_config = e + .storage() + .persistent() + .get::<_, ReserveConfig>(&key) + .unwrap(); + assert_eq!(set_config.decimals, metadata.decimals); + assert_eq!(set_config.c_factor, metadata.c_factor); + assert_eq!(set_config.l_factor, metadata.l_factor); + assert_eq!(set_config.util, metadata.util); + assert_eq!(set_config.max_util, metadata.max_util); + assert_eq!(set_config.r_one, metadata.r_one); + assert_eq!(set_config.r_two, metadata.r_two); + assert_eq!(set_config.r_three, metadata.r_three); + assert_eq!(set_config.reactivity, metadata.reactivity); + assert_eq!(set_config.index, 0); }); assert_ne!(deployed_pool_address_1, deployed_pool_address_2); assert!(pool_factory_client.is_pool(&deployed_pool_address_1)); diff --git a/pool/src/constants.rs b/pool/src/constants.rs index 1eb11ab6..2d415947 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 WEEK_IN_BLOCKS: u32 = 120960; diff --git a/pool/src/contract.rs b/pool/src/contract.rs index 6c77851f..a9d74bf8 100644 --- a/pool/src/contract.rs +++ b/pool/src/contract.rs @@ -1,8 +1,9 @@ use crate::{ auctions::{self, AuctionData}, + constants::WEEK_IN_BLOCKS, emissions::{self, ReserveEmissionMetadata}, pool::{self, Positions, Request}, - storage::{self, ReserveConfig}, + storage::{self, QueuedReserveInit, ReserveConfig}, }; use soroban_sdk::{contract, contractclient, contractimpl, Address, Env, Symbol, Vec}; @@ -37,6 +38,7 @@ pub trait Pool { backstop_id: Address, blnd_id: Address, usdc_id: Address, + reserves: Vec<(Address, ReserveConfig)>, ); /// (Admin only) Set a new address as the admin of this pool @@ -57,15 +59,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 the initialization 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 is already setup - fn init_reserve(e: Env, asset: Address, metadata: ReserveConfig) -> u32; + /// If the caller is not the admin + fn queue_init_reserve(e: Env, asset: Address, metadata: ReserveConfig); + + /// (Admin only) Cancels the queued initialization of a reserve in the pool + /// + /// ### Arguments + /// * `asset` - The underlying asset to add as a reserve + /// + /// ### Panics + /// If the caller is not the admin or the reserve is not queued for initialization + fn cancel_init_reserve(e: Env, asset: Address); + + /// (Admin only) Executes the queued initialization 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 init_reserve(e: Env, asset: Address) -> u32; /// (Admin only) Update a reserve in the pool /// @@ -224,6 +246,7 @@ impl Pool for PoolContract { backstop_id: Address, blnd_id: Address, usdc_id: Address, + reserves: Vec<(Address, ReserveConfig)>, ) { storage::extend_instance(&e); @@ -236,6 +259,7 @@ impl Pool for PoolContract { &backstop_id, &blnd_id, &usdc_id, + &reserves, ); } @@ -261,15 +285,39 @@ 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_init_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); + storage::set_queued_reserve_init( + &e, + &QueuedReserveInit { + new_config: metadata.clone(), + unlock_block: e.ledger().sequence() + WEEK_IN_BLOCKS, + }, + &asset, + ); + + e.events().publish( + (Symbol::new(&e, "queue_init_reserve"), admin), + (asset, metadata), + ); + } + + fn cancel_init_reserve(e: Env, asset: Address) { + storage::extend_instance(&e); + let admin = storage::get_admin(&e); + admin.require_auth(); + + storage::del_queued_reserve_init(&e, &asset); e.events() - .publish((Symbol::new(&e, "init_reserve"), admin), (asset, index)); + .publish((Symbol::new(&e, "cancel_init_reserve"), admin), asset); + } + + fn init_reserve(e: Env, asset: Address) -> u32 { + let index = pool::execute_queued_reserve_initialization(&e, &asset); index } diff --git a/pool/src/errors.rs b/pool/src/errors.rs index a3d83b5f..51114bb2 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, // Pool State Errors (10-19) InvalidHf = 10, InvalidPoolStatus = 11, diff --git a/pool/src/pool/config.rs b/pool/src/pool/config.rs index b12a3005..a4c4c703 100644 --- a/pool/src/pool/config.rs +++ b/pool/src/pool/config.rs @@ -2,7 +2,7 @@ use crate::{ errors::PoolError, storage::{self, PoolConfig, ReserveConfig, ReserveData}, }; -use soroban_sdk::{panic_with_error, Address, Env, Symbol}; +use soroban_sdk::{panic_with_error, Address, Env, Symbol, Vec}; use super::pool::Pool; @@ -19,6 +19,7 @@ pub fn execute_initialize( backstop_address: &Address, blnd_id: &Address, usdc_id: &Address, + reserves: &Vec<(Address, ReserveConfig)>, ) { if storage::has_admin(e) { panic_with_error!(e, PoolError::AlreadyInitialized); @@ -42,6 +43,9 @@ pub fn execute_initialize( ); storage::set_blnd_token(e, blnd_id); storage::set_usdc_token(e, usdc_id); + for (asset, config) in reserves.iter() { + initialize_reserve(e, &asset, &config); + } } /// Update the pool @@ -55,8 +59,25 @@ pub fn execute_update_pool(e: &Env, backstop_take_rate: u64) { storage::set_pool_config(e, &pool_config); } +/// Execute a queued reserve initialization for the pool +pub fn execute_queued_reserve_initialization(e: &Env, asset: &Address) -> u32 { + let queued_init = storage::get_queued_reserve_init(e, asset); + + if queued_init.unlock_block > e.ledger().sequence() { + panic_with_error!(e, PoolError::InitNotUnlocked); + } + + // remove queued init + storage::del_queued_reserve_init(e, asset); + + // initialize reserve + let index = initialize_reserve(e, asset, &queued_init.new_config); + + index +} + /// Initialize a reserve for the pool -pub fn initialize_reserve(e: &Env, asset: &Address, config: &ReserveConfig) -> u32 { +fn initialize_reserve(e: &Env, asset: &Address, config: &ReserveConfig) -> u32 { if storage::has_res(e, asset) { panic_with_error!(e, PoolError::AlreadyInitialized); } @@ -87,6 +108,8 @@ pub fn initialize_reserve(e: &Env, asset: &Address, config: &ReserveConfig) -> u backstop_credit: 0, }; storage::set_res_data(e, asset, &init_data); + e.events() + .publish((Symbol::new(&e, "init_reserve"),), (asset, index)); index } @@ -126,10 +149,12 @@ fn require_valid_reserve_metadata(e: &Env, metadata: &ReserveConfig) { #[cfg(test)] mod tests { + use crate::storage::QueuedReserveInit; use crate::testutils; use super::*; use soroban_sdk::testutils::{Address as _, Ledger, LedgerInfo}; + use soroban_sdk::vec; #[test] fn test_execute_initialize() { @@ -143,6 +168,21 @@ mod tests { let backstop_address = Address::generate(&e); let blnd_id = Address::generate(&e); let usdc_id = Address::generate(&e); + let (asset_id_0, _) = testutils::create_token_contract(&e, &admin); + let (asset_id_1, _) = testutils::create_token_contract(&e, &admin); + + 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, || { execute_initialize( @@ -154,6 +194,11 @@ mod tests { &backstop_address, &blnd_id, &usdc_id, + &vec![ + &e, + (asset_id_0.clone(), metadata.clone()), + (asset_id_1.clone(), metadata.clone()), + ], ); assert_eq!(storage::get_admin(&e), admin); @@ -164,6 +209,19 @@ mod tests { 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); + 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); }); } @@ -206,7 +264,82 @@ mod tests { execute_update_pool(&e, 1_000_000_000u64); }); } + #[test] + fn test_execute_queued_initialize_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_init( + &e, + &QueuedReserveInit { + new_config: metadata.clone(), + unlock_block: e.ledger().sequence(), + }, + &asset_id_0, + ); + execute_queued_reserve_initialization(&e, &asset_id_0); + let res_config_0 = storage::get_res_config(&e, &asset_id_0); + assert_eq!(res_config_0.decimals, metadata.decimals); + assert_eq!(res_config_0.c_factor, metadata.c_factor); + assert_eq!(res_config_0.l_factor, metadata.l_factor); + assert_eq!(res_config_0.util, metadata.util); + assert_eq!(res_config_0.max_util, metadata.max_util); + assert_eq!(res_config_0.r_one, metadata.r_one); + assert_eq!(res_config_0.r_two, metadata.r_two); + assert_eq!(res_config_0.r_three, metadata.r_three); + assert_eq!(res_config_0.reactivity, metadata.reactivity); + assert_eq!(res_config_0.index, 0); + }); + } + #[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_init( + &e, + &QueuedReserveInit { + new_config: metadata.clone(), + unlock_block: e.ledger().sequence() + 1, + }, + &asset_id_0, + ); + execute_queued_reserve_initialization(&e, &asset_id_0); + }); + } #[test] fn test_initialize_reserve() { let e = Env::default(); diff --git a/pool/src/pool/mod.rs b/pool/src/pool/mod.rs index d501510a..f659c763 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_initialize, execute_queued_reserve_initialization, execute_update_pool, + execute_update_reserve, }; mod health_factor; diff --git a/pool/src/storage.rs b/pool/src/storage.rs index bf4e8ac1..45230a30 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_block: u32, +} /// 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 initialization +/// +/// ### Arguments +/// * `asset` - The contract address of the asset +/// +/// ### Panics +/// If the reserve initialization has not been queued +pub fn get_queued_reserve_init(e: &Env, asset: &Address) -> QueuedReserveInit { + let key = PoolDataKey::ResInit(asset.clone()); + e.storage() + .persistent() + .extend_ttl(&key, LEDGER_THRESHOLD_SHARED, LEDGER_BUMP_SHARED); + e.storage() + .persistent() + .get::(&key) + .unwrap_optimized() +} + +/// Set a new swap in the queue +/// +/// ### Arguments +/// * `asset` - The contract address of the asset +/// * `config` - The reserve configuration for the asset +pub fn set_queued_reserve_init(e: &Env, res_init: &QueuedReserveInit, asset: &Address) { + let key = PoolDataKey::ResInit(asset.clone()); + e.storage() + .persistent() + .set::(&key, res_init); + e.storage() + .persistent() + .extend_ttl(&key, LEDGER_THRESHOLD_SHARED, LEDGER_BUMP_SHARED); +} + +/// Delete a queued reserve initialization +/// +/// ### Arguments +/// * `asset` - The contract address of the asset +/// +/// ### Panics +/// If the reserve initialization has not been queued +pub fn del_queued_reserve_init(e: &Env, asset: &Address) { + let key = PoolDataKey::ResInit(asset.clone()); + e.storage().persistent().remove(&key); +} + /********** Reserve Data (ResData) **********/ /// Fetch the reserve data for an asset diff --git a/test-suites/src/test_fixture.rs b/test-suites/src/test_fixture.rs index f53fef33..df62cc47 100644 --- a/test-suites/src/test_fixture.rs +++ b/test-suites/src/test_fixture.rs @@ -5,7 +5,7 @@ use crate::backstop::create_backstop; use crate::emitter::create_emitter; use crate::liquidity_pool::{create_lp_pool, LPClient}; use crate::oracle::create_mock_oracle; -use crate::pool::POOL_WASM; +use crate::pool::{default_reserve_metadata, POOL_WASM}; use crate::pool_factory::create_pool_factory; use crate::token::{create_stellar_token, create_token}; use backstop::BackstopClient; @@ -174,6 +174,7 @@ impl TestFixture<'_> { &BytesN::<32>::random(&self.env), &self.oracle.address, &backstop_take_rate, + &svec![&self.env,], ); self.pools.push(PoolFixture { pool: PoolClient::new(&self.env, &pool_id), @@ -189,9 +190,7 @@ impl TestFixture<'_> { ) { let mut pool_fixture = self.pools.remove(pool_index); let token = &self.tokens[asset_index]; - let index = pool_fixture - .pool - .init_reserve(&token.address, &reserve_config); + let index = pool_fixture.pool.init_reserve(&token.address); pool_fixture.reserves.insert(asset_index, index); self.pools.insert(pool_index, pool_fixture); } diff --git a/test-suites/tests/test_pool.rs b/test-suites/tests/test_pool.rs index 98a9b382..195bc0b0 100644 --- a/test-suites/tests/test_pool.rs +++ b/test-suites/tests/test_pool.rs @@ -548,6 +548,7 @@ fn test_pool_config() { &Address::generate(&fixture.env), &Address::generate(&fixture.env), &Address::generate(&fixture.env), + &vec![&fixture.env], ); assert!(result.is_err()); @@ -591,9 +592,7 @@ fn test_pool_config() { let mut reserve_config = default_reserve_metadata(); reserve_config.l_factor = 0_500_0000; reserve_config.c_factor = 0_200_0000; - pool_fixture - .pool - .init_reserve(&blnd.address, &reserve_config); + pool_fixture.pool.init_reserve(&blnd.address); assert_eq!( fixture.env.auths()[0], (