Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
79bea4b
chore(deps): cargo update --workspace
mattsse Apr 15, 2021
7aacf06
fix(deps): update renamed xcm crate names
mattsse Apr 15, 2021
c3799e4
fix: rename ModuleId to PalletId
mattsse Apr 15, 2021
7b3b477
feat:pallets/orcale: add oracle pallet template
mattsse Apr 9, 2021
9c566a6
feat:pallets/orcale: rename to price feed and include chainlink feed
mattsse Apr 15, 2021
82f4b46
feat:pallets/price-feed: integrate latest chainlink price feed pallet
mattsse Apr 15, 2021
09c2662
rustfmt
mattsse Apr 15, 2021
e5325d5
chore(deps): sync with chainlink deps
mattsse Apr 15, 2021
82ff8c8
chore(deps): sort members
mattsse Apr 16, 2021
39edc21
feat:pallets/price-feed: add types
mattsse Apr 16, 2021
2a07bf4
feat:pallets/price-feed: introduce price feed types and price feed trait
mattsse Apr 16, 2021
f0bd7e8
feat:pallets/price-feed: empty price feed trait implementation
mattsse Apr 16, 2021
6112a62
rustfmt
mattsse Apr 16, 2021
7851a9f
chore(deps): enable std feature for balances
mattsse Apr 16, 2021
ba70d39
feat:pallets/price-feed: add mock config
mattsse Apr 16, 2021
1946262
docs:pallets/price-feed: clarify notes on precision
mattsse Apr 16, 2021
1d609bc
test:pallets/price-feed: add basic feed creation test
mattsse Apr 16, 2021
b7ace6d
rustfmt
mattsse Apr 16, 2021
92bc95e
Update pallets/price-feed/src/lib.rs
mattsse Apr 19, 2021
b36ae3f
feat:pallets/price-feed: add mapping from assetId to feedId
mattsse Apr 19, 2021
d23ca01
feat:pallets/price-feed: add genesis build support
mattsse Apr 19, 2021
b7e8fea
refactor:pallets/price-feed: replace generic price type with FixedU128
mattsse Apr 19, 2021
a23ba9c
feat:pallets/price-feed: implement get_price_pair
mattsse Apr 19, 2021
7007279
rustfmt
mattsse Apr 19, 2021
bb074a6
chore(clippy): make clippy happy
mattsse Apr 19, 2021
f1b60ab
chore(deps): update chainlink feed pallet
mattsse Apr 19, 2021
9763582
feat:pallets/price-feed: drop unused type
mattsse Apr 19, 2021
549f8f0
test:pallets/price-feed: update mock template
mattsse Apr 19, 2021
46aef80
test:pallets/price-feed: add price pair tests
mattsse Apr 19, 2021
70444ee
test:pallets/price-feed: reexport feedbuilder for tests
mattsse Apr 19, 2021
0b3cc33
test:pallets/price-feed: add price feed tests
mattsse Apr 19, 2021
0b62072
chore:pallets/price-feed: remove commented out config typ eartefacts
mattsse Apr 19, 2021
fbf1c66
Merge branch 'main' into matt/add-price-feed
mattsse Apr 22, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,093 changes: 1,293 additions & 800 deletions Cargo.lock

Large diffs are not rendered by default.

76 changes: 76 additions & 0 deletions pallets/price-feed/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
[package]
authors = ['ChainSafe Systems']
description = 'FRAME pallet to implement PINT price feeds.'
edition = '2018'
license = 'LGPL-3.0-only'
name = 'pallet-price-feed'
readme = 'README.md'
repository = 'https://github.com/ChainSafe/PINT/'
version = '0.0.1'

[features]
default = ['std']
std = [
'serde',
'codec/std',
'frame-support/std',
'frame-system/std',
'pallet-balances/std',
'pallet-chainlink-feed/std',
]
[dependencies.codec]
default-features = false
features = ['derive']
package = 'parity-scale-codec'
version = '2.0.0'

[dependencies.frame-support]
default-features = false
git = 'https://github.com/paritytech/substrate.git'
branch = 'rococo-v1'
version = '3.0.0'

[dependencies.frame-system]
default-features = false
git = 'https://github.com/paritytech/substrate.git'
branch = 'rococo-v1'
version = '3.0.0'

[dependencies.pallet-chainlink-feed]
default_features = false
package = 'pallet-chainlink-feed'
git = "https://github.com/mattsse/chainlink-polkadot"
# this is substrate v3 branch that is upstreamed with rococo-v1
# and requires https://github.com/ChainSafe/PINT/issues/37
# see also https://github.com/ChainSafe/PINT/pull/39
branch = "substrate-v3"

[dependencies]
serde = { version = "1.0.101", optional = true }

[dev-dependencies.pallet-balances]
default-features = false
git = 'https://github.com/paritytech/substrate.git'
branch = 'rococo-v1'
version = '3.0.0'

[dev-dependencies.sp-core]
default-features = false
git = 'https://github.com/paritytech/substrate.git'
branch = 'rococo-v1'
version = '3.0.0'

[dev-dependencies.sp-io]
default-features = false
git = 'https://github.com/paritytech/substrate.git'
branch = 'rococo-v1'
version = '3.0.0'

[dev-dependencies.sp-runtime]
default-features = false
git = 'https://github.com/paritytech/substrate.git'
branch = 'rococo-v1'
version = '3.0.0'

[package.metadata.docs.rs]
targets = ['x86_64-unknown-linux-gnu']
1 change: 1 addition & 0 deletions pallets/price-feed/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
License: LGPL-3.0-only
270 changes: 270 additions & 0 deletions pallets/price-feed/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
// Copyright 2021 ChainSafe Systems
// SPDX-License-Identifier: LGPL-3.0-only

#![cfg_attr(not(feature = "std"), no_std)]

pub use pallet::*;

#[cfg(test)]
mod mock;
#[cfg(test)]
pub use mock::FeedBuilder;

#[cfg(test)]
mod tests;

/// Additional type used in this pallet
mod traits;
/// Additional structures used in this pallet
mod types;

#[frame_support::pallet]
// this is requires as the #[pallet::event] proc macro generates code that violates this lint
#[allow(clippy::unused_unit)]
pub mod pallet {
pub use crate::traits::PriceFeed;
use crate::types::{AssetPricePair, Price};
use frame_support::sp_runtime::FixedPointNumber;
use frame_support::sp_std::convert::TryInto;
#[cfg(feature = "std")]
use frame_support::traits::GenesisBuild;
use frame_support::{pallet_prelude::*, traits::Get};
use frame_system::pallet_prelude::*;
use pallet_chainlink_feed::{FeedInterface, FeedOracle};
use std::cmp::Ordering;

type FeedIdFor<T> = <<T as Config>::Oracle as FeedOracle<T>>::FeedId;

type FeedValueFor<T> =
<<<T as Config>::Oracle as FeedOracle<T>>::Feed as FeedInterface<T>>::Value;

/// Provides access to all the price feeds
/// This is used to determine the equivalent amount of PINT for assets
#[pallet::config]
pub trait Config: frame_system::Config {
/// The origin that is allowed to insert asset -> feed mappings
type AdminOrigin: EnsureOrigin<Self::Origin>;

/// The asset identifier for the native asset (PINT).
#[pallet::constant]
type SelfAssetId: Get<Self::AssetId>;

/// Type used to identify the assets.
type AssetId: Parameter + Member + MaybeSerializeDeserialize;

/// The internal oracle that gives access to the asset's price feeds.
///
/// NOTE: this assumes all the feeds provide data in the same base currency.
/// When querying the price of an asset (`quote`/`asset`) from the oracle,
/// its price is given by means of the asset pair `(base / quote)`. (e.g. DOT/PINT)
type Oracle: FeedOracle<Self>;

type Event: From<Event<Self>> + IsType<<Self as frame_system::Config>::Event>;
}

#[pallet::pallet]
#[pallet::generate_store(pub(super) trait Store)]
pub struct Pallet<T>(_);

#[pallet::storage]
/// Store a mapping (AssetId) -> FeedId for all active assets
pub type AssetFeeds<T: Config> =
StorageMap<_, Blake2_128Concat, T::AssetId, FeedIdFor<T>, OptionQuery>;

#[pallet::genesis_config]
pub struct GenesisConfig<T: Config>
where
<<T as Config>::Oracle as FeedOracle<T>>::FeedId: MaybeSerializeDeserialize,
{
/// The mappings to insert at genesis
pub asset_feeds: Vec<(T::AssetId, FeedIdFor<T>)>,
}

#[cfg(feature = "std")]
impl<T: Config> Default for GenesisConfig<T>
where
<<T as Config>::Oracle as FeedOracle<T>>::FeedId: MaybeSerializeDeserialize,
{
fn default() -> Self {
Self {
asset_feeds: Default::default(),
}
}
}

#[pallet::genesis_build]
impl<T: Config> GenesisBuild<T> for GenesisConfig<T>
where
<<T as Config>::Oracle as FeedOracle<T>>::FeedId: MaybeSerializeDeserialize,
{
fn build(&self) {
for (asset, feed) in &self.asset_feeds {
AssetFeeds::<T>::insert(asset.clone(), feed.clone())
}
}
}

#[cfg(feature = "std")]
impl<T: Config> GenesisConfig<T>
where
<<T as Config>::Oracle as FeedOracle<T>>::FeedId: MaybeSerializeDeserialize,
{
/// Direct implementation of `GenesisBuild::build_storage`.
///
/// Kept in order not to break dependency.
pub fn build_storage(&self) -> Result<frame_support::sp_runtime::Storage, String> {
<Self as GenesisBuild<T>>::build_storage(self)
}

/// Direct implementation of `GenesisBuild::assimilate_storage`.
///
/// Kept in order not to break dependency.
pub fn assimilate_storage(
&self,
storage: &mut frame_support::sp_runtime::Storage,
) -> Result<(), String> {
<Self as GenesisBuild<T>>::assimilate_storage(self, storage)
}
}

#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
/// A new assetId -> feedId mapping was inserted
/// \[AssetId, NewFeedId, OldFeedId\]
UpdateAssetPriceFeed(T::AssetId, FeedIdFor<T>, Option<FeedIdFor<T>>),
/// An assetId -> feedId was removed
/// \[AssetId, FeedId\]
RemoveAssetPriceFeed(T::AssetId, Option<FeedIdFor<T>>),
}

#[pallet::call]
impl<T: Config> Pallet<T> {
/// Callable by an admin to track a price feed identifier for the asset
#[pallet::weight(10_000)] // TODO: Set weights
pub fn track_asset_price_feed(
origin: OriginFor<T>,
asset_id: T::AssetId,
feed_id: FeedIdFor<T>,
) -> DispatchResultWithPostInfo {
T::AdminOrigin::ensure_origin(origin)?;
let old_feed_id = AssetFeeds::<T>::mutate(&asset_id, |maybe_feed_id| {
maybe_feed_id.replace(feed_id.clone())
});
Self::deposit_event(Event::UpdateAssetPriceFeed(asset_id, feed_id, old_feed_id));
Ok(().into())
}

/// Callable by an admin to untrack the asset's price feed.
#[pallet::weight(10_000)] // TODO: Set weights
pub fn untrack_asset_price_feed(
origin: OriginFor<T>,
asset_id: T::AssetId,
) -> DispatchResultWithPostInfo {
T::AdminOrigin::ensure_origin(origin)?;
let feed_id = AssetFeeds::<T>::take(&asset_id);
Self::deposit_event(Event::RemoveAssetPriceFeed(asset_id, feed_id));
Ok(().into())
}
}

#[pallet::error]
pub enum Error<T> {
/// Thrown if no price feed was found for an asset
AssetPriceFeedNotFound,
/// Thrown when the underlying price feed does not yet contain a valid round.
InvalidFeedValue,
/// Thrown if the calculation of the price ratio fails due to exceeding the
/// accuracy of the configured price.
ExceededAccuracy,
}

#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {}

impl<T: Config> Pallet<T> {
/// Returns the corresponding identifier for the asset's price feed
pub fn get_asset_feed_id(asset_id: &T::AssetId) -> Option<FeedIdFor<T>> {
AssetFeeds::<T>::get(asset_id)
}

/// Returns the latest value in the feed together with the feed's decimals
/// or an error if no feed was found for the given
/// or the feed doesn't contain any valid round yet.
fn get_latest_valid_value(
feed_id: FeedIdFor<T>,
) -> Result<(FeedValueFor<T>, u8), DispatchError> {
let feed = T::Oracle::feed(feed_id).ok_or(Error::<T>::AssetPriceFeedNotFound)?;
ensure!(
feed.first_valid_round().is_some(),
Error::<T>::InvalidFeedValue
);
Ok((feed.latest_data().answer, feed.decimals()))
}
}
impl<T: Config> Pallet<T>
where
FeedValueFor<T>: TryInto<u128>,
{
fn adjust_with_multiplier(value: u128, exp: u8) -> Result<u128, DispatchError> {
let multiplier = 10u128
.checked_pow(exp.into())
.ok_or(Error::<T>::ExceededAccuracy)?;
Ok(value
.checked_mul(multiplier)
.ok_or(Error::<T>::ExceededAccuracy)?)
}
}

impl<T: Config> PriceFeed<T::AssetId> for Pallet<T>
where
FeedValueFor<T>: TryInto<u128>,
{
/// Returns a `AssetPricePair` where `base` is the configured `SelfAssetId`.
fn get_price(quote: T::AssetId) -> Result<AssetPricePair<T::AssetId>, DispatchError> {
Self::get_price_pair(T::SelfAssetId::get(), quote)
}

fn get_price_pair(
base: T::AssetId,
quote: T::AssetId,
) -> Result<AssetPricePair<T::AssetId>, DispatchError> {
let base_feed_id =
Self::get_asset_feed_id(&base).ok_or(Error::<T>::AssetPriceFeedNotFound)?;
let quote_feed_id =
Self::get_asset_feed_id(&quote).ok_or(Error::<T>::AssetPriceFeedNotFound)?;

let (last_base_value, base_decimals) = Self::get_latest_valid_value(base_feed_id)?;
let (last_quote_value, quote_decimals) = Self::get_latest_valid_value(quote_feed_id)?;

let mut last_base_value = last_base_value
.try_into()
.map_err(|_| Error::<T>::ExceededAccuracy)?;
let mut last_quote_value = last_quote_value
.try_into()
.map_err(|_| Error::<T>::ExceededAccuracy)?;

// upscale the precision of the feed, which measures in fewer decimals
match base_decimals.cmp(&quote_decimals) {
Ordering::Less => {
last_base_value = Self::adjust_with_multiplier(
last_base_value,
quote_decimals - base_decimals,
)?;
}
Ordering::Greater => {
last_quote_value = Self::adjust_with_multiplier(
last_quote_value,
base_decimals - quote_decimals,
)?;
}
_ => {}
}

let price = Price::checked_from_rational(last_base_value, last_quote_value)
.ok_or(Error::<T>::ExceededAccuracy)?;

Ok(AssetPricePair { base, quote, price })
}
}
}
Loading