diff --git a/Cargo.lock b/Cargo.lock index 407406e565..0acd51cf33 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -731,6 +731,7 @@ dependencies = [ "frame-system", "impl-trait-for-tuples", "log", + "orml-oracle", "orml-traits", "pallet-assets", "pallet-contracts", @@ -5649,6 +5650,7 @@ dependencies = [ "frame-system", "hex", "libsecp256k1", + "orml-oracle", "pallet-assets", "pallet-balances", "pallet-collator-selection", @@ -5661,6 +5663,8 @@ dependencies = [ "pallet-evm-precompile-assets-erc20", "pallet-evm-precompile-dispatch", "pallet-inflation", + "pallet-membership", + "pallet-price-aggregator", "pallet-proxy", "pallet-unified-accounts", "pallet-utility", @@ -7755,6 +7759,25 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +[[package]] +name = "oracle-benchmarks" +version = "0.1.0" +dependencies = [ + "astar-primitives", + "frame-benchmarking", + "frame-support", + "frame-system", + "log", + "orml-oracle", + "orml-traits", + "parity-scale-codec", + "scale-info", + "serde", + "sp-arithmetic", + "sp-runtime", + "sp-std", +] + [[package]] name = "orchestra" version = "0.0.5" @@ -7796,6 +7819,24 @@ dependencies = [ "num-traits", ] +[[package]] +name = "orml-oracle" +version = "0.4.1-dev" +source = "git+https://github.com/open-web3-stack/open-runtime-module-library?branch=polkadot-v1.1.0#b3694e631df7f1ca16b1973122937753fcdee9d4" +dependencies = [ + "frame-support", + "frame-system", + "orml-traits", + "orml-utilities", + "parity-scale-codec", + "scale-info", + "serde", + "sp-application-crypto", + "sp-io", + "sp-runtime", + "sp-std", +] + [[package]] name = "orml-traits" version = "0.4.1-dev" @@ -9171,6 +9212,28 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-price-aggregator" +version = "0.1.0" +dependencies = [ + "astar-primitives", + "frame-benchmarking", + "frame-support", + "frame-system", + "log", + "num-traits", + "orml-traits", + "pallet-balances", + "parity-scale-codec", + "scale-info", + "serde", + "sp-arithmetic", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + [[package]] name = "pallet-proxy" version = "4.0.0-dev" @@ -13953,6 +14016,8 @@ dependencies = [ "moonbeam-rpc-primitives-debug", "moonbeam-rpc-primitives-txpool", "num_enum 0.5.11", + "oracle-benchmarks", + "orml-oracle", "orml-xcm-support", "orml-xtokens", "pallet-assets", @@ -13992,8 +14057,10 @@ dependencies = [ "pallet-identity", "pallet-inflation", "pallet-insecure-randomness-collective-flip", + "pallet-membership", "pallet-multisig", "pallet-preimage", + "pallet-price-aggregator", "pallet-proxy", "pallet-scheduler", "pallet-session", diff --git a/Cargo.toml b/Cargo.toml index 077e2a0c54..847abbfe2e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -175,6 +175,7 @@ pallet-scheduler = { git = "https://github.com/paritytech/polkadot-sdk", branch pallet-treasury = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.1.0", default-features = false } pallet-grandpa = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.1.0", default-features = false } pallet-message-queue = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.1.0", default-features = false } +pallet-membership = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.1.0", default-features = false } # EVM & Ethereum # (wasm) @@ -266,6 +267,7 @@ polkadot-service = { git = "https://github.com/paritytech/polkadot-sdk", branch orml-xtokens = { git = "https://github.com/open-web3-stack/open-runtime-module-library", branch = "polkadot-v1.1.0", default-features = false } orml-traits = { git = "https://github.com/open-web3-stack/open-runtime-module-library", branch = "polkadot-v1.1.0", default-features = false } orml-xcm-support = { git = "https://github.com/open-web3-stack/open-runtime-module-library", branch = "polkadot-v1.1.0", default-features = false } +orml-oracle = { git = "https://github.com/open-web3-stack/open-runtime-module-library", branch = "polkadot-v1.1.0", default-features = false } # Astar pallets & modules # (wasm) @@ -282,6 +284,10 @@ pallet-dynamic-evm-base-fee = { path = "./pallets/dynamic-evm-base-fee", default pallet-unified-accounts = { path = "./pallets/unified-accounts", default-features = false } astar-xcm-benchmarks = { path = "./pallets/astar-xcm-benchmarks", default-features = false } pallet-static-price-provider = { path = "./pallets/static-price-provider", default-features = false } +pallet-price-aggregator = { path = "./pallets/price-aggregator", default-features = false } + +# Get rid of this once orml-oracle has been updated with benchmarks +oracle-benchmarks = { path = "./pallets/oracle-benchmarks", default-features = false } dapp-staking-v3-runtime-api = { path = "./pallets/dapp-staking-v3/rpc/runtime-api", default-features = false } diff --git a/bin/collator/src/parachain/chain_spec/shibuya.rs b/bin/collator/src/parachain/chain_spec/shibuya.rs index 98ab8eec34..32cc39983b 100644 --- a/bin/collator/src/parachain/chain_spec/shibuya.rs +++ b/bin/collator/src/parachain/chain_spec/shibuya.rs @@ -23,12 +23,14 @@ use sc_service::ChainType; use shibuya_runtime::{ wasm_binary_unwrap, AccountId, AuraConfig, AuraId, Balance, BalancesConfig, CollatorSelectionConfig, CouncilConfig, DappStakingConfig, DemocracyConfig, EVMChainIdConfig, - EVMConfig, InflationConfig, InflationParameters, ParachainInfoConfig, Precompiles, - RuntimeGenesisConfig, SessionConfig, SessionKeys, Signature, SudoConfig, SystemConfig, - TechnicalCommitteeConfig, TierThreshold, TreasuryConfig, VestingConfig, SBY, + EVMConfig, InflationConfig, InflationParameters, OracleMembershipConfig, ParachainInfoConfig, + Precompiles, PriceAggregatorConfig, RuntimeGenesisConfig, SessionConfig, SessionKeys, + Signature, SudoConfig, SystemConfig, TechnicalCommitteeConfig, TierThreshold, TreasuryConfig, + VestingConfig, SBY, }; use sp_core::{sr25519, Pair, Public}; +use astar_primitives::oracle::CurrencyAmount; use sp_runtime::{ traits::{IdentifyAccount, Verify}, Permill, @@ -206,6 +208,20 @@ fn make_genesis( params: InflationParameters::default(), ..Default::default() }, + oracle_membership: OracleMembershipConfig { + members: vec![ + get_account_id_from_seed::("Alice"), + get_account_id_from_seed::("Bob"), + ] + .try_into() + .expect("Assumption is that at least two members will be allowed."), + ..Default::default() + }, + price_aggregator: PriceAggregatorConfig { + circular_buffer: vec![CurrencyAmount::from_rational(5, 10)] + .try_into() + .expect("Must work since buffer should have at least a single value."), + }, } } diff --git a/pallets/dapp-staking-v3/src/test/mock.rs b/pallets/dapp-staking-v3/src/test/mock.rs index 4f8746aadd..d002d80136 100644 --- a/pallets/dapp-staking-v3/src/test/mock.rs +++ b/pallets/dapp-staking-v3/src/test/mock.rs @@ -27,7 +27,7 @@ use frame_support::{ traits::{fungible::Mutate as FunMutate, ConstU128, ConstU32}, weights::Weight, }; -use sp_arithmetic::fixed_point::FixedU64; +use sp_arithmetic::fixed_point::FixedU128; use sp_core::H256; use sp_io::TestExternalities; use sp_runtime::{ @@ -106,8 +106,8 @@ impl pallet_balances::Config for Test { pub struct DummyPriceProvider; impl PriceProvider for DummyPriceProvider { - fn average_price() -> FixedU64 { - FixedU64::from_rational(1, 10) + fn average_price() -> FixedU128 { + FixedU128::from_rational(1, 10) } } diff --git a/pallets/dapp-staking-v3/src/test/tests_types.rs b/pallets/dapp-staking-v3/src/test/tests_types.rs index 5df6619d20..1f1f4b0dfa 100644 --- a/pallets/dapp-staking-v3/src/test/tests_types.rs +++ b/pallets/dapp-staking-v3/src/test/tests_types.rs @@ -18,7 +18,7 @@ use astar_primitives::{dapp_staking::StandardTierSlots, Balance}; use frame_support::assert_ok; -use sp_arithmetic::fixed_point::FixedU64; +use sp_arithmetic::fixed_point::FixedU128; use sp_runtime::Permill; use crate::*; @@ -2887,11 +2887,11 @@ fn tier_configuration_basic_tests() { assert!(init_config.is_valid(), "Init config must be valid!"); // Create a new config, based on a new price - let high_price = FixedU64::from_rational(20, 100); // in production will be expressed in USD + let high_price = FixedU128::from_rational(20, 100); // in production will be expressed in USD let new_config = init_config.calculate_new(high_price, ¶ms); assert!(new_config.is_valid()); - let low_price = FixedU64::from_rational(1, 100); // in production will be expressed in USD + let low_price = FixedU128::from_rational(1, 100); // in production will be expressed in USD let new_config = init_config.calculate_new(low_price, ¶ms); assert!(new_config.is_valid()); diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index e332a8cfa0..6fdb36f165 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -67,7 +67,7 @@ use frame_support::{pallet_prelude::*, BoundedBTreeMap, BoundedVec}; use parity_scale_codec::{Decode, Encode}; use serde::{Deserialize, Serialize}; -use sp_arithmetic::fixed_point::FixedU64; +use sp_arithmetic::fixed_point::FixedU128; use sp_runtime::{ traits::{CheckedAdd, UniqueSaturatedInto, Zero}, FixedPointNumber, Permill, Saturating, @@ -1629,7 +1629,7 @@ impl, T: TierSlotsFunc> TiersConfiguration { } /// Calculate new `TiersConfiguration`, based on the old settings, current native currency price and tier configuration. - pub fn calculate_new(&self, native_price: FixedU64, params: &TierParameters) -> Self { + pub fn calculate_new(&self, native_price: FixedU128, params: &TierParameters) -> Self { // It must always be at least 1 slot. let new_number_of_slots = T::number_of_slots(native_price).max(1); @@ -1671,7 +1671,7 @@ impl, T: TierSlotsFunc> TiersConfiguration { // new_threshold = old_threshold * (1 + %_threshold) // let new_tier_thresholds = if new_number_of_slots > self.number_of_slots { - let delta_threshold_decrease = FixedU64::from_rational( + let delta_threshold_decrease = FixedU128::from_rational( (new_number_of_slots - self.number_of_slots).into(), new_number_of_slots.into(), ); @@ -1693,7 +1693,7 @@ impl, T: TierSlotsFunc> TiersConfiguration { new_tier_thresholds } else if new_number_of_slots < self.number_of_slots { - let delta_threshold_increase = FixedU64::from_rational( + let delta_threshold_increase = FixedU128::from_rational( (self.number_of_slots - new_number_of_slots).into(), new_number_of_slots.into(), ); diff --git a/pallets/oracle-benchmarks/Cargo.toml b/pallets/oracle-benchmarks/Cargo.toml new file mode 100644 index 0000000000..534c34a2de --- /dev/null +++ b/pallets/oracle-benchmarks/Cargo.toml @@ -0,0 +1,52 @@ +[package] +name = "oracle-benchmarks" +version = "0.1.0" +license = "GPL-3.0-or-later" +description = "Temporary pallet to benchmark orml-oracle. Should be handled by orml-oracle itself in the future." +authors.workspace = true +edition.workspace = true +homepage.workspace = true +repository.workspace = true + +[dependencies] +log = { workspace = true } +parity-scale-codec = { workspace = true } +serde = { workspace = true } + +frame-support = { workspace = true } +frame-system = { workspace = true } +scale-info = { workspace = true } +sp-arithmetic = { workspace = true } +sp-runtime = { workspace = true } +sp-std = { workspace = true } + +orml-oracle = { workspace = true } +orml-traits = { workspace = true } + +astar-primitives = { workspace = true } + +frame-benchmarking = { workspace = true, optional = true } + +[features] +default = ["std"] +std = [ + "parity-scale-codec/std", + "log/std", + "scale-info/std", + "serde/std", + "sp-std/std", + "frame-support/std", + "frame-system/std", + "astar-primitives/std", + "sp-arithmetic/std", + "orml-traits/std", + "orml-oracle/std", +] +runtime-benchmarks = [ + "frame-benchmarking", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", + "astar-primitives/runtime-benchmarks", +] +try-runtime = ["frame-support/try-runtime"] diff --git a/pallets/oracle-benchmarks/src/benchmarking.rs b/pallets/oracle-benchmarks/src/benchmarking.rs new file mode 100644 index 0000000000..8e1d8aa25b --- /dev/null +++ b/pallets/oracle-benchmarks/src/benchmarking.rs @@ -0,0 +1,69 @@ +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +#![cfg(feature = "runtime-benchmarks")] + +use super::*; +use frame_benchmarking::v2::*; +use frame_support::{ + assert_ok, + traits::{Get, OnFinalize}, + BoundedVec, +}; +use frame_system::RawOrigin; +use sp_std::vec; + +#[benchmarks] +mod benchmarks { + use super::*; + + #[benchmark] + fn feed_values(x: Linear<1, { ::MaxFeedValues::get() }>) { + // Prepare account and add it as member + let account: T::AccountId = whitelisted_caller(); + T::AddMember::add_member(account.clone()); + + // Get base feed value + let currency_id_price_pair = T::BenchmarkCurrencyIdValuePair::get(); + + // Prepare feed values vector + let mut key_value_pairs = + BoundedVec::<_, ::MaxFeedValues>::default(); + for _ in 0..x { + key_value_pairs + .try_push(currency_id_price_pair.clone()) + .unwrap(); + } + + #[block] + { + assert_ok!(orml_oracle::Pallet::::feed_values( + RawOrigin::Signed(account.clone()).into(), + key_value_pairs + )); + } + } + + #[benchmark] + fn on_finalize() { + #[block] + { + orml_oracle::Pallet::::on_finalize(1_u32.into()); + } + } +} diff --git a/pallets/oracle-benchmarks/src/lib.rs b/pallets/oracle-benchmarks/src/lib.rs new file mode 100644 index 0000000000..a72f9bff29 --- /dev/null +++ b/pallets/oracle-benchmarks/src/lib.rs @@ -0,0 +1,54 @@ +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +//! A temporary solution to benchmark the `orml-oracle` pallet. +//! Should be removed once `orml-oracle` pallet has its own benchmarking. + +#![cfg_attr(not(feature = "std"), no_std)] + +use frame_support::traits::Get; +pub use pallet::*; +use sp_std::marker::PhantomData; + +pub mod weights; + +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; + +pub trait AddMember { + fn add_member(account: AccountId); +} + +#[frame_support::pallet] +pub mod pallet { + use super::*; + + #[pallet::pallet] + pub struct Pallet(PhantomData); + + #[pallet::config] + pub trait Config: frame_system::Config + orml_oracle::Config { + #[pallet::constant] + type BenchmarkCurrencyIdValuePair: Get<( + ::OracleKey, + ::OracleValue, + )>; + + type AddMember: AddMember; + } +} diff --git a/pallets/oracle-benchmarks/src/weights.rs b/pallets/oracle-benchmarks/src/weights.rs new file mode 100644 index 0000000000..86279efb0d --- /dev/null +++ b/pallets/oracle-benchmarks/src/weights.rs @@ -0,0 +1,86 @@ + +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +//! Autogenerated weights for oracle_benchmarks +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2024-03-18, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `gh-runner-01-ovh`, CPU: `Intel(R) Xeon(R) E-2236 CPU @ 3.40GHz` +//! EXECUTION: , WASM-EXECUTION: Compiled, CHAIN: Some("shibuya-dev"), DB CACHE: 1024 + +// Executed Command: +// ./target/release/astar-collator +// benchmark +// pallet +// --chain=shibuya-dev +// --steps=50 +// --repeat=20 +// --pallet=oracle_benchmarks +// --extrinsic=* +// --execution=wasm +// --wasm-execution=compiled +// --heap-pages=4096 +// --output=./benchmark-results/shibuya-dev/benchmarks_weights.rs +// --template=./scripts/templates/weight-template.hbs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use core::marker::PhantomData; +use orml_oracle::WeightInfo; + +/// Weights for oracle_benchmarks using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + /// Storage: `OracleMembership::Members` (r:1 w:0) + /// Proof: `OracleMembership::Members` (`max_values`: Some(1), `max_size`: Some(513), added: 1008, mode: `MaxEncodedLen`) + /// Storage: `Oracle::HasDispatched` (r:1 w:1) + /// Proof: `Oracle::HasDispatched` (`max_values`: Some(1), `max_size`: Some(257), added: 752, mode: `MaxEncodedLen`) + /// Storage: `Timestamp::Now` (r:1 w:0) + /// Proof: `Timestamp::Now` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Oracle::RawValues` (r:3 w:1) + /// Proof: `Oracle::RawValues` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`) + /// Storage: `Oracle::Values` (r:1 w:0) + /// Proof: `Oracle::Values` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `MaxEncodedLen`) + /// The range of component `x` is `[1, 2]`. + fn feed_values(x: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `279` + // Estimated: `8634` + // Minimum execution time: 28_512_000 picoseconds. + Weight::from_parts(20_589_375, 8634) + // Standard Error: 94_515 + .saturating_add(Weight::from_parts(8_962_312, 0).saturating_mul(x.into())) + .saturating_add(T::DbWeight::get().reads(7_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: `Oracle::HasDispatched` (r:0 w:1) + /// Proof: `Oracle::HasDispatched` (`max_values`: Some(1), `max_size`: Some(257), added: 752, mode: `MaxEncodedLen`) + fn on_finalize() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 812_000 picoseconds. + Weight::from_parts(885_000, 0) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } +} diff --git a/pallets/price-aggregator/Cargo.toml b/pallets/price-aggregator/Cargo.toml new file mode 100644 index 0000000000..c90324eac0 --- /dev/null +++ b/pallets/price-aggregator/Cargo.toml @@ -0,0 +1,57 @@ +[package] +name = "pallet-price-aggregator" +version = "0.1.0" +license = "GPL-3.0-or-later" +description = "Price aggregation & moving average calculation support." +authors.workspace = true +edition.workspace = true +homepage.workspace = true +repository.workspace = true + +[dependencies] +log = { workspace = true } +parity-scale-codec = { workspace = true } +serde = { workspace = true } + +astar-primitives = { workspace = true } +frame-support = { workspace = true } +frame-system = { workspace = true } +scale-info = { workspace = true } +sp-arithmetic = { workspace = true } +sp-runtime = { workspace = true } +sp-std = { workspace = true } + +orml-traits = { workspace = true } + +frame-benchmarking = { workspace = true, optional = true } + +[dev-dependencies] +num-traits = { workspace = true } +pallet-balances = { workspace = true } +sp-core = { workspace = true } +sp-io = { workspace = true } + +[features] +default = ["std"] +std = [ + "parity-scale-codec/std", + "log/std", + "sp-core/std", + "scale-info/std", + "serde/std", + "sp-std/std", + "frame-support/std", + "frame-system/std", + "pallet-balances/std", + "astar-primitives/std", + "sp-arithmetic/std", + "orml-traits/std", +] +runtime-benchmarks = [ + "frame-benchmarking", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", + "astar-primitives/runtime-benchmarks", +] +try-runtime = ["frame-support/try-runtime"] diff --git a/pallets/price-aggregator/src/benchmarking.rs b/pallets/price-aggregator/src/benchmarking.rs new file mode 100644 index 0000000000..9bfd020ca3 --- /dev/null +++ b/pallets/price-aggregator/src/benchmarking.rs @@ -0,0 +1,103 @@ +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +#![cfg(feature = "runtime-benchmarks")] + +use super::*; +use frame_benchmarking::v2::*; +use sp_std::vec; + +#[benchmarks] +mod benchmarks { + use super::*; + + #[benchmark] + fn process_block_aggregated_values() { + // Fill up the current block buffer with some values + let size_limit = ::MaxValuesPerBlock::get(); + let mut result = BoundedVec::::MaxValuesPerBlock>::default(); + for x in 1..=size_limit { + let value = CurrencyAmount::from_rational(x as u128 + 3, 10); + result + .try_push(value) + .expect("Must succeed since we are iterating to the limit"); + } + CurrentBlockValues::::put(result); + + #[block] + { + Pallet::::process_block_aggregated_values(); + } + + assert!( + CurrentBlockValues::::get().is_empty(), + "Should have been cleaned up." + ); + } + + #[benchmark] + fn process_intermediate_aggregated_values() { + // 1. Fill up the current aggregator and make it trigger on the current block end + IntermediateValueAggregator::::mutate(|a| { + a.limit_block = frame_system::Pallet::::block_number().saturated_into(); + + a.total = CurrencyAmount::from_rational(1234, 10); + a.count = 19; + }); + + // 2. Fill up the circular buffer with some values + let buffer_length = ::CircularBufferLength::get(); + ValuesCircularBuffer::::mutate(|b| { + for x in 1..=buffer_length { + b.add(CurrencyAmount::from_rational(x as u128 + 3, 10)); + } + }); + assert_eq!( + ValuesCircularBuffer::::get().buffer.len(), + buffer_length as usize, + "Sanity check." + ); + + // 3. Prepare local variables + let buffer_snapshot = ValuesCircularBuffer::::get(); + let current_block = frame_system::Pallet::::block_number(); + + #[block] + { + Pallet::::process_intermediate_aggregated_values(current_block); + } + + assert!(ValuesCircularBuffer::::get() != buffer_snapshot); + } + + impl_benchmark_test_suite!( + Pallet, + crate::benchmarking::tests::new_test_ext(), + crate::mock::Test, + ); +} + +#[cfg(test)] +mod tests { + use crate::mock; + use sp_io::TestExternalities; + + pub fn new_test_ext() -> TestExternalities { + mock::ExtBuilder::build() + } +} diff --git a/pallets/price-aggregator/src/lib.rs b/pallets/price-aggregator/src/lib.rs new file mode 100644 index 0000000000..014792aff5 --- /dev/null +++ b/pallets/price-aggregator/src/lib.rs @@ -0,0 +1,506 @@ +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +//! # Price Aggregator Pallet +//! +//! ## Overview +//! +//! Purpose of this pallet is to aggregate price data over some time, and then calculate the moving average. +//! +//! ## Solution +//! +//! The overall solution is broken down into several steps that occur over the course of various time periods. +//! +//! ### Block Aggregation +//! +//! During each block, the native currency price data is accumulated. This is done 'outside' the pallet, and it's only expected +//! that 'something' will push this data to the price aggregator pallet. The pallet itself doesn't care about the source of the data, nor who submitted it. +//! +//! At the end of each block, accumulated data is processed according to the specified algorithm (e.g. can be average, median, or something else). +//! In case processing was successful, the result is stored in the intermediate value aggregator. +//! In case processing fails, value is simply ignored. +//! +//! ### Intermediate Value Aggregation +//! +//! After a predetermined amount of time (blocks) has passed, the average value is calculated from the intermediate value aggregator. +//! In case it's a valid value (non-zero), it's pushed into the circular buffer used to calculate the moving average. +//! In case of an error, the value is simply ignored. +//! +//! ### Moving Average Calculation +//! +//! The moving average is calculated from the circular buffer, and is used to provide the 'average' price of the native currency, over some time period. +//! It's important to note that the moving average is not a 'real-time' value, but rather a 'lagging' indicator. + +#![cfg_attr(not(feature = "std"), no_std)] + +use frame_support::{pallet_prelude::*, traits::OnRuntimeUpgrade, DefaultNoBound}; +use frame_system::pallet_prelude::*; +pub use pallet::*; +use sp_arithmetic::{ + fixed_point::FixedU128, + traits::{CheckedAdd, SaturatedConversion, Saturating, Zero}, +}; +use sp_std::marker::PhantomData; + +use orml_traits::OnNewData; + +use astar_primitives::{ + oracle::{CurrencyAmount, CurrencyId, PriceProvider}, + BlockNumber, +}; + +pub mod weights; +pub use weights::WeightInfo; + +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; + +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; + +/// Trait for processing accumulated currency values within a single block. +/// +/// This can be anything from median, average, or more complex calculation. +pub trait ProcessBlockValues { + /// Process the accumulated values and return the result. + /// + /// In case of an error, return an error message. + fn process(values: &[CurrencyAmount]) -> Result; +} + +/// Used to calculate the simple average of the accumulated values. +pub struct AverageBlockValue; +impl ProcessBlockValues for AverageBlockValue { + fn process(values: &[CurrencyAmount]) -> Result { + if values.is_empty() { + return Err("No values exist for the current block."); + } + + let sum = values.iter().fold(CurrencyAmount::zero(), |acc, &value| { + acc.saturating_add(value) + }); + + Ok(sum.saturating_mul(FixedU128::from_rational(1, values.len() as u128))) + } +} + +/// Used to calculate the median of the accumulated values. +pub struct MedianBlockValue; +impl ProcessBlockValues for MedianBlockValue { + fn process(values: &[CurrencyAmount]) -> Result { + if values.is_empty() { + return Err("No values exist for the current block."); + } + + let mut sorted_values = values.to_vec(); + sorted_values.sort_unstable(); + + let mid = sorted_values.len() / 2; + + if sorted_values.len() % 2 == 0 { + Ok(sorted_values[mid.saturating_sub(1)] + .saturating_add(sorted_values[mid]) + .saturating_mul(CurrencyAmount::from_rational(1, 2))) + } else { + Ok(sorted_values[mid]) + } + } +} + +/// Used to aggregate the accumulated values over some time period. +/// +/// To avoid having a large memory footprint, values are summed up into a single accumulator. +/// Number of summed up values is tracked in a separate field. +#[derive(Encode, Decode, MaxEncodedLen, Default, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] +pub struct ValueAggregator { + /// Total accumulated value amount. + #[codec(compact)] + pub(crate) total: CurrencyAmount, + /// Number of values accumulated. + #[codec(compact)] + pub(crate) count: u32, + /// Block number at which aggregation should reset. + #[codec(compact)] + pub(crate) limit_block: BlockNumber, +} + +impl ValueAggregator { + /// New value aggregator, with the given block number as the new limit. + pub fn new(limit_block: BlockNumber) -> Self { + Self { + limit_block, + ..Default::default() + } + } + + /// Attempts to add a value to the aggregator, consuming `self` in the process. + /// + /// Returns an error if the addition would cause an overflow in the accumulator or the counter. + /// Otherwise returns the updated aggregator. + pub fn try_add(mut self, value: CurrencyAmount) -> Result { + self.total = self + .total + .checked_add(&value) + .ok_or("Failed to add value to the aggregator due to overflow.")?; + + self.count = self + .count + .checked_add(1) + .ok_or("Failed to increment count in the aggregator due to overflow.")?; + + Ok(self) + } + + /// Returns the average of the accumulated values. + pub fn average(&self) -> CurrencyAmount { + if self.count.is_zero() { + CurrencyAmount::zero() + } else { + self.total + .saturating_mul(FixedU128::from_rational(1, self.count.into())) + } + } +} + +/// Used to store the aggregated intermediate values into a circular buffer. +/// +/// Inserts values sequentially into the buffer, until the buffer has been filled out. +/// After that, the oldest value is always overwritten with the new value. +#[derive( + Encode, + Decode, + MaxEncodedLen, + RuntimeDebugNoBound, + PartialEqNoBound, + EqNoBound, + CloneNoBound, + TypeInfo, + DefaultNoBound, +)] +#[scale_info(skip_type_params(L))] +pub struct CircularBuffer> { + /// Currency values store. + pub(crate) buffer: BoundedVec, + /// Next index to write to. + #[codec(compact)] + pub(crate) head: u32, +} + +impl> CircularBuffer { + /// Adds a new value to the circular buffer, possibly overriding the oldest value if capacity is filled. + pub fn add(&mut self, value: CurrencyAmount) { + // This can never happen, parameters must ensure that. + // But we still check it and log an error if it does. + if self.head >= L::get() || self.head as usize > self.buffer.len() { + log::error!( + target: LOG_TARGET, + "Failed to push value to the circular buffer due to invalid next index. \ + Next index: {:?}, Buffer length: {:?}, Buffer capacity: {:?}", + self.head, + self.buffer.len(), + L::get() + ); + return; + } + + if self.buffer.len() > self.head as usize { + // Vec has been filled out, so we need to override the 'head' value + self.buffer[self.head as usize] = value; + } else { + // Vec is not full yet, so we can just push the value + let _ignorable = self.buffer.try_push(value); + } + self.head = self.head.saturating_add(1) % L::get(); + } + + /// Returns the average of the accumulated values. + pub fn average(&self) -> CurrencyAmount { + if self.buffer.is_empty() { + return CurrencyAmount::zero(); + } + + let sum = self + .buffer + .iter() + .fold(CurrencyAmount::zero(), |acc, &value| { + acc.saturating_add(value) + }); + + // At this point, length of the buffer is guaranteed to be greater than zero. + sum.saturating_mul(FixedU128::from_rational(1, self.buffer.len() as u128)) + } +} + +const LOG_TARGET: &str = "price-aggregator"; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + + /// The current storage version. + pub const STORAGE_VERSION: StorageVersion = StorageVersion::new(1); + + #[pallet::pallet] + #[pallet::storage_version(STORAGE_VERSION)] + pub struct Pallet(PhantomData); + + #[pallet::config] + pub trait Config: frame_system::Config { + /// The overarching event type. + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + /// Maximum number of distinct currency values we can store during a single block. + #[pallet::constant] + type MaxValuesPerBlock: Get; + + /// Used to process accumulated values in the current block. + type ProcessBlockValues: ProcessBlockValues; + + /// Native currency ID that this pallet is supposed to track. + type NativeCurrencyId: Get; + + /// Maximum length of the circular buffer used to calculate the moving average. + #[pallet::constant] + type CircularBufferLength: Get; + + /// Duration of aggregation period expressed in the number of blocks. + /// During this time, currency values are aggregated, and are then used to calculate the average value. + #[pallet::constant] + type AggregationDuration: Get>; + + type WeightInfo: WeightInfo; + } + + #[pallet::genesis_config] + #[derive(frame_support::DefaultNoBound)] + pub struct GenesisConfig { + pub circular_buffer: BoundedVec, + } + + #[pallet::genesis_build] + impl BuildGenesisConfig for GenesisConfig { + fn build(&self) { + ValuesCircularBuffer::::put(CircularBuffer:: { + buffer: self.circular_buffer.clone(), + head: self.circular_buffer.len() as u32 % T::CircularBufferLength::get(), + }); + + IntermediateValueAggregator::::mutate(|aggregator| { + aggregator.limit_block = T::AggregationDuration::get().saturated_into(); + }); + } + } + + #[pallet::event] + #[pallet::generate_deposit(pub(crate) fn deposit_event)] + pub enum Event { + /// New average native currency value has been calculated and pushed into the moving average buffer. + AverageAggregatedValue { value: CurrencyAmount }, + } + + /// Storage for the accumulated native currency price in the current block. + #[pallet::storage] + #[pallet::whitelist_storage] + pub type CurrentBlockValues = + StorageValue<_, BoundedVec, ValueQuery>; + + /// Used to store the aggregated processed block values during some time period. + #[pallet::storage] + #[pallet::whitelist_storage] + pub type IntermediateValueAggregator = StorageValue<_, ValueAggregator, ValueQuery>; + + /// Used to store aggregated intermediate values for some time period. + #[pallet::storage] + pub type ValuesCircularBuffer = + StorageValue<_, CircularBuffer, ValueQuery>; + + #[pallet::hooks] + impl Hooks> for Pallet { + fn on_initialize(now: BlockNumberFor) -> Weight { + // Need to account for the reads and writes of: + // - CurrentBlockValues + // - IntermediateValueAggregator + // + // Also need to account for the weight of processing block accumulated values. + let mut total_weight = T::DbWeight::get() + .reads_writes(2, 2) + .saturating_add(T::WeightInfo::process_block_aggregated_values()); + + if IntermediateValueAggregator::::get().limit_block <= now.saturated_into() { + total_weight + .saturating_accrue(T::WeightInfo::process_intermediate_aggregated_values()); + } + + total_weight + } + + fn on_finalize(now: BlockNumberFor) { + // 1. Process the accumulated native currency values in the current block. + Self::process_block_aggregated_values(); + + // 2. Check if we need to push the average aggregated value to the storage. + if IntermediateValueAggregator::::get().limit_block <= now.saturated_into() { + Self::process_intermediate_aggregated_values(now); + } + } + + fn integrity_test() { + assert!(T::MaxValuesPerBlock::get() > 0); + assert!(T::CircularBufferLength::get() > 0); + assert!(!T::AggregationDuration::get().is_zero()); + } + } + + impl Pallet { + /// Used to process the native currency values accumulated in the current block. + /// + /// Guarantees that the accumulated values are cleared after processing. + /// In case of an error during processing, intermediate aggregated value is not updated. + pub(crate) fn process_block_aggregated_values() { + // 1. Take the accumulated block values, clearing the existing storage. + let accumulated_values = CurrentBlockValues::::take(); + + // 2. Attempt to process accumulated block values. + let processed_value = match T::ProcessBlockValues::process( + accumulated_values.as_slice(), + ) { + Ok(value) => value, + Err(message) => { + log::trace!( + target: LOG_TARGET, + "Failed to process the accumulated native currency values in the current block. \ + Reason: {:?}", + message + ); + + // Nothing to do if we have no valid value to store. + return; + } + }; + + // 3. Attempt to store the processed value. + // This operation is practically infallible, but we check the results for the additional safety. + let intermediate_value = IntermediateValueAggregator::::get(); + match intermediate_value.try_add(processed_value) { + Ok(new_aggregator) => { + IntermediateValueAggregator::::put(new_aggregator); + } + Err(message) => { + log::error!( + target: LOG_TARGET, + "Failed to add the processed native currency value to the intermediate storage. \ + Reason: {:?}", + message + ); + } + } + } + + /// Used to process the intermediate aggregated values, and push them to the moving average storage. + pub(crate) fn process_intermediate_aggregated_values(now: BlockNumberFor) { + // 1. Get the average value from the intermediate aggregator. + let average_value = IntermediateValueAggregator::::get().average(); + + // 2. Reset the aggregator back to zero, and set the new limit block. + IntermediateValueAggregator::::put(ValueAggregator::new( + now.saturating_add(T::AggregationDuration::get()) + .saturated_into(), + )); + + // 3. In case aggregated value equals 0, it means something has gone wrong since it's extremely unlikely + // that price goes to absolute zero. The much more likely case is that there's a problem with the oracle data feed. + if average_value.is_zero() { + log::error!( + target: LOG_TARGET, + "The average aggregated price equals zero, which most likely means that oracle data feed is faulty. \ + Not pushing the 'zero' value to the moving average storage." + ); + return; + } + + // 4. Push the 'valid' average aggregated value to the circular buffer. + ValuesCircularBuffer::::mutate(|buffer| buffer.add(average_value)); + Self::deposit_event(Event::AverageAggregatedValue { + value: average_value, + }); + } + } + + // Make this pallet an 'observer' ('listener') of the new oracle data feed. + impl OnNewData for Pallet { + fn on_new_data(who: &T::AccountId, key: &CurrencyId, value: &CurrencyAmount) { + // Ignore any currency that is not native currency. + if T::NativeCurrencyId::get() != *key { + return; + } + + CurrentBlockValues::::mutate(|v| match v.try_push(*value) { + Ok(()) => {} + Err(_) => { + log::error!( + target: LOG_TARGET, + "Failed to push native currency value into the ongoing block due to exceeded capacity. \ + Value was submitted by: {:?}", + who + ); + } + }); + } + } + + // Make this pallet a `price provider` for the native currency. + // + // For this particular implementation, a simple moving average is used to calculate the average price. + impl PriceProvider for Pallet { + fn average_price() -> FixedU128 { + ValuesCircularBuffer::::get().average() + } + } +} + +/// Used to update static price due to storage schema change. +pub struct PriceAggregatorInitializer(PhantomData<(T, P)>); +impl> OnRuntimeUpgrade for PriceAggregatorInitializer { + fn on_runtime_upgrade() -> Weight { + if Pallet::::on_chain_storage_version() > 0 { + return Weight::zero(); + } + + // 1. Prepare price aggregator storage. + let now = frame_system::Pallet::::block_number(); + let limit_block = now.saturating_add(T::AggregationDuration::get().saturated_into()); + IntermediateValueAggregator::::put(ValueAggregator::new(limit_block.saturated_into())); + + // 2. Put the initial value into the circular buffer so it's not empty. + use sp_arithmetic::FixedPointNumber; + let init_price = P::get().max(FixedU128::from_rational(1, FixedU128::DIV.into())); + log::info!( + "Pushing initial price value into moving average buffer: {}", + init_price + ); + ValuesCircularBuffer::::mutate(|buffer| buffer.add(init_price)); + + // 3. Set the initial storage version. + STORAGE_VERSION.put::>(); + + // Reading block number is 'free' in the terms of weight. + T::DbWeight::get().writes(3) + } +} diff --git a/pallets/price-aggregator/src/mock.rs b/pallets/price-aggregator/src/mock.rs new file mode 100644 index 0000000000..dd0661b031 --- /dev/null +++ b/pallets/price-aggregator/src/mock.rs @@ -0,0 +1,135 @@ +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +use crate::{ + self as pallet_price_aggregator, AverageBlockValue, BlockNumberFor, IntermediateValueAggregator, +}; + +use frame_support::{ + construct_runtime, parameter_types, + traits::{ConstU128, ConstU32, Hooks}, + weights::Weight, +}; +use sp_core::H256; +use sp_io::TestExternalities; +use sp_runtime::{ + traits::{BlakeTwo256, IdentityLookup}, + BuildStorage, +}; + +use astar_primitives::{oracle::CurrencyId, Balance, BlockNumber}; +type AccountId = u64; + +type Block = frame_system::mocking::MockBlockU32; + +parameter_types! { + pub const BlockHashCount: BlockNumber = 250; + pub BlockWeights: frame_system::limits::BlockWeights = + frame_system::limits::BlockWeights::simple_max(Weight::from_parts(1024, 0)); +} + +impl frame_system::Config for Test { + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type RuntimeOrigin = RuntimeOrigin; + type Nonce = u64; + type RuntimeCall = RuntimeCall; + type Block = Block; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = BlockHashCount; + type DbWeight = (); + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; +} + +impl pallet_balances::Config for Test { + type MaxLocks = ConstU32<4>; + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; + type Balance = Balance; + type RuntimeEvent = RuntimeEvent; + type DustRemoval = (); + type ExistentialDeposit = ConstU128<1>; + type AccountStore = System; + type WeightInfo = (); + type RuntimeHoldReason = RuntimeHoldReason; + type FreezeIdentifier = (); + type MaxHolds = ConstU32<0>; + type MaxFreezes = ConstU32<0>; +} + +parameter_types! { + pub const NativeCurrencyId: CurrencyId = CurrencyId::ASTR; + pub const AggregationDuration: BlockNumberFor = 16; +} + +impl pallet_price_aggregator::Config for Test { + type RuntimeEvent = RuntimeEvent; + // Should at least be 3 for tests to work properly + type MaxValuesPerBlock = ConstU32<4>; + type ProcessBlockValues = AverageBlockValue; + type NativeCurrencyId = NativeCurrencyId; + type CircularBufferLength = ConstU32<7>; + type AggregationDuration = AggregationDuration; + type WeightInfo = (); +} + +construct_runtime!( + pub struct Test { + System: frame_system, + Balances: pallet_balances, + PriceAggregator: pallet_price_aggregator, + } +); + +pub struct ExtBuilder; +impl ExtBuilder { + pub fn build() -> TestExternalities { + let storage = frame_system::GenesisConfig::::default() + .build_storage() + .unwrap(); + + let mut ext = TestExternalities::from(storage); + ext.execute_with(|| { + // 1. Set the initial limit block for the intermediate value aggregator + IntermediateValueAggregator::::mutate(|v| { + v.limit_block = + ::AggregationDuration::get() + 1 + }); + + // 2. Init block setting + let init_block_number = 1; + System::set_block_number(init_block_number); + PriceAggregator::on_initialize(init_block_number); + }); + + ext + } +} diff --git a/pallets/price-aggregator/src/tests.rs b/pallets/price-aggregator/src/tests.rs new file mode 100644 index 0000000000..2184038964 --- /dev/null +++ b/pallets/price-aggregator/src/tests.rs @@ -0,0 +1,562 @@ +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +use crate::mock::*; +use crate::{ + pallet::Config, AverageBlockValue, CircularBuffer, CurrentBlockValues, Event, + IntermediateValueAggregator, MedianBlockValue, ProcessBlockValues, ValueAggregator, + ValuesCircularBuffer, +}; + +use astar_primitives::oracle::{CurrencyAmount, CurrencyId}; + +use orml_traits::OnNewData; + +use frame_support::{ + assert_storage_noop, + traits::{Get, Hooks}, + BoundedVec, +}; +use sp_runtime::{traits::Zero, Saturating}; + +pub use num_traits::Bounded; + +#[test] +fn average_block_value_works() { + // 0. Empty vec check + let empty_vec: Vec = vec![]; + assert!(AverageBlockValue::process(&empty_vec).is_err()); + + // 1. Single value check + let single_value_vec = vec![CurrencyAmount::from_rational(15, 10)]; + assert_eq!( + AverageBlockValue::process(&single_value_vec), + Ok(single_value_vec[0]) + ); + + // 2. Multiple values check + let multiple_values_vec = vec![ + CurrencyAmount::from_rational(5, 10), + CurrencyAmount::from_rational(15, 10), + ]; + assert_eq!( + AverageBlockValue::process(&multiple_values_vec), + Ok(CurrencyAmount::from_rational(10, 10)) + ); +} + +#[test] +fn median_block_value_works() { + // 0. Empty vec check + let empty_vec: Vec = vec![]; + assert!(MedianBlockValue::process(&empty_vec).is_err()); + + // 1. Single value check + let single_value_vec = vec![CurrencyAmount::from_rational(7, 10)]; + assert_eq!( + MedianBlockValue::process(&single_value_vec), + Ok(single_value_vec[0]) + ); + + // 2. Odd number values check + let odd_values_vec = vec![ + CurrencyAmount::from_rational(3, 10), + CurrencyAmount::from_rational(7, 10), + CurrencyAmount::from_rational(9, 10), + ]; + assert_eq!( + MedianBlockValue::process(&odd_values_vec), + Ok(CurrencyAmount::from_rational(7, 10)) + ); + + let odd_values_vec = vec![ + CurrencyAmount::from_rational(11, 10), + CurrencyAmount::from_rational(2, 10), + CurrencyAmount::from_rational(7, 10), + ]; + assert_eq!( + MedianBlockValue::process(&odd_values_vec), + Ok(CurrencyAmount::from_rational(7, 10)) + ); + + // 3.1. Even number values check + let even_values_vec_1 = vec![ + CurrencyAmount::from_rational(4, 10), + CurrencyAmount::from_rational(6, 10), + ]; + assert_eq!( + MedianBlockValue::process(&even_values_vec_1), + Ok(CurrencyAmount::from_rational(5, 10)) + ); + + let even_values_vec_1 = vec![ + CurrencyAmount::from_rational(6, 10), + CurrencyAmount::from_rational(4, 10), + ]; + assert_eq!( + MedianBlockValue::process(&even_values_vec_1), + Ok(CurrencyAmount::from_rational(5, 10)) + ); + + // 3.1. Even number values check + let even_values_vec_2 = vec![ + CurrencyAmount::from_rational(1, 10), + CurrencyAmount::from_rational(4, 10), + CurrencyAmount::from_rational(6, 10), + CurrencyAmount::from_rational(23, 10), + ]; + assert_eq!( + MedianBlockValue::process(&even_values_vec_2), + Ok(CurrencyAmount::from_rational(5, 10)) + ); + + let even_values_vec_2 = vec![ + CurrencyAmount::from_rational(23, 10), + CurrencyAmount::from_rational(4, 10), + CurrencyAmount::from_rational(3, 10), + CurrencyAmount::from_rational(6, 10), + ]; + assert_eq!( + MedianBlockValue::process(&even_values_vec_2), + Ok(CurrencyAmount::from_rational(5, 10)) + ); +} + +#[test] +fn value_aggregator_basic_checks() { + let limit_block = 10; + let value_aggregator = ValueAggregator::new(limit_block); + + // 0. Sanity checks + assert!(value_aggregator.total.is_zero()); + assert!(value_aggregator.count.is_zero()); + assert_eq!(value_aggregator.limit_block, limit_block); + assert!(value_aggregator.average().is_zero()); + + // 1. Add a value, verify state is as expected + let amount_1 = CurrencyAmount::from_rational(15, 10); + let result = value_aggregator.clone().try_add(amount_1); + assert_eq!( + result, + Ok(ValueAggregator { + total: amount_1, + count: 1, + limit_block, + }) + ); + assert_eq!(result.unwrap().average(), amount_1); + + // 2. Add another value, verify state is as expected + let value_aggregator = result.unwrap(); + let amount_2 = CurrencyAmount::from_rational(5, 10); + let result = value_aggregator.clone().try_add(amount_2); + assert_eq!( + result, + Ok(ValueAggregator { + total: amount_1 + amount_2, + count: 2, + limit_block, + }) + ); + assert_eq!( + result.unwrap().average(), + CurrencyAmount::from_rational(10, 10) + ); +} + +#[test] +fn value_aggregator_overflow_checks() { + // 1. Currency overflow check + let max_currency_aggregator = ValueAggregator { + total: CurrencyAmount::max_value(), + count: 10, + limit_block: 10, + }; + + let amount = CurrencyAmount::from_rational(1, 10); + let result = max_currency_aggregator.clone().try_add(amount); + assert!(result.is_err()); + + // 2. Counter overflow check + let max_count_aggregator = ValueAggregator { + total: CurrencyAmount::zero(), + count: u32::MAX, + limit_block: 10, + }; + let result = max_count_aggregator.clone().try_add(amount); + assert!(result.is_err()); +} + +#[test] +fn circular_buffer_basic_checks() { + // 0. Buffer size prep + const BUFFER_SIZE: u32 = 16; + struct BufferSize; + impl Get for BufferSize { + fn get() -> u32 { + BUFFER_SIZE + } + } + + // 1. Sanity checks + let mut circular_buffer = CircularBuffer::::default(); + assert!(circular_buffer.buffer.is_empty()); + assert!(circular_buffer.head.is_zero()); + + // 2. Add a value, verify state is as expected + let amount_1 = CurrencyAmount::from_rational(19, 10); + let mut expected_buffer = vec![amount_1]; + circular_buffer.add(amount_1); + assert_eq!(circular_buffer.buffer.clone().into_inner(), expected_buffer); + assert_eq!(circular_buffer.head, 1); + assert_eq!(circular_buffer.average(), amount_1); + + // 3. Add another value, verify state is as expected + let amount_2 = CurrencyAmount::from_rational(7, 10); + circular_buffer.add(amount_2); + expected_buffer.push(amount_2); + assert_eq!(circular_buffer.buffer.clone().into_inner(), expected_buffer); + assert_eq!(circular_buffer.head, 2); + assert_eq!( + circular_buffer.average(), + CurrencyAmount::from_rational(13, 10) + ); + + // 4. Fill up the buffer, verify state is as expected + let amount_3 = CurrencyAmount::from_rational(27, 10); + for _ in 2..BUFFER_SIZE { + circular_buffer.add(amount_3); + expected_buffer.push(amount_3); + } + assert_eq!(circular_buffer.buffer.clone().into_inner(), expected_buffer); + assert!(circular_buffer.head.is_zero()); + + // 5. Add another value, verify 0-th element is replaced + let amount_4 = CurrencyAmount::from_rational(9, 10); + circular_buffer.add(amount_4); + expected_buffer[0] = amount_4; + assert_eq!(circular_buffer.buffer.clone().into_inner(), expected_buffer); + assert_eq!(circular_buffer.head, 1); + + // 6. Repeat the cycle few more times, expect it works as expected + for x in 0..BUFFER_SIZE * 5 { + // Store head for the next check + let init_head = circular_buffer.head; + + // Generate a new amount + let amount = amount_3 * CurrencyAmount::from_rational(x as u128 + 1, 1); + + assert!(circular_buffer.buffer[init_head as usize] != amount); + circular_buffer.add(amount); + assert_eq!(circular_buffer.buffer[init_head as usize], amount); + assert_eq!(circular_buffer.head, (init_head + 1) % BUFFER_SIZE); + } +} + +#[test] +fn circular_buffer_inconsistency_safeguard_checks() { + // 0. Buffer size prep + const BUFFER_SIZE: u32 = 4; + struct BufferSize; + impl Get for BufferSize { + fn get() -> u32 { + BUFFER_SIZE + } + } + + // 1. Check that if head is ahead of length, operation does nothing + let amount = CurrencyAmount::from_rational(15, 100); + let mut inconsistent_buffer = CircularBuffer:: { + buffer: Default::default(), + head: 1, + }; + + inconsistent_buffer.add(amount); + assert!(inconsistent_buffer.buffer.is_empty()); + assert_eq!(inconsistent_buffer.head, 1); + + // 2. Check that when head equals length, operation does nothing + let init_buffer = BoundedVec::try_from(vec![amount, amount, amount, amount]) + .expect("Must work since size matches the bound."); + let mut inconsistent_buffer = CircularBuffer:: { + buffer: init_buffer.clone(), + head: BUFFER_SIZE, + }; + + inconsistent_buffer.add(amount); + assert_eq!(inconsistent_buffer.buffer, init_buffer); + assert_eq!(inconsistent_buffer.head, BUFFER_SIZE); +} + +#[test] +fn on_new_data_works_as_expected() { + ExtBuilder::build().execute_with(|| { + // 0. Initial sanity check + assert!( + CurrentBlockValues::::get().is_empty(), + "Init state must be empty." + ); + + // 1. Inform pallet of a new piece of data, verify state is as expected + let dummy_account_1 = 123; + let native_currency_id = ::NativeCurrencyId::get(); + let amount_1 = CurrencyAmount::from_rational(15, 10); + PriceAggregator::on_new_data(&dummy_account_1, &native_currency_id, &amount_1); + assert_eq!( + CurrentBlockValues::::get().into_inner(), + vec![amount_1], + ); + + // 2. Try to add non-native currency, verify no state change + let non_native_currency_id = CurrencyId::SDN; + assert!( + non_native_currency_id != native_currency_id, + "Sanity check." + ); + + let non_native_amount = CurrencyAmount::from_rational(7, 10); + assert_storage_noop!(PriceAggregator::on_new_data( + &dummy_account_1, + &non_native_currency_id, + &non_native_amount + )); + + // 3. Add additional amount, verify state is as expected + let amount_2 = CurrencyAmount::from_rational(3, 10); + PriceAggregator::on_new_data(&dummy_account_1, &native_currency_id, &amount_2); + assert_eq!( + CurrentBlockValues::::get().into_inner(), + vec![amount_1, amount_2], + ); + + // 4. Fill up storage to the limit, verify state is as expected + let limit = ::MaxValuesPerBlock::get(); + let mut result = vec![amount_1, amount_2]; + let amount_3 = CurrencyAmount::from_rational(19, 10); + + for _ in 2..limit { + PriceAggregator::on_new_data(&dummy_account_1, &native_currency_id, &amount_3); + result.push(amount_3); + } + + assert_eq!(result.len(), limit as usize, "Sanity check."); + assert_eq!(CurrentBlockValues::::get().into_inner(), result); + + // 5. Try to add one more value, overflowing the buffer, verify no state change + assert_storage_noop!(PriceAggregator::on_new_data( + &dummy_account_1, + &native_currency_id, + &amount_3 + )); + }); +} + +#[test] +fn on_finalize_updates_aggregated_data() { + ExtBuilder::build().execute_with(|| { + // 1. Store some data into the current block values buffer + let dummy_account_1 = 123; + let native_currency_id = ::NativeCurrencyId::get(); + let amount_1 = CurrencyAmount::from_rational(13, 10); + let amount_2 = CurrencyAmount::from_rational(17, 10); + PriceAggregator::on_new_data(&dummy_account_1, &native_currency_id, &amount_1); + PriceAggregator::on_new_data(&dummy_account_1, &native_currency_id, &amount_2); + + // 2. Finalize the block, verify state is as expected. + let block_number_1 = System::block_number(); + PriceAggregator::on_finalize(block_number_1); + + assert!( + CurrentBlockValues::::get().is_empty(), + "Buffer must be empty after the finalization." + ); + let intermediate_value_aggregator = IntermediateValueAggregator::::get(); + assert_eq!(intermediate_value_aggregator.count, 1); + + let average_amount_1 = CurrencyAmount::from_rational(15, 10); + assert_eq!(intermediate_value_aggregator.total, average_amount_1); + + // 3. Move to the next block, but for this one no new data is added + let intermediate_value_snapshot = IntermediateValueAggregator::::get(); + + let block_number_2 = block_number_1 + 1; + System::set_block_number(block_number_2); + PriceAggregator::on_initialize(block_number_2); + + // No new data is added, everything must still work without breaking + PriceAggregator::on_finalize(block_number_2); + assert_eq!( + IntermediateValueAggregator::::get(), + intermediate_value_snapshot, + "No new data was added, so the state must remain the same." + ); + + // 4. Add new data, verify state is updated as expected, i.e. nothing was broken by the previous step + let block_number_3 = block_number_2 + 1; + System::set_block_number(block_number_3); + PriceAggregator::on_initialize(block_number_3); + + let amount_3 = CurrencyAmount::from_rational(19, 10); + PriceAggregator::on_new_data(&dummy_account_1, &native_currency_id, &amount_3); + PriceAggregator::on_new_data(&dummy_account_1, &native_currency_id, &amount_3); + PriceAggregator::on_finalize(block_number_3); + + let intermediate_value_aggregator = IntermediateValueAggregator::::get(); + assert_eq!( + intermediate_value_aggregator.count, 2, + "Count must be 2 since we added only 2 new values." + ); + assert_eq!( + intermediate_value_aggregator.total, + average_amount_1 + amount_3, + "New entry must have been added, increasing the total." + ); + }) +} + +#[test] +fn on_finalize_updates_circular_buffer() { + ExtBuilder::build().execute_with(|| { + let dummy_account = 456; + let native_currency_id = ::NativeCurrencyId::get(); + + // 1. Advance just until limit block is reached, checking appropriate storage items along the way + let mut total = CurrencyAmount::zero(); + let current_block = System::block_number(); + let limit_block = IntermediateValueAggregator::::get().limit_block; + + for block in current_block..limit_block { + // Add new data + let amount = CurrencyAmount::from_rational(block.into(), 10); + PriceAggregator::on_new_data(&dummy_account, &native_currency_id, &amount); + total.saturating_accrue(amount); + + // Finalize the block + PriceAggregator::on_finalize(block); + assert_eq!( + IntermediateValueAggregator::::get().total, + total, + "Check total is updated as expected." + ); + assert!( + ValuesCircularBuffer::::get().buffer.is_empty(), + "Circular buffer is expected to remain empty until limit block is reached." + ); + + let new_block = block + 1; + System::set_block_number(new_block); + PriceAggregator::on_initialize(new_block); + } + + // 2. Move over to the next block, expect circular buffer to be updated since limit block will be reached. + let current_block = System::block_number(); + assert_eq!(current_block, limit_block, "Sanity check."); + + // Don't add any new data, just finalize the block. This is neat since we already know the exact 'total' amount + // but also get to test that circular buffer update doesn't break due to missing value. + PriceAggregator::on_finalize(current_block); + + // Check that value aggregator is reset & new block limit is correct + let reset_intermediate_aggregator = IntermediateValueAggregator::::get(); + assert_eq!(reset_intermediate_aggregator.total, CurrencyAmount::zero()); + assert_eq!(reset_intermediate_aggregator.count, 0); + assert_eq!( + reset_intermediate_aggregator.limit_block, + current_block + ::AggregationDuration::get() + ); + + // Check that circular buffer was updated as expected + let circular_buffer = ValuesCircularBuffer::::get(); + let expected_average = total * CurrencyAmount::from_rational(1, limit_block as u128 - 1); + assert_eq!( + circular_buffer.buffer.clone().into_inner(), + vec![expected_average] + ); + assert_eq!(circular_buffer.head, 1); + + // Verify deposited event + System::assert_last_event(RuntimeEvent::PriceAggregator( + Event::AverageAggregatedValue { + value: expected_average, + }, + )); + }) +} + +#[test] +fn circular_buffer_really_is_circular() { + ExtBuilder::build().execute_with(|| { + // 0. Init data + let aggregation_duration = ::AggregationDuration::get(); + let circular_buffer_length: u32 = ::CircularBufferLength::get(); + + fn advance_to_block(block: u32) { + let dummy_account = 456; + let native_currency_id = ::NativeCurrencyId::get(); + let init_block = System::block_number(); + + for block in init_block..block { + // Submit some amount to prevent error spam + let amount = CurrencyAmount::from_rational(block as u128, 10); + PriceAggregator::on_new_data(&dummy_account, &native_currency_id, &amount); + + PriceAggregator::on_finalize(block); + + let new_block = block + 1; + System::set_block_number(new_block); + PriceAggregator::on_initialize(new_block); + } + } + + // 1. Fill up the circular buffer + for x in 0..circular_buffer_length { + // Advance until circular buffer is updated + let intermediate_aggregator = IntermediateValueAggregator::::get(); + advance_to_block(intermediate_aggregator.limit_block + 1); + + // Check that circular buffer is updated as expected + let circular_buffer = ValuesCircularBuffer::::get(); + assert_eq!(circular_buffer.buffer.len(), x as usize + 1); + assert_eq!(circular_buffer.head, (x + 1) % circular_buffer_length); + + // Check that intermediate aggregator is reset & limit block is updated + let reset_intermediate_aggregator = IntermediateValueAggregator::::get(); + assert_eq!(reset_intermediate_aggregator.total, CurrencyAmount::zero()); + assert_eq!(reset_intermediate_aggregator.count, 0); + assert_eq!( + reset_intermediate_aggregator.limit_block, + intermediate_aggregator.limit_block + aggregation_duration + ); + } + + // 2. Continue adding the data, verify circular buffer is updated as expected + for x in 0..circular_buffer_length * 3 { + // Advance until circular buffer is updated + let intermediate_aggregator = IntermediateValueAggregator::::get(); + advance_to_block(intermediate_aggregator.limit_block + 1); + + // Check that circular buffer is updated as expected + let circular_buffer = ValuesCircularBuffer::::get(); + assert_eq!( + circular_buffer.buffer.len(), + circular_buffer_length as usize + ); + assert_eq!(circular_buffer.head, (x + 1) % circular_buffer_length); + } + }) +} diff --git a/pallets/price-aggregator/src/weights.rs b/pallets/price-aggregator/src/weights.rs new file mode 100644 index 0000000000..e7b1a0aa0a --- /dev/null +++ b/pallets/price-aggregator/src/weights.rs @@ -0,0 +1,99 @@ + +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +//! Autogenerated weights for pallet_price_aggregator +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2024-03-18, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `gh-runner-01-ovh`, CPU: `Intel(R) Xeon(R) E-2236 CPU @ 3.40GHz` +//! EXECUTION: , WASM-EXECUTION: Compiled, CHAIN: Some("shibuya-dev"), DB CACHE: 1024 + +// Executed Command: +// ./target/release/astar-collator +// benchmark +// pallet +// --chain=shibuya-dev +// --steps=50 +// --repeat=20 +// --pallet=pallet_price_aggregator +// --extrinsic=* +// --execution=wasm +// --wasm-execution=compiled +// --heap-pages=4096 +// --output=./benchmark-results/shibuya-dev/price_aggregator_weights.rs +// --template=./scripts/templates/weight-template.hbs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use core::marker::PhantomData; + +/// Weight functions needed for pallet_price_aggregator. +pub trait WeightInfo { + fn process_block_aggregated_values() -> Weight; + fn process_intermediate_aggregated_values() -> Weight; +} + +/// Weights for pallet_price_aggregator using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + fn process_block_aggregated_values() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 3_417_000 picoseconds. + Weight::from_parts(3_520_000, 0) + } + /// Storage: `PriceAggregator::ValuesCircularBuffer` (r:1 w:1) + /// Proof: `PriceAggregator::ValuesCircularBuffer` (`max_values`: Some(1), `max_size`: Some(117), added: 612, mode: `MaxEncodedLen`) + fn process_intermediate_aggregated_values() -> Weight { + // Proof Size summary in bytes: + // Measured: `102` + // Estimated: `1602` + // Minimum execution time: 9_093_000 picoseconds. + Weight::from_parts(9_297_000, 1602) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } +} + +// For backwards compatibility and tests +impl WeightInfo for () { + fn process_block_aggregated_values() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 3_417_000 picoseconds. + Weight::from_parts(3_520_000, 0) + } + /// Storage: `PriceAggregator::ValuesCircularBuffer` (r:1 w:1) + /// Proof: `PriceAggregator::ValuesCircularBuffer` (`max_values`: Some(1), `max_size`: Some(117), added: 612, mode: `MaxEncodedLen`) + fn process_intermediate_aggregated_values() -> Weight { + // Proof Size summary in bytes: + // Measured: `102` + // Estimated: `1602` + // Minimum execution time: 9_093_000 picoseconds. + Weight::from_parts(9_297_000, 1602) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } +} diff --git a/pallets/static-price-provider/src/lib.rs b/pallets/static-price-provider/src/lib.rs index d71599c815..ff139151b0 100644 --- a/pallets/static-price-provider/src/lib.rs +++ b/pallets/static-price-provider/src/lib.rs @@ -33,7 +33,7 @@ use frame_support::{pallet_prelude::*, traits::OnRuntimeUpgrade}; use frame_system::{ensure_root, pallet_prelude::*}; pub use pallet::*; -use sp_arithmetic::{fixed_point::FixedU64, traits::Zero, FixedPointNumber}; +use sp_arithmetic::{fixed_point::FixedU128, traits::Zero, FixedPointNumber}; use sp_std::marker::PhantomData; use astar_primitives::oracle::PriceProvider; @@ -49,7 +49,7 @@ pub mod pallet { use super::*; /// The current storage version. - pub const STORAGE_VERSION: StorageVersion = StorageVersion::new(1); + pub const STORAGE_VERSION: StorageVersion = StorageVersion::new(2); #[pallet::pallet] #[pallet::storage_version(STORAGE_VERSION)] @@ -65,7 +65,7 @@ pub mod pallet { #[pallet::generate_deposit(pub(crate) fn deposit_event)] pub enum Event { /// New static native currency price has been set. - PriceSet { price: FixedU64 }, + PriceSet { price: FixedU128 }, } #[pallet::error] @@ -77,16 +77,16 @@ pub mod pallet { /// Default value handler for active price. /// This pallet is temporary and it's not worth bothering with genesis config. pub struct DefaultActivePrice; - impl Get for DefaultActivePrice { - fn get() -> FixedU64 { - FixedU64::from_rational(1, 10) + impl Get for DefaultActivePrice { + fn get() -> FixedU128 { + FixedU128::from_rational(1, 10) } } /// Current active native currency price. #[pallet::storage] #[pallet::whitelist_storage] - pub type ActivePrice = StorageValue<_, FixedU64, ValueQuery, DefaultActivePrice>; + pub type ActivePrice = StorageValue<_, FixedU128, ValueQuery, DefaultActivePrice>; #[pallet::call] impl Pallet { @@ -95,7 +95,7 @@ pub mod pallet { /// This is a temporary solution before oracle is implemented & operational. #[pallet::call_index(0)] #[pallet::weight(T::DbWeight::get().writes(1))] - pub fn force_set_price(origin: OriginFor, price: FixedU64) -> DispatchResult { + pub fn force_set_price(origin: OriginFor, price: FixedU128) -> DispatchResult { ensure_root(origin)?; ensure!(!price.is_zero(), Error::::ZeroPrice); @@ -108,23 +108,26 @@ pub mod pallet { } impl PriceProvider for Pallet { - fn average_price() -> FixedU64 { + fn average_price() -> FixedU128 { ActivePrice::::get() } } } -/// `OnRuntimeUpgrade` logic for integrating this pallet into the live network. -pub struct InitActivePrice(PhantomData<(T, P)>); -impl> OnRuntimeUpgrade for InitActivePrice { +/// Used to update static price due to storage schema change. +pub struct ActivePriceUpdate(PhantomData<(T, P)>); +impl> OnRuntimeUpgrade for ActivePriceUpdate { fn on_runtime_upgrade() -> Weight { - let init_price = P::get().max(FixedU64::from_rational(1, FixedU64::DIV.into())); + if Pallet::::on_chain_storage_version() != 1 { + return T::DbWeight::get().reads(1); + } + let init_price = P::get().max(FixedU128::from_rational(1, FixedU128::DIV.into())); log::info!("Setting initial active price to {}", init_price); ActivePrice::::put(init_price); - STORAGE_VERSION.put::>(); + StorageVersion::new(2).put::>(); - T::DbWeight::get().writes(2) + T::DbWeight::get().reads_writes(1, 2) } } diff --git a/precompiles/dapp-staking-v3/src/test/mock.rs b/precompiles/dapp-staking-v3/src/test/mock.rs index b15dfc5147..93a456c5a1 100644 --- a/precompiles/dapp-staking-v3/src/test/mock.rs +++ b/precompiles/dapp-staking-v3/src/test/mock.rs @@ -31,7 +31,7 @@ use frame_system::RawOrigin; use pallet_evm::{ AddressMapping, EnsureAddressNever, EnsureAddressRoot, PrecompileResult, PrecompileSet, }; -use sp_arithmetic::{fixed_point::FixedU64, Permill}; +use sp_arithmetic::{fixed_point::FixedU128, Permill}; use sp_core::{H160, H256}; use sp_io::TestExternalities; use sp_runtime::{ @@ -186,8 +186,8 @@ type MockSmartContract = SmartContract<::AccountId pub struct DummyPriceProvider; impl PriceProvider for DummyPriceProvider { - fn average_price() -> FixedU64 { - FixedU64::from_rational(1, 10) + fn average_price() -> FixedU128 { + FixedU128::from_rational(1, 10) } } diff --git a/primitives/Cargo.toml b/primitives/Cargo.toml index 6ec6bf028a..c3379c04ca 100644 --- a/primitives/Cargo.toml +++ b/primitives/Cargo.toml @@ -34,6 +34,7 @@ xcm-builder = { workspace = true } xcm-executor = { workspace = true } # ORML dependencies +orml-oracle = { workspace = true } orml-traits = { workspace = true } pallet-contracts = { workspace = true } @@ -64,6 +65,7 @@ std = [ "xcm/std", "xcm-builder/std", "xcm-executor/std", + "orml-oracle/std", "orml-traits/std", "pallet-xc-asset-config/std", "fp-evm/std", diff --git a/primitives/src/dapp_staking.rs b/primitives/src/dapp_staking.rs index 282be84945..7a1ab00d4d 100644 --- a/primitives/src/dapp_staking.rs +++ b/primitives/src/dapp_staking.rs @@ -16,12 +16,11 @@ // You should have received a copy of the GNU General Public License // along with Astar. If not, see . -use super::{Balance, BlockNumber}; +use super::{oracle::CurrencyAmount, Balance, BlockNumber}; use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; use frame_support::pallet_prelude::{RuntimeDebug, Weight}; -use sp_arithmetic::fixed_point::FixedU64; use sp_core::H160; use sp_runtime::{traits::UniqueSaturatedInto, FixedPointNumber}; use sp_std::hash::Hash; @@ -188,13 +187,13 @@ impl AccountCheck for () { /// Trait for calculating the total number of tier slots for the given price. pub trait TierSlots { /// Returns the total number of tier slots for the given price. - fn number_of_slots(price: FixedU64) -> u16; + fn number_of_slots(price: CurrencyAmount) -> u16; } /// Standard tier slots implementation, as proposed in the Tokenomics 2.0 document. pub struct StandardTierSlots; impl TierSlots for StandardTierSlots { - fn number_of_slots(price: FixedU64) -> u16 { + fn number_of_slots(price: CurrencyAmount) -> u16 { let result: u64 = price.saturating_mul_int(1000_u64).saturating_add(50); result.unique_saturated_into() } diff --git a/primitives/src/oracle.rs b/primitives/src/oracle.rs index aa12e690e7..cfb1fd843f 100644 --- a/primitives/src/oracle.rs +++ b/primitives/src/oracle.rs @@ -16,7 +16,9 @@ // You should have received a copy of the GNU General Public License // along with Astar. If not, see . -use sp_arithmetic::fixed_point::FixedU64; +use frame_support::{pallet_prelude::*, traits::Time}; +use sp_arithmetic::fixed_point::FixedU128; +use sp_std::vec::Vec; /// Interface for fetching price of the native token. /// @@ -24,5 +26,32 @@ use sp_arithmetic::fixed_point::FixedU64; /// the price over a certain period of time. pub trait PriceProvider { /// Get the price of the native token. - fn average_price() -> FixedU64; + fn average_price() -> CurrencyAmount; +} + +pub type CurrencyAmount = FixedU128; + +#[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] +pub enum CurrencyId { + ASTR, + SDN, +} + +type TimestampedValue = orml_oracle::TimestampedValue< + CurrencyAmount, + <>::Time as Time>::Moment, +>; + +/// A dummy implementation of `CombineData` trait that does nothing. +pub struct DummyCombineData(PhantomData<(T, I)>); +impl, I> orml_traits::CombineData> + for DummyCombineData +{ + fn combine_data( + _key: &CurrencyId, + _values: Vec>, + _prev_value: Option>, + ) -> Option> { + None + } } diff --git a/runtime/astar/src/lib.rs b/runtime/astar/src/lib.rs index 704d88d4c1..5de67a7278 100644 --- a/runtime/astar/src/lib.rs +++ b/runtime/astar/src/lib.rs @@ -1121,21 +1121,16 @@ pub type Executive = frame_executive::Executive< Migrations, >; +use astar_primitives::oracle::CurrencyAmount; parameter_types! { - pub const DappStakingMigrationName: &'static str = "DappStakingMigration"; + // Keep it exactly the same as before + pub const InitPrice: CurrencyAmount = CurrencyAmount::from_rational(18, 100); } + /// All migrations that will run on the next runtime upgrade. /// /// Once done, migrations should be removed from the tuple. -pub type Migrations = ( - // Part of astar-83, need to first cleanup old storage before re-using the pallet - frame_support::migrations::RemovePallet< - DappStakingMigrationName, - ::DbWeight, - >, - // Part of astar-83 - (pallet_dapp_staking_migration::SingularStakingInfoTranslationUpgrade,), -); +pub type Migrations = (pallet_static_price_provider::ActivePriceUpdate,); type EventRecord = frame_system::EventRecord< ::RuntimeEvent, diff --git a/runtime/shibuya/Cargo.toml b/runtime/shibuya/Cargo.toml index 892bc57945..45d3b18f59 100644 --- a/runtime/shibuya/Cargo.toml +++ b/runtime/shibuya/Cargo.toml @@ -59,6 +59,7 @@ pallet-evm-precompile-sha3fips = { workspace = true } pallet-evm-precompile-simple = { workspace = true } pallet-identity = { workspace = true } pallet-insecure-randomness-collective-flip = { workspace = true } +pallet-membership = { workspace = true } pallet-multisig = { workspace = true } pallet-preimage = { workspace = true } pallet-proxy = { workspace = true } @@ -92,6 +93,7 @@ xcm-builder = { workspace = true } xcm-executor = { workspace = true } # orml dependencies +orml-oracle = { workspace = true } orml-xcm-support = { workspace = true } orml-xtokens = { workspace = true } @@ -114,6 +116,7 @@ pallet-evm-precompile-unified-accounts = { workspace = true } pallet-evm-precompile-xcm = { workspace = true } pallet-evm-precompile-xvm = { workspace = true } pallet-inflation = { workspace = true } +pallet-price-aggregator = { workspace = true } pallet-static-price-provider = { workspace = true } pallet-unified-accounts = { workspace = true } pallet-xc-asset-config = { workspace = true } @@ -121,6 +124,9 @@ pallet-xcm = { workspace = true } pallet-xcm-benchmarks = { workspace = true, optional = true } pallet-xvm = { workspace = true } +# Get rid of this after running the benchmarks +oracle-benchmarks = { workspace = true } + dapp-staking-v3-runtime-api = { workspace = true } precompile-utils = { workspace = true } @@ -158,6 +164,8 @@ std = [ "sp-io/std", "sp-runtime/std", "sp-runtime-interface/std", + "oracle-benchmarks/std", + "pallet-membership/std", "sp-version/std", "sp-block-builder/std", "sp-transaction-pool/std", @@ -196,9 +204,11 @@ std = [ "pallet-evm-precompile-unified-accounts/std", "pallet-evm-precompile-dispatch-lockdrop/std", "pallet-dapp-staking-v3/std", + "orml-oracle/std", "dapp-staking-v3-runtime-api/std", "pallet-inflation/std", "pallet-static-price-provider/std", + "pallet-price-aggregator/std", "pallet-identity/std", "pallet-multisig/std", "pallet-insecure-randomness-collective-flip/std", @@ -250,6 +260,7 @@ std = [ ] runtime-benchmarks = [ "astar-xcm-benchmarks/runtime-benchmarks", + "oracle-benchmarks/runtime-benchmarks", "pallet-dapp-staking-migration/runtime-benchmarks", "pallet-xcm-benchmarks/runtime-benchmarks", "frame-benchmarking", @@ -260,11 +271,13 @@ runtime-benchmarks = [ "pallet-dapp-staking-v3/runtime-benchmarks", "pallet-inflation/runtime-benchmarks", "pallet-balances/runtime-benchmarks", + "pallet-membership/runtime-benchmarks", "pallet-timestamp/runtime-benchmarks", "pallet-collective/runtime-benchmarks", "pallet-ethereum/runtime-benchmarks", "pallet-xcm/runtime-benchmarks", "pallet-xc-asset-config/runtime-benchmarks", + "pallet-price-aggregator/runtime-benchmarks", "xcm-builder/runtime-benchmarks", "pallet-preimage/runtime-benchmarks", "pallet-collator-selection/runtime-benchmarks", @@ -291,9 +304,12 @@ try-runtime = [ "pallet-sudo/try-runtime", "pallet-timestamp/try-runtime", "pallet-transaction-payment/try-runtime", + "orml-oracle/try-runtime", "pallet-utility/try-runtime", "pallet-vesting/try-runtime", + "pallet-price-aggregator/try-runtime", "pallet-ethereum/try-runtime", + "pallet-membership/try-runtime", "pallet-xc-asset-config/try-runtime", "pallet-assets/try-runtime", "pallet-authorship/try-runtime", @@ -307,6 +323,7 @@ try-runtime = [ "pallet-scheduler/try-runtime", "pallet-proxy/try-runtime", "pallet-contracts/try-runtime", + "oracle-benchmarks/try-runtime", "pallet-democracy/try-runtime", "pallet-collective/try-runtime", "pallet-treasury/try-runtime", diff --git a/runtime/shibuya/src/lib.rs b/runtime/shibuya/src/lib.rs index 12330074f6..70ab534b0c 100644 --- a/runtime/shibuya/src/lib.rs +++ b/runtime/shibuya/src/lib.rs @@ -72,6 +72,7 @@ use astar_primitives::{ PeriodNumber, SmartContract, StandardTierSlots, TierId, }, evm::{EvmRevertCodeHandler, HashedDefaultMappings}, + oracle::{CurrencyAmount, CurrencyId, DummyCombineData}, xcm::AssetLocationIdConverter, Address, AssetId, BlockNumber, Hash, Header, Nonce, }; @@ -422,7 +423,7 @@ impl pallet_dapp_staking_v3::Config for Runtime { type Currency = Balances; type SmartContract = SmartContract; type ManagerOrigin = frame_system::EnsureRoot; - type NativePriceProvider = StaticPriceProvider; + type NativePriceProvider = PriceAggregator; type StakingRewardHandler = Inflation; type CycleConfiguration = InflationCycleConfig; type Observers = Inflation; @@ -1270,6 +1271,90 @@ impl pallet_unified_accounts::Config for Runtime { type WeightInfo = pallet_unified_accounts::weights::SubstrateWeight; } +parameter_types! { + // Of course it's not true for Shibuya, but SBY is worthless, a test token. + pub const NativeCurrencyId: CurrencyId = CurrencyId::ASTR; + // Aggregate values for one day. + pub const AggregationDuration: BlockNumber = 7200; +} + +impl pallet_price_aggregator::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type MaxValuesPerBlock = ConstU32<8>; + type ProcessBlockValues = pallet_price_aggregator::MedianBlockValue; + type NativeCurrencyId = NativeCurrencyId; + // 7 days + type CircularBufferLength = ConstU32<7>; + type AggregationDuration = AggregationDuration; + type WeightInfo = pallet_price_aggregator::weights::SubstrateWeight; +} + +parameter_types! { + // Cannot specify `Root` so need to do it like this, unfortunately. + pub RootOperatorAccountId: AccountId = AccountId::from([0xffu8; 32]); +} + +impl orml_oracle::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type OnNewData = PriceAggregator; + type CombineData = DummyCombineData; + type Time = Timestamp; + type OracleKey = CurrencyId; + type OracleValue = CurrencyAmount; + type RootOperatorAccountId = RootOperatorAccountId; + type Members = OracleMembership; + type MaxHasDispatchedSize = ConstU32<8>; + type WeightInfo = oracle_benchmarks::weights::SubstrateWeight; + #[cfg(feature = "runtime-benchmarks")] + type MaxFeedValues = ConstU32<2>; + #[cfg(not(feature = "runtime-benchmarks"))] + type MaxFeedValues = ConstU32<1>; +} + +pub type OracleMembershipInstance = pallet_membership::Instance1; +impl pallet_membership::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type AddOrigin = EnsureRoot; + type RemoveOrigin = EnsureRoot; + type SwapOrigin = EnsureRoot; + type ResetOrigin = EnsureRoot; + type PrimeOrigin = EnsureRoot; + + type MembershipInitialized = (); + type MembershipChanged = (); + type MaxMembers = ConstU32<16>; + type WeightInfo = pallet_membership::weights::SubstrateWeight; +} + +// The oracle-benchmarks pallet should be removed once we uplift to high enough version +// (assumption is `polkadot-v1.10.0`) to have access to normal oracle pallet benchmarks). +// +// The pallet is stateless so in order to remove it, only code needs to be cleaned up. +pub struct DummyKeyPairValue; +impl Get<(CurrencyId, CurrencyAmount)> for DummyKeyPairValue { + fn get() -> (CurrencyId, CurrencyAmount) { + (CurrencyId::ASTR, CurrencyAmount::from_rational(15, 100)) + } +} +pub struct AddMemberBenchmark; +impl oracle_benchmarks::AddMember for AddMemberBenchmark { + fn add_member(account: AccountId) { + use frame_support::assert_ok; + use frame_system::RawOrigin; + assert_ok!( + pallet_membership::Pallet::::add_member( + RawOrigin::Root.into(), + account.into() + ) + ); + } +} + +impl oracle_benchmarks::Config for Runtime { + type BenchmarkCurrencyIdValuePair = DummyKeyPairValue; + type AddMember = AddMemberBenchmark; +} + impl pallet_dapp_staking_migration::Config for Runtime { type RuntimeEvent = RuntimeEvent; type WeightInfo = pallet_dapp_staking_migration::weights::SubstrateWeight; @@ -1296,6 +1381,9 @@ construct_runtime!( DappStaking: pallet_dapp_staking_v3 = 34, Inflation: pallet_inflation = 35, Assets: pallet_assets = 36, + PriceAggregator: pallet_price_aggregator = 37, + Oracle: orml_oracle = 38, + OracleMembership: pallet_membership:: = 39, Authorship: pallet_authorship = 40, CollatorSelection: pallet_collator_selection = 41, @@ -1329,6 +1417,8 @@ construct_runtime!( Sudo: pallet_sudo = 99, + // Remove after benchmarks are available in orml_oracle + OracleBenchmarks: oracle_benchmarks = 251, // Remove after migrating to v6 storage DappStakingMigration: pallet_dapp_staking_migration = 252, // To be removed & cleaned up once proper oracle is implemented @@ -1373,8 +1463,33 @@ pub type Executive = frame_executive::Executive< /// All migrations that will run on the next runtime upgrade. /// /// Once done, migrations should be removed from the tuple. -pub type Migrations = - (pallet_dapp_staking_migration::SingularStakingInfoTranslationUpgrade,); +pub type Migrations = ( + OracleIntegrationLogic, + pallet_price_aggregator::PriceAggregatorInitializer, +); + +pub struct InitPrice; +impl Get for InitPrice { + fn get() -> CurrencyAmount { + // 0.15 $ + CurrencyAmount::from_rational(15, 100) + } +} + +use frame_support::traits::OnRuntimeUpgrade; +pub struct OracleIntegrationLogic; +impl OnRuntimeUpgrade for OracleIntegrationLogic { + fn on_runtime_upgrade() -> Weight { + // 1. Set initial storage versions for the membership pallet + use frame_support::traits::StorageVersion; + StorageVersion::new(4) + .put::>(); + + // No storage version for the `orml_oracle` pallet, it's essentially 0 + + ::DbWeight::get().writes(1) + } +} type EventRecord = frame_system::EventRecord< ::RuntimeEvent, @@ -1462,6 +1577,9 @@ mod benches { [pallet_unified_accounts, UnifiedAccounts] [xcm_benchmarks_generic, XcmGeneric] [xcm_benchmarks_fungible, XcmFungible] + [pallet_price_aggregator, PriceAggregator] + [pallet_membership, OracleMembership] + [oracle_benchmarks, OracleBenchmarks] [pallet_dapp_staking_migration, DappStakingMigration] ); } diff --git a/runtime/shiden/src/lib.rs b/runtime/shiden/src/lib.rs index bb9053401f..3a016d0477 100644 --- a/runtime/shiden/src/lib.rs +++ b/runtime/shiden/src/lib.rs @@ -52,7 +52,6 @@ use pallet_transaction_payment::{ use parity_scale_codec::{Compact, Decode, Encode, MaxEncodedLen}; use polkadot_runtime_common::BlockHashCount; use sp_api::impl_runtime_apis; -use sp_arithmetic::fixed_point::FixedU64; use sp_core::{ConstBool, OpaqueMetadata, H160, H256, U256}; use sp_inherents::{CheckInherentsResult, InherentData}; use sp_runtime::{ @@ -72,6 +71,7 @@ use astar_primitives::{ PeriodNumber, SmartContract, TierId, TierSlots as TierSlotsFunc, }, evm::EvmRevertCodeHandler, + oracle::CurrencyAmount, xcm::AssetLocationIdConverter, Address, AssetId, BlockNumber, Hash, Header, Nonce, }; @@ -355,7 +355,7 @@ impl DappStakingAccountCheck for AccountCheck { pub struct ShidenTierSlots; impl TierSlotsFunc for ShidenTierSlots { - fn number_of_slots(price: FixedU64) -> u16 { + fn number_of_slots(price: CurrencyAmount) -> u16 { // According to the forum proposal, the original formula's factor is reduced from 1000x to 100x. let result: u64 = price.saturating_mul_int(100_u64).saturating_add(50); result.unique_saturated_into() @@ -1118,13 +1118,15 @@ pub type Executive = frame_executive::Executive< Migrations, >; +parameter_types! { + // Keep it exactly the same as before + pub const InitPrice: CurrencyAmount = CurrencyAmount::from_rational(32, 100); +} + /// All migrations that will run on the next runtime upgrade. /// /// Once done, migrations should be removed from the tuple. -pub type Migrations = ( - // Part of shiden-122 - pallet_dapp_staking_migration::SingularStakingInfoTranslationUpgrade, -); +pub type Migrations = (pallet_static_price_provider::ActivePriceUpdate,); use frame_support::traits::OnRuntimeUpgrade; pub struct SetNewTierConfig; diff --git a/tests/integration/Cargo.toml b/tests/integration/Cargo.toml index 595ee1afb2..7255a5ef15 100644 --- a/tests/integration/Cargo.toml +++ b/tests/integration/Cargo.toml @@ -21,10 +21,12 @@ pallet-evm = { workspace = true } # frame dependencies frame-support = { workspace = true } frame-system = { workspace = true } +orml-oracle = { workspace = true } pallet-assets = { workspace = true } pallet-balances = { workspace = true } pallet-contracts = { workspace = true } pallet-contracts-primitives = { workspace = true } +pallet-membership = { workspace = true } pallet-proxy = { workspace = true } pallet-utility = { workspace = true } sp-core = { workspace = true } @@ -40,6 +42,7 @@ pallet-ethereum-checked = { workspace = true } pallet-evm-precompile-assets-erc20 = { workspace = true } pallet-evm-precompile-dispatch = { workspace = true } pallet-inflation = { workspace = true } +pallet-price-aggregator = { workspace = true } pallet-unified-accounts = { workspace = true } precompile-utils = { workspace = true } unified-accounts-chain-extension-types = { workspace = true } diff --git a/tests/integration/src/lib.rs b/tests/integration/src/lib.rs index 8693ee2c63..1372aae234 100644 --- a/tests/integration/src/lib.rs +++ b/tests/integration/src/lib.rs @@ -43,3 +43,6 @@ mod dapp_staking_v3; #[cfg(any(feature = "shibuya"))] mod assets_chain_extensions; + +#[cfg(any(feature = "shibuya"))] +mod oracle; diff --git a/tests/integration/src/oracle.rs b/tests/integration/src/oracle.rs new file mode 100644 index 0000000000..f3378b0e13 --- /dev/null +++ b/tests/integration/src/oracle.rs @@ -0,0 +1,91 @@ +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +use crate::setup::*; + +use astar_primitives::oracle::{CurrencyAmount, PriceProvider}; +use pallet_price_aggregator::{IntermediateValueAggregator, ValueAggregator}; + +#[test] +fn price_submission_works() { + new_test_ext().execute_with(|| { + let native_currency_id = + ::NativeCurrencyId::get(); + assert_eq!(PriceAggregator::average_price(), INIT_PRICE, "Sanity check"); + + // 0. Need to set limit block to something sensible, otherwise we'll waste time on many redundant iterations + let limit_block = 10; + IntermediateValueAggregator::::put(ValueAggregator::new(limit_block)); + + // 1. Submit a price for a valid asset - the native currency + let price_1 = CurrencyAmount::from_rational(15, 100); + assert_ok!(Oracle::feed_values( + RuntimeOrigin::signed(ALICE.clone()), + vec![(native_currency_id, price_1)].try_into().unwrap() + )); + + let price_2 = CurrencyAmount::from_rational(17, 100); + assert_ok!(Oracle::feed_values( + RuntimeOrigin::signed(BOB.clone()), + vec![(native_currency_id, price_2)].try_into().unwrap() + )); + + // 2. Advance a block, and check price aggregator intermediate state is as expected + // (perhaps a bit detailed, but still good to check whether it's integrated) + run_for_blocks(1); + let expected_average = (price_1 + price_2) * CurrencyAmount::from_rational(1, 2); + assert_eq!( + IntermediateValueAggregator::::get().average(), + expected_average + ); + + // 3. Keep advancing blocks, adding new values only each other block, and verify the average is as expected at the end + for i in System::block_number() + 1..limit_block { + if i % 2 == 0 { + let step = CurrencyAmount::from_rational(i as u128 % 5, 100); + + assert_ok!(Oracle::feed_values( + RuntimeOrigin::signed(ALICE.clone()), + vec![(native_currency_id, price_1 + step)] + .try_into() + .unwrap() + )); + assert_ok!(Oracle::feed_values( + RuntimeOrigin::signed(BOB.clone()), + vec![(native_currency_id, price_2 - step)] + .try_into() + .unwrap() + )); + } + run_for_blocks(1); + } + + // 4. Execute limit block and verify state is updated as expected + run_for_blocks(2); // Need to run on_finalize of the limit block + let expected_moving_average = + (expected_average + INIT_PRICE) * CurrencyAmount::from_rational(1, 2); + assert_eq!(PriceAggregator::average_price(), expected_moving_average); + + // 5. Run until next limit block without any transactions, don't expect any changes + let limit_block = limit_block * 2; + IntermediateValueAggregator::::put(ValueAggregator::new(limit_block)); + + run_to_block(limit_block + 1); + assert_eq!(PriceAggregator::average_price(), expected_moving_average); + }) +} diff --git a/tests/integration/src/setup.rs b/tests/integration/src/setup.rs index 5ec33ada14..de9b12db5e 100644 --- a/tests/integration/src/setup.rs +++ b/tests/integration/src/setup.rs @@ -29,7 +29,8 @@ pub use sp_io::hashing::keccak_256; pub use sp_runtime::{AccountId32, MultiAddress}; pub use astar_primitives::{ - dapp_staking::CycleConfiguration, evm::UnifiedAddressMapper, BlockNumber, + dapp_staking::CycleConfiguration, evm::UnifiedAddressMapper, oracle::CurrencyAmount, + BlockNumber, }; #[cfg(feature = "shibuya")] @@ -159,6 +160,8 @@ pub const CAT: AccountId32 = AccountId32::new([3_u8; 32]); pub const INITIAL_AMOUNT: u128 = 100_000 * UNIT; +pub const INIT_PRICE: CurrencyAmount = CurrencyAmount::from_rational(1, 10); + pub type SystemError = frame_system::Error; pub use pallet_balances::Call as BalancesCall; pub use pallet_dapp_staking_v3 as DappStakingCall; @@ -193,6 +196,26 @@ impl ExtBuilder { .assimilate_storage(&mut t) .unwrap(); + #[cfg(any(feature = "shibuya"))] + // Setup initial oracle members + as BuildStorage>::assimilate_storage( + &pallet_membership::GenesisConfig:: { + members: vec![ALICE, BOB].try_into().expect("Safe to assume at least 2 members are supported."), + ..Default::default() + }, + &mut t) + .unwrap(); + + #[cfg(any(feature = "shibuya"))] + // Setup initial native currency price + as BuildStorage>::assimilate_storage( + &pallet_price_aggregator::GenesisConfig:: { + circular_buffer: vec![INIT_PRICE].try_into().unwrap(), + }, + &mut t, + ) + .unwrap(); + // Needed to trigger initial inflation config setting. as BuildStorage>::assimilate_storage( &pallet_inflation::GenesisConfig::default(), @@ -241,6 +264,10 @@ pub fn run_to_block(n: BlockNumber) { while System::block_number() < n { let block_number = System::block_number(); TransactionPayment::on_finalize(block_number); + #[cfg(any(feature = "shibuya"))] + Oracle::on_finalize(block_number); + #[cfg(any(feature = "shibuya"))] + PriceAggregator::on_finalize(block_number); DappStaking::on_finalize(block_number); Authorship::on_finalize(block_number); Session::on_finalize(block_number); @@ -258,6 +285,10 @@ pub fn run_to_block(n: BlockNumber) { Timestamp::set_timestamp(block_number as u64 * BLOCK_TIME); TransactionPayment::on_initialize(block_number); DappStaking::on_initialize(block_number); + #[cfg(any(feature = "shibuya"))] + Oracle::on_initialize(block_number); + #[cfg(any(feature = "shibuya"))] + PriceAggregator::on_initialize(block_number); Authorship::on_initialize(block_number); Aura::on_initialize(block_number); AuraExt::on_initialize(block_number);