diff --git a/Cargo.lock b/Cargo.lock index a8bf730f0..0b1752968 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -41,16 +41,16 @@ dependencies = [ [[package]] name = "astro-satellite-package" version = "0.1.0" -source = "git+https://github.com/astroport-fi/astroport_ibc#e0cc213c878a5e9549530175c0aff940c9c99b81" +source = "git+https://github.com/astroport-fi/astroport_ibc#ffb48ebfd7dbbc010cf86c9b02bad236c456fca0" dependencies = [ - "astroport-governance 1.2.0", + "astroport-governance 1.2.0 (git+https://github.com/astroport-fi/astroport-governance)", "cosmwasm-schema", "cosmwasm-std", ] [[package]] name = "astroport" -version = "3.4.0" +version = "3.5.0" dependencies = [ "astroport-circular-buffer", "cosmwasm-schema", @@ -97,9 +97,9 @@ dependencies = [ [[package]] name = "astroport-escrow-fee-distributor" version = "1.0.2" -source = "git+https://github.com/astroport-fi/hidden_astroport_governance?branch=main#cccde2c619bb4fc1e0a23c926b1595ab66f909c1" +source = "git+https://github.com/astroport-fi/hidden_astroport_governance?branch=main#784452baf414f13d8b9e7de461391eb765ff9043" dependencies = [ - "astroport-governance 1.4.0 (git+https://github.com/astroport-fi/hidden_astroport_governance?branch=main)", + "astroport-governance 1.2.0 (git+https://github.com/astroport-fi/hidden_astroport_governance?branch=main)", "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 0.15.1", @@ -151,7 +151,7 @@ dependencies = [ "anyhow", "astroport", "astroport-factory", - "astroport-governance 1.4.0 (git+https://github.com/astroport-fi/hidden_astroport_governance)", + "astroport-governance 1.4.0", "astroport-mocks", "astroport-native-coin-registry", "astroport-nft", @@ -196,7 +196,7 @@ dependencies = [ [[package]] name = "astroport-governance" version = "1.2.0" -source = "git+https://github.com/astroport-fi/astroport-governance?branch=feat/merge_hidden_2023_05_22#1e865abe55093d249b69b538e2d54472b643d6c7" +source = "git+https://github.com/astroport-fi/hidden_astroport_governance?branch=main#784452baf414f13d8b9e7de461391eb765ff9043" dependencies = [ "astroport", "cosmwasm-schema", @@ -207,21 +207,20 @@ dependencies = [ [[package]] name = "astroport-governance" -version = "1.4.0" -source = "git+https://github.com/astroport-fi/hidden_astroport_governance?branch=main#cccde2c619bb4fc1e0a23c926b1595ab66f909c1" +version = "1.2.0" +source = "git+https://github.com/astroport-fi/astroport-governance#f0ef7c6dde76fc77ce360262923366a5cde3c3f8" dependencies = [ "astroport", "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 0.15.1", "cw20 0.15.1", - "thiserror", ] [[package]] name = "astroport-governance" version = "1.4.0" -source = "git+https://github.com/astroport-fi/hidden_astroport_governance#cccde2c619bb4fc1e0a23c926b1595ab66f909c1" +source = "git+https://github.com/astroport-fi/hidden_astroport_governance#259fbc78d33f1b76e4213054babc95a1d9202f5c" dependencies = [ "astroport", "cosmwasm-schema", @@ -264,7 +263,7 @@ dependencies = [ "astroport", "astroport-escrow-fee-distributor", "astroport-factory", - "astroport-governance 1.4.0 (git+https://github.com/astroport-fi/hidden_astroport_governance?branch=main)", + "astroport-governance 1.2.0 (git+https://github.com/astroport-fi/hidden_astroport_governance?branch=main)", "astroport-native-coin-registry", "astroport-pair", "astroport-pair-stable", @@ -341,9 +340,9 @@ dependencies = [ [[package]] name = "astroport-nft" version = "1.0.0" -source = "git+https://github.com/astroport-fi/hidden_astroport_governance?branch=main#cccde2c619bb4fc1e0a23c926b1595ab66f909c1" +source = "git+https://github.com/astroport-fi/hidden_astroport_governance?branch=main#784452baf414f13d8b9e7de461391eb765ff9043" dependencies = [ - "astroport-governance 1.4.0 (git+https://github.com/astroport-fi/hidden_astroport_governance?branch=main)", + "astroport-governance 1.2.0 (git+https://github.com/astroport-fi/hidden_astroport_governance?branch=main)", "cosmwasm-schema", "cosmwasm-std", "cw2 0.15.1", @@ -440,7 +439,7 @@ dependencies = [ [[package]] name = "astroport-pair-concentrated" -version = "2.0.5" +version = "2.1.0" dependencies = [ "anyhow", "astroport", @@ -463,7 +462,7 @@ dependencies = [ [[package]] name = "astroport-pair-concentrated-injective" -version = "2.0.5" +version = "2.1.0" dependencies = [ "anyhow", "astroport", @@ -492,7 +491,7 @@ dependencies = [ [[package]] name = "astroport-pair-stable" -version = "3.1.1" +version = "3.2.0" dependencies = [ "anyhow", "astroport", @@ -1541,9 +1540,9 @@ checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" [[package]] name = "generator-controller" version = "1.3.0" -source = "git+https://github.com/astroport-fi/hidden_astroport_governance?branch=main#cccde2c619bb4fc1e0a23c926b1595ab66f909c1" +source = "git+https://github.com/astroport-fi/hidden_astroport_governance?branch=main#784452baf414f13d8b9e7de461391eb765ff9043" dependencies = [ - "astroport-governance 1.4.0 (git+https://github.com/astroport-fi/hidden_astroport_governance?branch=main)", + "astroport-governance 1.2.0 (git+https://github.com/astroport-fi/hidden_astroport_governance?branch=main)", "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 0.15.1", @@ -2831,9 +2830,9 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "voting-escrow" version = "1.3.0" -source = "git+https://github.com/astroport-fi/hidden_astroport_governance?branch=main#cccde2c619bb4fc1e0a23c926b1595ab66f909c1" +source = "git+https://github.com/astroport-fi/hidden_astroport_governance?branch=main#784452baf414f13d8b9e7de461391eb765ff9043" dependencies = [ - "astroport-governance 1.4.0 (git+https://github.com/astroport-fi/hidden_astroport_governance?branch=main)", + "astroport-governance 1.2.0 (git+https://github.com/astroport-fi/hidden_astroport_governance?branch=main)", "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 0.15.1", @@ -2846,9 +2845,9 @@ dependencies = [ [[package]] name = "voting-escrow-delegation" version = "1.0.0" -source = "git+https://github.com/astroport-fi/hidden_astroport_governance?branch=main#cccde2c619bb4fc1e0a23c926b1595ab66f909c1" +source = "git+https://github.com/astroport-fi/hidden_astroport_governance?branch=main#784452baf414f13d8b9e7de461391eb765ff9043" dependencies = [ - "astroport-governance 1.4.0 (git+https://github.com/astroport-fi/hidden_astroport_governance?branch=main)", + "astroport-governance 1.2.0 (git+https://github.com/astroport-fi/hidden_astroport_governance?branch=main)", "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 0.15.1", diff --git a/contracts/pair_concentrated/Cargo.toml b/contracts/pair_concentrated/Cargo.toml index fa2423b79..a8a65ff00 100644 --- a/contracts/pair_concentrated/Cargo.toml +++ b/contracts/pair_concentrated/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "astroport-pair-concentrated" -version = "2.0.5" +version = "2.1.0" authors = ["Astroport"] edition = "2021" description = "The Astroport concentrated liquidity pair" diff --git a/contracts/pair_concentrated/src/contract.rs b/contracts/pair_concentrated/src/contract.rs index b4b986d6b..b275f79ab 100644 --- a/contracts/pair_concentrated/src/contract.rs +++ b/contracts/pair_concentrated/src/contract.rs @@ -20,7 +20,7 @@ use astroport::asset::{ use astroport::common::{claim_ownership, drop_ownership_proposal, propose_new_owner}; use astroport::cosmwasm_ext::{AbsDiff, DecimalToInteger, IntegerToDecimal}; use astroport::factory::PairType; -use astroport::observation::{MIN_TRADE_SIZE, OBSERVATIONS_SIZE}; +use astroport::observation::{PrecommitObservation, MIN_TRADE_SIZE, OBSERVATIONS_SIZE}; use astroport::pair::{Cw20HookMsg, ExecuteMsg, InstantiateMsg}; use astroport::pair_concentrated::{ ConcentratedPoolParams, ConcentratedPoolUpdateParams, MigrateMsg, UpdatePoolParams, @@ -785,15 +785,19 @@ fn swap( } } - // Store time series data. - // Skipping small unsafe values which can seriously mess oracle price due to rounding errors + // Store observation from precommit data + accumulate_swap_sizes(deps.storage, &env)?; + + // Store time series data in precommit observation. + // Skipping small unsafe values which can seriously mess oracle price due to rounding errors. + // This data will be reflected in observations in the next action. if offer_asset_dec.amount >= MIN_TRADE_SIZE && swap_result.dy >= MIN_TRADE_SIZE { let (base_amount, quote_amount) = if offer_ind == 0 { (offer_asset.amount, return_amount) } else { (return_amount, offer_asset.amount) }; - accumulate_swap_sizes(deps.storage, &env, base_amount, quote_amount)?; + PrecommitObservation::save(deps.storage, &env, base_amount, quote_amount)?; } CONFIG.save(deps.storage, &config)?; diff --git a/contracts/pair_concentrated/src/utils.rs b/contracts/pair_concentrated/src/utils.rs index 4074a5b23..47a508db7 100644 --- a/contracts/pair_concentrated/src/utils.rs +++ b/contracts/pair_concentrated/src/utils.rs @@ -7,7 +7,7 @@ use itertools::Itertools; use astroport::asset::{Asset, AssetInfo, DecimalAsset}; use astroport::cosmwasm_ext::AbsDiff; -use astroport::observation::Observation; +use astroport::observation::{Observation, PrecommitObservation}; use astroport::querier::{query_factory_config, query_supply}; use astroport_circular_buffer::error::BufferResult; use astroport_circular_buffer::BufferManager; @@ -424,65 +424,82 @@ fn safe_sma_calculation( res.try_into().map_err(StdError::from) } +/// Same as [`safe_sma_calculation`] but is being used when buffer is not full yet. +fn safe_sma_buffer_not_full(sma: Uint128, count: u32, new_amount: Uint128) -> StdResult { + let res = (sma.full_mul(count) + Uint256::from(new_amount)).checked_div((count + 1).into())?; + res.try_into().map_err(StdError::from) +} + /// Calculate and save moving averages of swap sizes. -pub fn accumulate_swap_sizes( - storage: &mut dyn Storage, - env: &Env, - base_amount: Uint128, - quote_amount: Uint128, -) -> BufferResult<()> { - let mut buffer = BufferManager::new(storage, OBSERVATIONS)?; - - let new_observation; - if let Some(last_obs) = buffer.read_last(storage)? { - // Since this is circular buffer the next index contains the oldest value - let count = buffer.capacity(); - if let Some(oldest_obs) = buffer.read_single(storage, buffer.head() + 1)? { - let new_base_sma = safe_sma_calculation( - last_obs.base_sma, - oldest_obs.base_amount, - count, - base_amount, - )?; - let new_quote_sma = safe_sma_calculation( - last_obs.quote_sma, - oldest_obs.quote_amount, - count, - quote_amount, - )?; - new_observation = Observation { - base_amount, - quote_amount, - base_sma: new_base_sma, - quote_sma: new_quote_sma, - timestamp: env.block.time.seconds(), - }; +pub fn accumulate_swap_sizes(storage: &mut dyn Storage, env: &Env) -> BufferResult<()> { + if let Some(PrecommitObservation { + base_amount, + quote_amount, + precommit_ts, + }) = PrecommitObservation::may_load(storage)? + { + let mut buffer = BufferManager::new(storage, OBSERVATIONS)?; + + let new_observation; + if let Some(last_obs) = buffer.read_last(storage)? { + // Skip saving observation if it has been already saved + if last_obs.timestamp < precommit_ts { + // Since this is circular buffer the next index contains the oldest value + let count = buffer.capacity(); + if let Some(oldest_obs) = buffer.read_single(storage, buffer.head() + 1)? { + let new_base_sma = safe_sma_calculation( + last_obs.base_sma, + oldest_obs.base_amount, + count, + base_amount, + )?; + let new_quote_sma = safe_sma_calculation( + last_obs.quote_sma, + oldest_obs.quote_amount, + count, + quote_amount, + )?; + new_observation = Observation { + base_amount, + quote_amount, + base_sma: new_base_sma, + quote_sma: new_quote_sma, + timestamp: precommit_ts, + }; + } else { + // Buffer is not full yet + let count = buffer.head(); + let base_sma = safe_sma_buffer_not_full(last_obs.base_sma, count, base_amount)?; + let quote_sma = + safe_sma_buffer_not_full(last_obs.quote_sma, count, quote_amount)?; + new_observation = Observation { + base_amount, + quote_amount, + base_sma, + quote_sma, + timestamp: precommit_ts, + }; + } + + buffer.instant_push(storage, &new_observation)? + } } else { - // Buffer is not full yet - let count = Uint128::from(buffer.head()); - let new_base_sma = (last_obs.base_sma * count + base_amount) / (count + Uint128::one()); - let new_quote_sma = - (last_obs.quote_sma * count + quote_amount) / (count + Uint128::one()); - new_observation = Observation { - base_amount, - quote_amount, - base_sma: new_base_sma, - quote_sma: new_quote_sma, - timestamp: env.block.time.seconds(), - }; + // Buffer is empty + if env.block.time.seconds() > precommit_ts { + new_observation = Observation { + timestamp: precommit_ts, + base_sma: base_amount, + base_amount, + quote_sma: quote_amount, + quote_amount, + }; + + buffer.instant_push(storage, &new_observation)? + } } - } else { - // Buffer is empty - new_observation = Observation { - timestamp: env.block.time.seconds(), - base_sma: base_amount, - base_amount, - quote_sma: quote_amount, - quote_amount, - }; } - buffer.instant_push(storage, &new_observation) + Ok(()) } #[cfg(test)] @@ -492,6 +509,7 @@ mod tests { use std::str::FromStr; use cosmwasm_std::testing::{mock_env, MockStorage}; + use cosmwasm_std::{BlockInfo, Timestamp}; use super::*; @@ -539,32 +557,30 @@ mod tests { } #[test] - fn test_swap_obeservations() { + fn test_swap_observations() { let mut store = MockStorage::new(); - let env = mock_env(); + let mut env = mock_env(); + env.block.time = Timestamp::from_seconds(1); + + let next_block = |block: &mut BlockInfo| { + block.height += 1; + block.time = block.time.plus_seconds(1); + }; BufferManager::init(&mut store, OBSERVATIONS, 10).unwrap(); - for _ in 0..50 { - accumulate_swap_sizes( - &mut store, - &env, - Uint128::from(1000u128), - Uint128::from(500u128), - ) - .unwrap(); + for _ in 0..=50 { + accumulate_swap_sizes(&mut store, &env).unwrap(); + PrecommitObservation::save(&mut store, &env, 1000u128.into(), 500u128.into()).unwrap(); + next_block(&mut env.block); } let buffer = BufferManager::new(&store, OBSERVATIONS).unwrap(); + let obs = buffer.read_last(&store).unwrap().unwrap(); + assert_eq!(obs.timestamp, 50); assert_eq!(buffer.head(), 0); - assert_eq!( - buffer.read_last(&store).unwrap().unwrap().base_sma.u128(), - 1000u128 - ); - assert_eq!( - buffer.read_last(&store).unwrap().unwrap().quote_sma.u128(), - 500u128 - ); + assert_eq!(obs.base_sma.u128(), 1000u128); + assert_eq!(obs.quote_sma.u128(), 500u128); } } diff --git a/contracts/pair_concentrated/tests/helper.rs b/contracts/pair_concentrated/tests/helper.rs index cc8f87dfb..dd11184ba 100644 --- a/contracts/pair_concentrated/tests/helper.rs +++ b/contracts/pair_concentrated/tests/helper.rs @@ -307,6 +307,16 @@ impl Helper { sender: &Addr, offer_asset: &Asset, max_spread: Option, + ) -> AnyResult { + self.swap_full_params(sender, offer_asset, max_spread, None) + } + + pub fn swap_full_params( + &mut self, + sender: &Addr, + offer_asset: &Asset, + max_spread: Option, + belief_price: Option, ) -> AnyResult { match &offer_asset.info { AssetInfo::Token { contract_addr } => { @@ -315,7 +325,7 @@ impl Helper { amount: offer_asset.amount, msg: to_binary(&Cw20HookMsg::Swap { ask_asset_info: None, - belief_price: None, + belief_price, max_spread, to: None, }) @@ -336,7 +346,7 @@ impl Helper { let msg = ExecuteMsg::Swap { offer_asset: offer_asset.clone(), ask_asset_info: None, - belief_price: None, + belief_price, max_spread, to: None, }; diff --git a/contracts/pair_concentrated/tests/pair_concentrated_integration.rs b/contracts/pair_concentrated/tests/pair_concentrated_integration.rs index e331911f4..efc9ff521 100644 --- a/contracts/pair_concentrated/tests/pair_concentrated_integration.rs +++ b/contracts/pair_concentrated/tests/pair_concentrated_integration.rs @@ -1,23 +1,22 @@ #![cfg(not(tarpaulin_include))] -use astroport_mocks::{astroport_address, MockConcentratedPairBuilder, MockGeneratorBuilder}; -use cosmwasm_std::{Addr, Coin, Decimal, StdError, Uint128}; - -use astroport_mocks::cw_multi_test::{BasicApp, Executor}; use std::cell::RefCell; use std::rc::Rc; use std::str::FromStr; +use cosmwasm_std::{Addr, Coin, Decimal, StdError, Uint128}; + use astroport::asset::{ native_asset_info, Asset, AssetInfo, AssetInfoExt, MINIMUM_LIQUIDITY_AMOUNT, }; use astroport::cosmwasm_ext::AbsDiff; use astroport::observation::OracleObservation; - use astroport::pair::{ExecuteMsg, PoolResponse}; use astroport::pair_concentrated::{ ConcentratedPoolParams, ConcentratedPoolUpdateParams, PromoteParams, QueryMsg, UpdatePoolParams, }; +use astroport_mocks::cw_multi_test::{BasicApp, Executor}; +use astroport_mocks::{astroport_address, MockConcentratedPairBuilder, MockGeneratorBuilder}; use astroport_pair_concentrated::consts::{AMP_MAX, AMP_MIN, MA_HALF_TIME_LIMITS}; use astroport_pair_concentrated::error::ContractError; @@ -88,7 +87,7 @@ fn check_observe_queries() { res, OracleObservation { timestamp: helper.app.block_info().time.seconds(), - price: Decimal::from_str("0.99741246").unwrap() + price: Decimal::from_str("1.002627596167552265").unwrap() } ); } @@ -266,7 +265,7 @@ fn provide_and_withdraw() { ); let err = helper - .provide_liquidity(&user1, &[random_coin]) + .provide_liquidity(&user1, &[random_coin.clone()]) .unwrap_err(); assert_eq!( "The asset random-coin does not belong to the pair", @@ -279,6 +278,22 @@ fn provide_and_withdraw() { err.root_cause().to_string() ); + // Try to provide 3 assets + let err = helper + .provide_liquidity( + &user1, + &[ + random_coin.clone(), + helper.assets[&test_coins[0]].with_balance(1u8), + helper.assets[&test_coins[1]].with_balance(1u8), + ], + ) + .unwrap_err(); + assert_eq!( + ContractError::InvalidNumberOfAssets(2), + err.downcast().unwrap() + ); + // Try to provide with zero amount let err = helper .provide_liquidity( @@ -299,6 +314,23 @@ fn provide_and_withdraw() { &[helper.assets[&test_coins[1]].with_balance(50_000_000000u128)], &user1, ); + + // Test very small initial provide + let err = helper + .provide_liquidity( + &user1, + &[ + helper.assets[&test_coins[0]].with_balance(1000u128), + helper.assets[&test_coins[1]].with_balance(500u128), + ], + ) + .unwrap_err(); + assert_eq!( + ContractError::MinimumLiquidityAmountError {}, + err.downcast().unwrap() + ); + + // This is normal provision helper.provide_liquidity(&user1, &assets).unwrap(); assert_eq!(70710_677118, helper.token_balance(&helper.lp_token, &user1)); @@ -668,6 +700,24 @@ fn check_swaps_simple() { ]; helper.provide_liquidity(&owner, &assets).unwrap(); + // trying to swap cw20 without calling Cw20::Send method + let err = helper + .app + .execute_contract( + owner.clone(), + helper.pair_addr.clone(), + &ExecuteMsg::Swap { + offer_asset: helper.assets[&test_coins[1]].with_balance(1u8), + ask_asset_info: None, + belief_price: None, + max_spread: None, + to: None, + }, + &[], + ) + .unwrap_err(); + assert_eq!(ContractError::Cw20DirectSwap {}, err.downcast().unwrap()); + let d = helper.query_d().unwrap(); assert_eq!(dec_to_f64(d), 200000f64); @@ -1038,6 +1088,39 @@ fn update_owner() { .unwrap_err(); assert_eq!(err.root_cause().to_string(), "Generic error: Unauthorized"); + // Drop ownership proposal + let err = helper + .app + .execute_contract( + Addr::unchecked("invalid_addr"), + helper.pair_addr.clone(), + &ExecuteMsg::DropOwnershipProposal {}, + &[], + ) + .unwrap_err(); + assert_eq!(err.root_cause().to_string(), "Generic error: Unauthorized"); + + helper + .app + .execute_contract( + helper.owner.clone(), + helper.pair_addr.clone(), + &ExecuteMsg::DropOwnershipProposal {}, + &[], + ) + .unwrap(); + + // Propose new owner + helper + .app + .execute_contract( + Addr::unchecked(&helper.owner), + helper.pair_addr.clone(), + &msg, + &[], + ) + .unwrap(); + // Claim ownership helper .app @@ -1539,3 +1622,89 @@ fn provide_withdraw_slippage() { .provide_liquidity_with_slip_tolerance(&owner, &assets, Some(f64_to_dec(0.5))) .unwrap(); } + +#[test] +fn test_frontrun_before_initial_provide() { + let owner = Addr::unchecked("owner"); + + let test_coins = vec![TestCoin::native("uusd"), TestCoin::native("uluna")]; + + let params = ConcentratedPoolParams { + amp: f64_to_dec(10f64), + gamma: f64_to_dec(0.000145), + mid_fee: f64_to_dec(0.0026), + out_fee: f64_to_dec(0.0045), + fee_gamma: f64_to_dec(0.00023), + repeg_profit_threshold: f64_to_dec(0.000002), + min_price_scale_delta: f64_to_dec(0.000146), + price_scale: Decimal::from_ratio(10u8, 1u8), + ma_half_time: 600, + track_asset_balances: None, + }; + + let mut helper = Helper::new(&owner, test_coins.clone(), params).unwrap(); + + // Random person tries to frontrun initial provide and imbalance pool upfront + helper + .app + .send_tokens( + owner.clone(), + helper.pair_addr.clone(), + &[helper.assets[&test_coins[0]] + .with_balance(10_000_000000u128) + .as_coin() + .unwrap()], + ) + .unwrap(); + + // Fully balanced provide + let assets = vec![ + helper.assets[&test_coins[0]].with_balance(10_000000u128), + helper.assets[&test_coins[1]].with_balance(1_000000u128), + ]; + helper.provide_liquidity(&owner, &assets).unwrap(); + // Now pool became imbalanced with value (10010, 1) (or in internal representation (10010, 10)) + // while price scale stays 10 + + let arber = Addr::unchecked("arber"); + let offer_asset_luna = helper.assets[&test_coins[1]].with_balance(1_000000u128); + // Arber spinning pool back to balanced state + loop { + helper.app.next_block(10); + helper.give_me_money(&[offer_asset_luna.clone()], &arber); + // swapping until price satisfies an arber + if helper + .swap_full_params( + &arber, + &offer_asset_luna, + Some(f64_to_dec(0.02)), + Some(f64_to_dec(0.1)), // imagine market price is 10 -> i.e. inverted price is 1/10 + ) + .is_err() + { + break; + } + } + + // price scale changed, however it isn't equal to 10 because of repegging + // But next swaps will align price back to the market value + let config = helper.query_config().unwrap(); + let price_scale = config.pool_state.price_state.price_scale; + assert!( + dec_to_f64(price_scale) - 77.255853 < 1e-5, + "price_scale: {price_scale} is far from expected price", + ); + + // Arber collected significant profit (denominated in uusd) + // Essentially 10_000 - fees (which settled in the pool) + let arber_balance = helper.coin_balance(&test_coins[0], &arber); + assert_eq!(arber_balance, 9667_528248); + + // Pool's TVL increased from (10, 1) i.e. 20 to (320, 32) i.e. 640 considering market price is 10.0 + let pools = config + .pair_info + .query_pools(&helper.app.wrap(), &helper.pair_addr) + .unwrap(); + assert_eq!(pools[0].amount.u128(), 320_624088); + assert_eq!(pools[1].amount.u128(), 32_000000); +} diff --git a/contracts/pair_concentrated_inj/Cargo.toml b/contracts/pair_concentrated_inj/Cargo.toml index 212516ab7..e7c22f10e 100644 --- a/contracts/pair_concentrated_inj/Cargo.toml +++ b/contracts/pair_concentrated_inj/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "astroport-pair-concentrated-injective" -version = "2.0.5" +version = "2.1.0" authors = ["Astroport"] edition = "2021" description = "The Astroport concentrated liquidity pair which supports Injective orderbook integration" diff --git a/contracts/pair_concentrated_inj/src/contract.rs b/contracts/pair_concentrated_inj/src/contract.rs index eec92932f..cfa4f636b 100644 --- a/contracts/pair_concentrated_inj/src/contract.rs +++ b/contracts/pair_concentrated_inj/src/contract.rs @@ -18,7 +18,7 @@ use astroport::asset::{ use astroport::common::{claim_ownership, drop_ownership_proposal, propose_new_owner}; use astroport::cosmwasm_ext::{AbsDiff, DecimalToInteger, IntegerToDecimal}; use astroport::factory::PairType; -use astroport::observation::{MIN_TRADE_SIZE, OBSERVATIONS_SIZE}; +use astroport::observation::{PrecommitObservation, MIN_TRADE_SIZE, OBSERVATIONS_SIZE}; use astroport::pair::{Cw20HookMsg, InstantiateMsg}; use astroport::pair_concentrated::UpdatePoolParams; use astroport::pair_concentrated_inj::{ @@ -828,15 +828,19 @@ where } } - // Store time series data. - // Skipping small unsafe values which can seriously mess oracle price due to rounding errors + // Store observation from precommit data + accumulate_swap_sizes(deps.storage, &env, &mut ob_state)?; + + // Store time series data in precommit observation. + // Skipping small unsafe values which can seriously mess oracle price due to rounding errors. + // This data will be reflected in observations in the next action. if offer_asset_dec.amount >= MIN_TRADE_SIZE && swap_result.dy >= MIN_TRADE_SIZE { let (base_amount, quote_amount) = if offer_ind == 0 { (offer_asset.amount, return_amount) } else { (return_amount, offer_asset.amount) }; - accumulate_swap_sizes(deps.storage, &env, &mut ob_state, base_amount, quote_amount)?; + PrecommitObservation::save(deps.storage, &env, base_amount, quote_amount)?; } CONFIG.save(deps.storage, &config)?; diff --git a/contracts/pair_concentrated_inj/src/migrate.rs b/contracts/pair_concentrated_inj/src/migrate.rs index 2d2710b16..3d84cbaae 100644 --- a/contracts/pair_concentrated_inj/src/migrate.rs +++ b/contracts/pair_concentrated_inj/src/migrate.rs @@ -13,7 +13,7 @@ use astroport_pair_concentrated::state::Config as CLConfig; use crate::state::{AmpGamma, Config, PoolParams, PoolState, PriceState, CONFIG}; const MIGRATE_FROM: &str = "astroport-pair-concentrated"; -const MIGRATION_VERSION: &str = "2.0.5"; +const MIGRATION_VERSION: &str = "2.1.0"; /// Manages the contract migration. #[cfg_attr(not(feature = "library"), entry_point)] diff --git a/contracts/pair_concentrated_inj/src/utils.rs b/contracts/pair_concentrated_inj/src/utils.rs index 606304117..0cd26b8ca 100644 --- a/contracts/pair_concentrated_inj/src/utils.rs +++ b/contracts/pair_concentrated_inj/src/utils.rs @@ -8,7 +8,7 @@ use itertools::Itertools; use astroport::asset::{Asset, AssetInfo, DecimalAsset}; use astroport::cosmwasm_ext::{AbsDiff, IntegerToDecimal}; -use astroport::observation::Observation; +use astroport::observation::{Observation, PrecommitObservation}; use astroport::querier::query_factory_config; use astroport_circular_buffer::error::BufferResult; use astroport_circular_buffer::BufferManager; @@ -393,71 +393,91 @@ fn safe_sma_calculation( res.try_into().map_err(StdError::from) } +/// Same as [`safe_sma_calculation`] but is being used when buffer is not full yet. +fn safe_sma_buffer_not_full(sma: Uint128, count: u32, new_amount: Uint128) -> StdResult { + let res = (sma.full_mul(count) + Uint256::from(new_amount)).checked_div((count + 1).into())?; + res.try_into().map_err(StdError::from) +} + /// Calculate and save moving averages of swap sizes. pub fn accumulate_swap_sizes( storage: &mut dyn Storage, env: &Env, ob_state: &mut OrderbookState, - base_amount: Uint128, - quote_amount: Uint128, ) -> BufferResult<()> { - let mut buffer = BufferManager::new(storage, OBSERVATIONS)?; - - let new_observation; - if let Some(last_obs) = buffer.read_last(storage)? { - // Since this is circular buffer the next index contains the oldest value - let count = buffer.capacity(); - if let Some(oldest_obs) = buffer.read_single(storage, buffer.head() + 1)? { - let new_base_sma = safe_sma_calculation( - last_obs.base_sma, - oldest_obs.base_amount, - count, - base_amount, - )?; - let new_quote_sma = safe_sma_calculation( - last_obs.quote_sma, - oldest_obs.quote_amount, - count, - quote_amount, - )?; - new_observation = Observation { - base_amount, - quote_amount, - base_sma: new_base_sma, - quote_sma: new_quote_sma, - timestamp: env.block.time.seconds(), - }; + if let Some(PrecommitObservation { + base_amount, + quote_amount, + precommit_ts, + }) = PrecommitObservation::may_load(storage)? + { + let mut buffer = BufferManager::new(storage, OBSERVATIONS)?; + + let new_observation; + if let Some(last_obs) = buffer.read_last(storage)? { + // Skip saving observation if it has been already saved + if last_obs.timestamp < precommit_ts { + // Since this is circular buffer the next index contains the oldest value + let count = buffer.capacity(); + if let Some(oldest_obs) = buffer.read_single(storage, buffer.head() + 1)? { + let new_base_sma = safe_sma_calculation( + last_obs.base_sma, + oldest_obs.base_amount, + count, + base_amount, + )?; + let new_quote_sma = safe_sma_calculation( + last_obs.quote_sma, + oldest_obs.quote_amount, + count, + quote_amount, + )?; + new_observation = Observation { + base_amount, + quote_amount, + base_sma: new_base_sma, + quote_sma: new_quote_sma, + timestamp: precommit_ts, + }; + } else { + // Buffer is not full yet + let count = buffer.head(); + let base_sma = safe_sma_buffer_not_full(last_obs.base_sma, count, base_amount)?; + let quote_sma = + safe_sma_buffer_not_full(last_obs.quote_sma, count, quote_amount)?; + new_observation = Observation { + base_amount, + quote_amount, + base_sma, + quote_sma, + timestamp: precommit_ts, + }; + } + + // Enable orderbook if we have enough observations + if !ob_state.ready && (buffer.head() + 1) >= ob_state.min_trades_to_avg { + ob_state.ready(true) + } + + buffer.instant_push(storage, &new_observation)? + } } else { - // Buffer is not full yet - let count = Uint128::from(buffer.head()); - let new_base_sma = (last_obs.base_sma * count + base_amount) / (count + Uint128::one()); - let new_quote_sma = - (last_obs.quote_sma * count + quote_amount) / (count + Uint128::one()); - new_observation = Observation { - base_amount, - quote_amount, - base_sma: new_base_sma, - quote_sma: new_quote_sma, - timestamp: env.block.time.seconds(), - }; + // Buffer is empty + if env.block.time.seconds() > precommit_ts { + new_observation = Observation { + timestamp: precommit_ts, + base_sma: base_amount, + base_amount, + quote_sma: quote_amount, + quote_amount, + }; + + buffer.instant_push(storage, &new_observation)? + } } - - // Enable orderbook if we have enough observations - if !ob_state.ready && (buffer.head() + 1) >= ob_state.min_trades_to_avg { - ob_state.ready(true) - } - } else { - // Buffer is empty - new_observation = Observation { - timestamp: env.block.time.seconds(), - base_sma: base_amount, - base_amount, - quote_sma: quote_amount, - quote_amount, - }; } - buffer.instant_push(storage, &new_observation) + Ok(()) } /// Calculate provide fee applied on the amount of LP tokens. Only charged for imbalanced provide. @@ -522,10 +542,12 @@ mod tests { use std::fmt::Display; use std::str::FromStr; - use crate::orderbook::consts::MIN_TRADES_TO_AVG_LIMITS; use cosmwasm_std::testing::{mock_env, MockStorage}; + use cosmwasm_std::{BlockInfo, Timestamp}; use injective_cosmwasm::{MarketId, SubaccountId}; + use crate::orderbook::consts::MIN_TRADES_TO_AVG_LIMITS; + use super::*; pub fn f64_to_dec(val: f64) -> T @@ -571,10 +593,16 @@ mod tests { assert_eq!(dec_to_f64(fee_rate), 0.002205); } + fn next_block(block: &mut BlockInfo) { + block.height += 1; + block.time = block.time.plus_seconds(1); + } + #[test] - fn test_swap_obeservations() { + fn test_swap_observations() { let mut store = MockStorage::new(); - let env = mock_env(); + let mut env = mock_env(); + env.block.time = Timestamp::from_seconds(1); let mut ob_state = OrderbookState { market_id: MarketId::unchecked("test"), subaccount: SubaccountId::unchecked("test"), @@ -590,34 +618,25 @@ mod tests { }; BufferManager::init(&mut store, OBSERVATIONS, 10).unwrap(); - for _ in 0..50 { - accumulate_swap_sizes( - &mut store, - &env, - &mut ob_state, - Uint128::from(1000u128), - Uint128::from(500u128), - ) - .unwrap(); + for _ in 0..=50 { + accumulate_swap_sizes(&mut store, &env, &mut ob_state).unwrap(); + PrecommitObservation::save(&mut store, &env, 1000u128.into(), 500u128.into()).unwrap(); + next_block(&mut env.block); } let buffer = BufferManager::new(&store, OBSERVATIONS).unwrap(); + let obs = buffer.read_last(&store).unwrap().unwrap(); + assert_eq!(obs.timestamp, 50); assert_eq!(buffer.head(), 0); - assert_eq!( - buffer.read_last(&store).unwrap().unwrap().base_sma.u128(), - 1000u128 - ); - assert_eq!( - buffer.read_last(&store).unwrap().unwrap().quote_sma.u128(), - 500u128 - ); + assert_eq!(obs.base_sma.u128(), 1000u128); + assert_eq!(obs.quote_sma.u128(), 500u128); } #[test] fn test_contract_ready() { let mut store = MockStorage::new(); - let env = mock_env(); + let mut env = mock_env(); let min_trades_to_avg = 10; let mut ob_state = OrderbookState { market_id: MarketId::unchecked("test"), @@ -634,27 +653,15 @@ mod tests { }; BufferManager::init(&mut store, OBSERVATIONS, min_trades_to_avg).unwrap(); - for _ in 0..min_trades_to_avg - 1 { - accumulate_swap_sizes( - &mut store, - &env, - &mut ob_state, - Uint128::from(1000u128), - Uint128::from(500u128), - ) - .unwrap(); + for _ in 0..min_trades_to_avg { + accumulate_swap_sizes(&mut store, &env, &mut ob_state).unwrap(); + PrecommitObservation::save(&mut store, &env, 1000u128.into(), 500u128.into()).unwrap(); + next_block(&mut env.block); } assert!(!ob_state.ready, "Contract should not be ready yet"); // last observation to make contract ready - accumulate_swap_sizes( - &mut store, - &env, - &mut ob_state, - Uint128::from(1000u128), - Uint128::from(500u128), - ) - .unwrap(); + accumulate_swap_sizes(&mut store, &env, &mut ob_state).unwrap(); assert!(ob_state.ready, "Contract should be ready"); } diff --git a/contracts/pair_concentrated_inj/tests/pair_inj_concentrated_integration.rs b/contracts/pair_concentrated_inj/tests/pair_inj_concentrated_integration.rs index 62d32b472..9d7fddb57 100644 --- a/contracts/pair_concentrated_inj/tests/pair_inj_concentrated_integration.rs +++ b/contracts/pair_concentrated_inj/tests/pair_inj_concentrated_integration.rs @@ -1016,6 +1016,7 @@ fn check_orderbook_integration() { None, ) .unwrap(); + helper.next_block(false).unwrap(); } let err = helper @@ -1028,7 +1029,7 @@ fn check_orderbook_integration() { let ob_state = helper.query_ob_config_smart().unwrap(); assert_eq!(ob_state.orders_number, 5); - assert_eq!(ob_state.need_reconcile, true); + assert_eq!(ob_state.need_reconcile, false); // sudo endpoint was already executed and liq. deployed in OB assert_eq!(ob_state.ready, true); let ob_config = helper.query_ob_config().unwrap(); @@ -1047,14 +1048,14 @@ fn check_orderbook_integration() { .deposits .total_balance .into(); - assert_eq!(inj_deposit, 2489_766000000000000000); - assert_eq!(astro_deposit, 4979_553543); + assert_eq!(inj_deposit, 2489_981000000000000000); + assert_eq!(astro_deposit, 4979_051501); let inj_pool = helper.coin_balance(&test_coins[0], &helper.pair_addr); let astro_pool = helper.coin_balance(&test_coins[1], &helper.pair_addr); - assert_eq!(inj_pool, 497543_148893233248565365); - assert_eq!(astro_pool, 995084_822997); + assert_eq!(inj_pool, 497542_933893233248565365); + assert_eq!(astro_pool, 995085_325039); // total liquidity is close to initial provided liquidity let total_inj = inj_deposit + inj_pool; @@ -1624,6 +1625,7 @@ fn test_migrate_cl_to_orderbook_cl() { None, ) .unwrap(); + helper.next_block(true).unwrap(); } let migrate_msg = MigrateMsg::MigrateToOrderbook { @@ -1701,6 +1703,7 @@ fn test_migrate_cl_to_orderbook_cl() { None, ) .unwrap(); + helper.next_block(true).unwrap(); } // Check that orders have been created @@ -1720,8 +1723,8 @@ fn test_migrate_cl_to_orderbook_cl() { .deposits .total_balance .into(); - assert_eq!(inj_deposit, 2489_761000000000000000); - assert_eq!(astro_deposit, 4979_563465); + assert_eq!(inj_deposit, 2489_976000000000000000); + assert_eq!(astro_deposit, 4979_061419); let inj_pool = helper.coin_balance(&test_coins[0], &helper.pair_addr); let astro_pool = helper.coin_balance(&test_coins[1], &helper.pair_addr); diff --git a/contracts/pair_stable/Cargo.toml b/contracts/pair_stable/Cargo.toml index cab2aba2f..513646947 100644 --- a/contracts/pair_stable/Cargo.toml +++ b/contracts/pair_stable/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "astroport-pair-stable" -version = "3.1.1" +version = "3.2.0" authors = ["Astroport"] edition = "2021" description = "The Astroport stableswap pair contract implementation" diff --git a/contracts/pair_stable/src/contract.rs b/contracts/pair_stable/src/contract.rs index 980664041..3cec5ece0 100644 --- a/contracts/pair_stable/src/contract.rs +++ b/contracts/pair_stable/src/contract.rs @@ -28,7 +28,9 @@ use astroport::pair::{ }; use crate::migration::{migrate_config_from_v21, migrate_config_to_v210}; -use astroport::observation::{query_observation, MIN_TRADE_SIZE, OBSERVATIONS_SIZE}; +use astroport::observation::{ + query_observation, PrecommitObservation, MIN_TRADE_SIZE, OBSERVATIONS_SIZE, +}; use astroport::pair::{ Cw20HookMsg, ExecuteMsg, MigrateMsg, PoolResponse, QueryMsg, ReverseSimulationResponse, SimulationResponse, StablePoolConfig, @@ -669,8 +671,12 @@ pub fn swap( } } - // Store time series data. - // Skipping small unsafe values which can seriously mess oracle price due to rounding errors + // Store observation from precommit data + accumulate_swap_sizes(deps.storage, &env)?; + + // Store time series data in precommit observation. + // Skipping small unsafe values which can seriously mess oracle price due to rounding errors. + // This data will be reflected in observations in the next action. let ask_precision = get_precision(deps.storage, &ask_pool.info)?; if offer_asset_dec.amount >= MIN_TRADE_SIZE && return_amount.to_decimal256(ask_precision)? >= MIN_TRADE_SIZE @@ -678,7 +684,7 @@ pub fn swap( // Store time series data let (base_amount, quote_amount) = determine_base_quote_amount(&pools, &offer_asset, return_amount)?; - accumulate_swap_sizes(deps.storage, &env, base_amount, quote_amount)?; + PrecommitObservation::save(deps.storage, &env, base_amount, quote_amount)?; } Ok(Response::new() diff --git a/contracts/pair_stable/src/utils.rs b/contracts/pair_stable/src/utils.rs index f9ac5896f..eb6a0f2e7 100644 --- a/contracts/pair_stable/src/utils.rs +++ b/contracts/pair_stable/src/utils.rs @@ -1,13 +1,14 @@ +use std::cmp::Ordering; + use cosmwasm_std::{ - to_binary, wasm_execute, Addr, Api, CosmosMsg, Decimal, Env, QuerierWrapper, StdError, - StdResult, Storage, Uint128, Uint256, Uint64, + to_binary, wasm_execute, Addr, Api, CosmosMsg, Decimal, Env, QuerierWrapper, StdResult, + Storage, Uint128, Uint64, }; use cw20::Cw20ExecuteMsg; use itertools::Itertools; -use std::cmp::Ordering; use astroport::asset::{Asset, AssetInfo, Decimal256Ext, DecimalAsset}; -use astroport::observation::Observation; +use astroport::observation::{Observation, PrecommitObservation}; use astroport::querier::query_factory_config; use astroport_circular_buffer::error::BufferResult; use astroport_circular_buffer::BufferManager; @@ -291,77 +292,45 @@ pub(crate) fn compute_swap( } /// Calculate and save moving averages of swap sizes. -pub fn accumulate_swap_sizes( - storage: &mut dyn Storage, - env: &Env, - base_amount: Uint128, - quote_amount: Uint128, -) -> BufferResult<()> { - let mut buffer = BufferManager::new(storage, OBSERVATIONS)?; - - let new_observation; - if let Some(last_obs) = buffer.read_last(storage)? { - // Since this is circular buffer the next index contains the oldest value - let count = buffer.capacity(); - if let Some(oldest_obs) = buffer.read_single(storage, buffer.head() + 1)? { - let new_base_sma = safe_sma_calculation( - last_obs.base_sma, - oldest_obs.base_amount, - count, - base_amount, - )?; - let new_quote_sma = safe_sma_calculation( - last_obs.quote_sma, - oldest_obs.quote_amount, - count, - quote_amount, - )?; - new_observation = Observation { - base_amount, - quote_amount, - base_sma: new_base_sma, - quote_sma: new_quote_sma, - timestamp: env.block.time.seconds(), - }; +pub fn accumulate_swap_sizes(storage: &mut dyn Storage, env: &Env) -> BufferResult<()> { + if let Some(PrecommitObservation { + base_amount, + quote_amount, + precommit_ts, + }) = PrecommitObservation::may_load(storage)? + { + let mut buffer = BufferManager::new(storage, OBSERVATIONS)?; + + if let Some(last_obs) = buffer.read_last(storage)? { + // Skip saving observation if it has been already saved + if last_obs.timestamp < precommit_ts { + buffer.instant_push( + storage, + &Observation { + base_amount, + quote_amount, + timestamp: precommit_ts, + ..Default::default() + }, + )? + } } else { - // Buffer is not full yet - let count = Uint128::from(buffer.head()); - let new_base_sma = (last_obs.base_sma * count + base_amount) / (count + Uint128::one()); - let new_quote_sma = - (last_obs.quote_sma * count + quote_amount) / (count + Uint128::one()); - new_observation = Observation { - base_amount, - quote_amount, - base_sma: new_base_sma, - quote_sma: new_quote_sma, - timestamp: env.block.time.seconds(), - }; + // Buffer is empty + if env.block.time.seconds() > precommit_ts { + buffer.instant_push( + storage, + &Observation { + timestamp: precommit_ts, + base_amount, + quote_amount, + ..Default::default() + }, + )? + } } - } else { - // Buffer is empty - new_observation = Observation { - timestamp: env.block.time.seconds(), - base_sma: base_amount, - base_amount, - quote_sma: quote_amount, - quote_amount, - }; } - buffer.instant_push(storage, &new_observation) -} - -/// Internal function to calculate new moving average using Uint256. -/// Overflow is possible only if new average order size is greater than 2^128 - 1 which is unlikely. -fn safe_sma_calculation( - sma: Uint128, - oldest_amount: Uint128, - count: u32, - new_amount: Uint128, -) -> StdResult { - let res = (sma.full_mul(count) + Uint256::from(new_amount) - Uint256::from(oldest_amount)) - .checked_div(count.into())?; - res.try_into().map_err(StdError::from) + Ok(()) } /// Internal function to determine which asset is base one, which is quote one diff --git a/contracts/pair_stable/tests/stablepool_tests.rs b/contracts/pair_stable/tests/stablepool_tests.rs index 91288f7d2..80fc662ad 100644 --- a/contracts/pair_stable/tests/stablepool_tests.rs +++ b/contracts/pair_stable/tests/stablepool_tests.rs @@ -464,7 +464,18 @@ fn check_pool_prices() { Some(helper.assets[&test_coins[0]].clone()), ) .unwrap(); + + // One more swap to trigger price update in the next step + helper + .swap( + &owner, + &offer_asset, + Some(helper.assets[&test_coins[0]].clone()), + ) + .unwrap(); + helper.app.next_block(86400); + assert_eq!( helper.query_observe(0).unwrap(), OracleObservation { diff --git a/contracts/tokenomics/maker/src/utils.rs b/contracts/tokenomics/maker/src/utils.rs index 364e52ede..74160f883 100644 --- a/contracts/tokenomics/maker/src/utils.rs +++ b/contracts/tokenomics/maker/src/utils.rs @@ -8,8 +8,8 @@ use astroport::pair::Cw20HookMsg; use astroport::querier::query_pair_info; use cosmwasm_std::{ - coins, to_binary, wasm_execute, Addr, Binary, CosmosMsg, Decimal, Deps, Env, QuerierWrapper, - StdError, StdResult, SubMsg, Uint128, WasmMsg, + coins, to_binary, wasm_execute, Addr, Binary, CosmosMsg, Decimal, Deps, Empty, Env, + QuerierWrapper, StdError, StdResult, SubMsg, Uint128, WasmMsg, }; use cw20::Cw20ExecuteMsg; @@ -219,7 +219,9 @@ pub fn build_send_msg( })), AssetInfo::NativeToken { denom } => Ok(CosmosMsg::Wasm(wasm_execute( recipient, - &astro_satellite_package::ExecuteMsg::TransferAstro {}, + // Satellite type parameter is only needed for CheckMessages endpoint which is not used in Maker contract. + // So it's safe to pass Empty as CustomMsg + &astro_satellite_package::ExecuteMsg::::TransferAstro {}, coins(asset.amount.u128(), denom), )?)), } diff --git a/packages/astroport/Cargo.toml b/packages/astroport/Cargo.toml index 88602da1b..ffc0d8538 100644 --- a/packages/astroport/Cargo.toml +++ b/packages/astroport/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "astroport" -version = "3.4.0" +version = "3.5.0" authors = ["Astroport"] edition = "2021" description = "Common Astroport types, queriers and other utils" diff --git a/packages/astroport/src/observation.rs b/packages/astroport/src/observation.rs index c723cd721..9d9ffdd6f 100644 --- a/packages/astroport/src/observation.rs +++ b/packages/astroport/src/observation.rs @@ -4,6 +4,7 @@ use cosmwasm_schema::cw_serde; use cosmwasm_std::{ CustomQuery, Decimal, Decimal256, Deps, Env, StdError, StdResult, Storage, Uint128, }; +use cw_storage_plus::Item; /// Circular buffer size which stores observations pub const OBSERVATIONS_SIZE: u32 = 3000; @@ -14,7 +15,7 @@ pub const MIN_TRADE_SIZE: Decimal256 = Decimal256::raw(1000000000000000); /// Stores trade size observations. We use it in orderbook integration /// and derive prices for external contracts/users. #[cw_serde] -#[derive(Copy)] +#[derive(Copy, Default)] pub struct Observation { pub timestamp: u64, /// Base asset simple moving average (mean) @@ -54,7 +55,18 @@ where oldest_ind = 0; newest_ind %= buffer.capacity(); } else { - return Err(StdError::generic_err("Buffer is empty")); + return match PrecommitObservation::may_load(deps.storage)? { + // First observation after pool initialization could be captured but not committed yet + Some(obs) if obs.precommit_ts <= target => Ok(OracleObservation { + timestamp: target, + price: Decimal::from_ratio(obs.base_amount, obs.quote_amount), + }), + Some(_) => Err(StdError::generic_err(format!( + "Requested observation is too old. Last known observation is at {}", + target + ))), + None => Err(StdError::generic_err("Buffer is empty")), + }; } } @@ -143,6 +155,47 @@ fn binary_search( } } +#[cw_serde] +pub struct PrecommitObservation { + pub base_amount: Uint128, + pub quote_amount: Uint128, + pub precommit_ts: u64, +} + +impl<'a> PrecommitObservation { + /// Temporal storage for observation which should be committed in the next block + const PRECOMMIT_OBSERVATION: Item<'a, PrecommitObservation> = + Item::new("precommit_observation"); + + pub fn save( + storage: &mut dyn Storage, + env: &Env, + base_amount: Uint128, + quote_amount: Uint128, + ) -> StdResult<()> { + let next_obs = match Self::may_load(storage)? { + // Accumulating observations at the same block + Some(mut prev_obs) if env.block.time.seconds() == prev_obs.precommit_ts => { + prev_obs.base_amount += base_amount; + prev_obs.quote_amount += quote_amount; + prev_obs + } + _ => PrecommitObservation { + base_amount, + quote_amount, + precommit_ts: env.block.time.seconds(), + }, + }; + + Self::PRECOMMIT_OBSERVATION.save(storage, &next_obs) + } + + #[inline] + pub fn may_load(storage: &dyn Storage) -> StdResult> { + Self::PRECOMMIT_OBSERVATION.may_load(storage) + } +} + #[cfg(test)] mod test { use crate::observation::Observation;