diff --git a/beam-contract/src/lib.rs b/beam-contract/src/lib.rs index a5f4027..ea62436 100644 --- a/beam-contract/src/lib.rs +++ b/beam-contract/src/lib.rs @@ -101,6 +101,23 @@ impl BeamOracleContract { PriceOracleContractBase::expires(e, asset) } + // Estimates amount of fee tokens required to extend the asset retention config for a given time + // + // # Arguments + // + // * `period` - Desired retention period extension (in seconds) + // + // # Returns + // + // Fee asset and estimated amount required for the fee bump + // + // # Panics + // + // Panics if the asset is not supported or if retention config is malformed/missing + pub fn estimate_retention_cost(e: &Env, period: u64) -> (Address, i128) { + PriceOracleContractBase::estimate_retention_cost(e, period) + } + // Extends asset expiration date by a given amount of tokens. // // # Arguments @@ -109,11 +126,15 @@ impl BeamOracleContract { // * `asset` - Quoted asset // * `amount` - Amount of tokens to burn for extending the expiration date // + // # Returns + // + // Current asset expiration timestamp (in seconds) + // // # Panics // // Panics if asset is not supported or if retention config is malformed/missing - pub fn extend_asset_ttl(e: &Env, sponsor: Address, asset: Asset, amount: i128) { - PriceOracleContractBase::extend_asset_ttl(e, sponsor, asset, amount, 0); + pub fn extend_asset_ttl(e: &Env, sponsor: Address, asset: Asset, amount: i128) -> u64 { + PriceOracleContractBase::extend_asset_ttl(e, sponsor, asset, amount, 0) } // Return fee token address daily price feed retainer fee amount diff --git a/beam-contract/src/tests/contract_tests.rs b/beam-contract/src/tests/contract_tests.rs index d3fed13..ba24bf2 100644 --- a/beam-contract/src/tests/contract_tests.rs +++ b/beam-contract/src/tests/contract_tests.rs @@ -4,7 +4,7 @@ extern crate std; use crate::{BeamOracleContract, BeamOracleContractClient}; use oracle::testutils::register_token; use oracle::types::{Asset, FeeConfig}; -use oracle::{assets, init_contract_with_admin}; +use oracle::{assets, init_contract_with_admin, timestamps}; use soroban_sdk::{testutils::Address as _, Address, Vec}; use test_case::test_case; @@ -87,9 +87,18 @@ fn check_extending_asset_ttl() { let sponsor = Address::generate(&env); fee_token_client.mint(&sponsor, &10_000_000); - //check the extending - client.extend_asset_ttl(&sponsor, &new_asset, &1_000_000); - assert_eq!(client.expires(&new_asset), Some(87_300)); + //estimate bump cost for 1 day + let day = 24 * 60 * 60u64; + let amount = client.estimate_retention_cost(&day); + assert_eq!(amount.0, fee_token_client.address); + assert_eq!(amount.1, 1_000_000); + + //check bump + let current_expiration = client.expires(&new_asset).unwrap(); + assert_eq!(current_expiration, 0); + let ttl = client.extend_asset_ttl(&sponsor, &new_asset, &amount.1); + assert_eq!(ttl, client.expires(&new_asset).unwrap()); + assert_eq!(ttl, timestamps::ledger_timestamp(&env) / 1000 + day); //check that expiration records length matches assets length env.as_contract(&client.address, || { diff --git a/oracle/src/assets.rs b/oracle/src/assets.rs index 27fa29f..71c4df3 100644 --- a/oracle/src/assets.rs +++ b/oracle/src/assets.rs @@ -7,6 +7,7 @@ const ASSET_LIMIT: u32 = 256; //storage keys const ASSETS_KEY: &str = "assets"; const EXPIRATION_KEY: &str = "expiration"; +const DAY: i128 = 86400000; fn get_expiration_timestamp(e: &Env, initial_expiration_period: u32) -> u64 { if initial_expiration_period > 0 { @@ -88,7 +89,7 @@ pub fn extend_ttl( asset: Asset, amount: i128, initial_expiration_period: u32, -) { +) -> u64 { //check if the amount is valid if amount <= 0 { e.panic_with_error(Error::InvalidAmount); @@ -100,24 +101,14 @@ pub fn extend_ttl( } let asset_index = asset_index.unwrap(); //load required fee amount from retention config - let (xrf, fee) = match settings::get_fee_config(e) { - FeeConfig::Some(fee_data) => { - if fee_data.1 <= 0 { - e.panic_with_error(Error::InvalidConfig); - } - fee_data - } - FeeConfig::None => { - e.panic_with_error(Error::InvalidConfig); - } - }; - //burn corresponding amount of fee tokens - TokenClient::new(&e, &xrf).burn(&sponsor, &amount); + let (xrf, fee) = load_fee_settings(e); //calculate extension period - let bump = amount * 86400000 / fee; // in milliseconds + let bump = amount * DAY / fee; // in milliseconds if bump <= 0 { e.panic_with_error(Error::InvalidAmount); } + //burn corresponding amount of fee tokens + TokenClient::new(&e, &xrf).burn(&sponsor, &amount); //load expiration info let mut expiration = load_expiration_records(e); let now = timestamps::ledger_timestamp(&e); @@ -133,7 +124,32 @@ pub fn extend_ttl( //write into the vector that holds expiration dates for all symbols expiration.set(asset_index, asset_expiration); //update expiration records in instance storage - set_expirations_records(e, &expiration) + set_expirations_records(e, &expiration); + //return current asset TTL + asset_expiration +} + +// Estimate amount of fee tokens required to bump the retention for a given time (in milliseconds) +pub fn estimate_retention_cost(e: &Env, bump: u64) -> (Address, i128) { + //load daily retention cost from config + let (xrf, fee) = load_fee_settings(e); + let amount = bump as i128 * fee / DAY; + (xrf, amount) +} + +// Load current asset retention fee settings +fn load_fee_settings(e: &Env) -> (Address, i128) { + match settings::get_fee_config(e) { + FeeConfig::Some(fee_data) => { + if fee_data.1 <= 0 { + e.panic_with_error(Error::InvalidConfig); + } + fee_data + } + FeeConfig::None => { + e.panic_with_error(Error::InvalidConfig); + } + } } // Load expiration data for all assets diff --git a/oracle/src/price_oracle.rs b/oracle/src/price_oracle.rs index 946a171..f1e1d30 100644 --- a/oracle/src/price_oracle.rs +++ b/oracle/src/price_oracle.rs @@ -96,7 +96,7 @@ impl PriceOracleContractBase { // // # Returns // - // Asset expiration timestamp (in seconds) or None if asset is not supported + // Asset expiration timestamp (in seconds) or None if asset is expired // // # Panics // @@ -108,6 +108,23 @@ impl PriceOracleContractBase { } } + // Estimates amount of fee tokens required to extend the asset retention config for a given time + // + // # Arguments + // + // * `period` - Desired retention period extension (in seconds) + // + // # Returns + // + // Fee asset and estimated amount required for the fee bump + // + // # Panics + // + // Panics if the asset is not supported or if retention config is malformed/missing + pub fn estimate_retention_cost(e: &Env, period: u64) -> (Address, i128) { + assets::estimate_retention_cost(e, period * 1000) + } + // Extends the asset expiration date by a given amount of tokens. // // # Arguments @@ -117,6 +134,10 @@ impl PriceOracleContractBase { // * `amount` - Amount of tokens to burn for extending the expiration date // * `initial_expiration_period` - Initial expiration period for new assets (in days) // + // # Returns + // + // Current asset expiration timestamp (in seconds) + // // # Panics // // Panics if the asset is not supported or if retention config is malformed/missing @@ -126,10 +147,11 @@ impl PriceOracleContractBase { asset: Asset, amount: i128, initial_expiration_period: u32, - ) { + ) -> u64 { //check sponsor authorization sponsor.require_auth(); - assets::extend_ttl(e, sponsor, asset, amount, initial_expiration_period); + //extend and return current TTL + assets::extend_ttl(e, sponsor, asset, amount, initial_expiration_period) / 1000 } // Return the fee token address daily price feed retainer fee amount diff --git a/pulse-contract/src/lib.rs b/pulse-contract/src/lib.rs index deff1b7..2964384 100644 --- a/pulse-contract/src/lib.rs +++ b/pulse-contract/src/lib.rs @@ -99,6 +99,23 @@ impl PulseOracleContract { PriceOracleContractBase::expires(e, asset) } + // Estimates amount of fee tokens required to extend the asset retention config for a given time + // + // # Arguments + // + // * `period` - Desired retention period extension (in seconds) + // + // # Returns + // + // Fee asset and estimated amount required for the fee bump + // + // # Panics + // + // Panics if the asset is not supported or if retention config is malformed/missing + pub fn estimate_retention_cost(e: &Env, period: u64) -> (Address, i128) { + PriceOracleContractBase::estimate_retention_cost(e, period) + } + // Extends the asset expiration date by a given amount of tokens. // // # Arguments @@ -107,17 +124,21 @@ impl PulseOracleContract { // * `asset` - Quoted asset // * `amount` - Amount of tokens to burn for extending the expiration date // + // # Returns + // + // Current asset expiration timestamp (in seconds) + // // # Panics // // Panics if the asset is not supported or if retention config is malformed/missing - pub fn extend_asset_ttl(e: &Env, sponsor: Address, asset: Asset, amount: i128) { + pub fn extend_asset_ttl(e: &Env, sponsor: Address, asset: Asset, amount: i128) -> u64 { PriceOracleContractBase::extend_asset_ttl( e, sponsor, asset, amount, INITIAL_EXPIRATION_PERIOD, - ); + ) } // Return the fee token address daily price feed retainer fee amount diff --git a/pulse-contract/src/tests/contract_interface_tests.rs b/pulse-contract/src/tests/contract_interface_tests.rs index 8447d79..ac7cc1d 100644 --- a/pulse-contract/src/tests/contract_interface_tests.rs +++ b/pulse-contract/src/tests/contract_interface_tests.rs @@ -197,10 +197,16 @@ fn extend_asset_ttl_test() { let asset = &init_data.assets.first_unchecked(); let initial_expiration = client.expires(&asset).unwrap(); - //extend TTL by 10 day (864000 seconds) - client.extend_asset_ttl(&sponsor, &asset, &10_000_000); + //estimate bump cost for 10 day (864000 seconds) + let ten_days = 10 * 24 * 60 * 60u64; + let amount = client.estimate_retention_cost(&ten_days); + assert_eq!(amount.0, fee_token.address); + assert_eq!(amount.1, 10_000_000); + + //extend TTL + let ttl = client.extend_asset_ttl(&sponsor, &asset, &amount.1); + assert_eq!(ttl, client.expires(&asset).unwrap()); //verify new expiration - let new_expiration = client.expires(&asset).unwrap(); - assert_eq!(new_expiration, initial_expiration + 864000); + assert_eq!(ttl, initial_expiration + 864000); }