Skip to content

support multi currency rewards #601

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 24 commits into from
Sep 15, 2021
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
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
5 changes: 2 additions & 3 deletions rewards/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,11 @@ codec = { package = "parity-scale-codec", version = "2.2.0", default-features =
sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.9", default-features = false }
sp-io = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.9", default-features = false }
sp-std = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.9", default-features = false }
sp-core = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.9", default-features = false }
frame-support = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.9", default-features = false }
frame-system = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.9", default-features = false }
orml-traits = { path = "../traits", version = "0.4.1-dev", default-features = false }

[dev-dependencies]
sp-core = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.9" }

[features]
default = ["std"]
std = [
Expand All @@ -28,6 +26,7 @@ std = [
"sp-runtime/std",
"sp-io/std",
"sp-std/std",
"sp-core/std",
"frame-support/std",
"frame-system/std",
"orml-traits/std",
Expand Down
205 changes: 149 additions & 56 deletions rewards/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,31 @@
#![allow(clippy::unused_unit)]
#![cfg_attr(not(feature = "std"), no_std)]

mod migrations;
mod mock;
mod tests;

pub use migrations::migrate_to_multi_currency_reward;

use codec::{FullCodec, HasCompact, MaxEncodedLen};
use frame_support::pallet_prelude::*;
use frame_support::{pallet_prelude::*, weights::Weight};
use orml_traits::RewardHandler;
use sp_core::U256;
use sp_runtime::{
traits::{AtLeast32BitUnsigned, Bounded, MaybeSerializeDeserialize, Member, Saturating, Zero},
FixedPointNumber, FixedPointOperand, FixedU128, RuntimeDebug,
traits::{AtLeast32BitUnsigned, MaybeSerializeDeserialize, Member, Saturating, UniqueSaturatedInto, Zero},
FixedPointOperand, RuntimeDebug, SaturatedConversion,
};
use sp_std::{
borrow::ToOwned,
cmp::{Eq, PartialEq},
collections::btree_map::BTreeMap,
fmt::Debug,
vec::Vec,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should have prelude::* which includes vec and few others

};

use orml_traits::RewardHandler;

/// The Reward Pool Info.
#[derive(Clone, Encode, Decode, PartialEq, Eq, RuntimeDebug, Default, MaxEncodedLen)]
pub struct PoolInfo<Share: HasCompact, Balance: HasCompact> {
pub struct PoolInfoV0<Share: HasCompact, Balance: HasCompact> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can move this to migration

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

/// Total shares amount
#[codec(compact)]
pub total_shares: Share,
Expand All @@ -31,6 +37,29 @@ pub struct PoolInfo<Share: HasCompact, Balance: HasCompact> {
pub total_withdrawn_rewards: Balance,
}

/// The Reward Pool Info.
#[derive(Clone, Encode, Decode, PartialEq, Eq, RuntimeDebug)]
pub struct PoolInfo<Share: HasCompact, Balance: HasCompact, CurrencyId: Ord> {
/// Total shares amount
pub total_shares: Share,
/// Reward infos <reward_currency, (total_reward, total_withdrawn_reward)>
pub rewards: BTreeMap<CurrencyId, (Balance, Balance)>,
}

impl<Share, Balance, CurrencyId> Default for PoolInfo<Share, Balance, CurrencyId>
where
Share: Default + HasCompact,
Balance: HasCompact,
CurrencyId: Ord,
{
fn default() -> Self {
Self {
total_shares: Default::default(),
rewards: BTreeMap::new(),
}
}
}

pub use module::*;

#[frame_support::pallet]
Expand Down Expand Up @@ -62,21 +91,33 @@ pub mod module {
/// The reward pool ID type.
type PoolId: Parameter + Member + Clone + FullCodec;

type CurrencyId: Parameter + Member + Copy + MaybeSerializeDeserialize + Ord;

/// The `RewardHandler`
type Handler: RewardHandler<Self::AccountId, Balance = Self::Balance, PoolId = Self::PoolId>;
type Handler: RewardHandler<Self::AccountId, Self::CurrencyId, Balance = Self::Balance, PoolId = Self::PoolId>;
}

/// Stores reward pool info.
#[pallet::storage]
#[pallet::getter(fn pools)]
pub type Pools<T: Config> = StorageMap<_, Twox64Concat, T::PoolId, PoolInfo<T::Share, T::Balance>, ValueQuery>;
pub type Pools<T: Config> =
StorageMap<_, Twox64Concat, T::PoolId, PoolInfo<T::Share, T::Balance, T::CurrencyId>, ValueQuery>;

/// Record share amount and withdrawn reward amount for specific `AccountId`
/// under `PoolId`.
/// Record share amount, reward currency and withdrawn reward amount for
/// specific `AccountId` under `PoolId`.
///
/// double_map (PoolId, AccountId) => (Share, BTreeMap<CurrencyId, Balance>)
#[pallet::storage]
#[pallet::getter(fn share_and_withdrawn_reward)]
pub type ShareAndWithdrawnReward<T: Config> =
StorageDoubleMap<_, Twox64Concat, T::PoolId, Twox64Concat, T::AccountId, (T::Share, T::Balance), ValueQuery>;
pub type ShareAndWithdrawnReward<T: Config> = StorageDoubleMap<
_,
Twox64Concat,
T::PoolId,
Twox64Concat,
T::AccountId,
(T::Share, BTreeMap<T::CurrencyId, T::Balance>),
ValueQuery,
>;

#[pallet::pallet]
pub struct Pallet<T>(_);
Expand All @@ -89,12 +130,19 @@ pub mod module {
}

impl<T: Config> Pallet<T> {
pub fn accumulate_reward(pool: &T::PoolId, reward_increment: T::Balance) {
if !reward_increment.is_zero() {
Pools::<T>::mutate(pool, |pool_info| {
pool_info.total_rewards = pool_info.total_rewards.saturating_add(reward_increment)
});
pub fn accumulate_reward(pool: &T::PoolId, reward_currency: T::CurrencyId, reward_increment: T::Balance) {
if reward_increment.is_zero() {
return;
}
Pools::<T>::mutate(pool, |pool_info| {
if let Some((total_reward, _)) = pool_info.rewards.get_mut(&reward_currency) {
*total_reward = total_reward.saturating_add(reward_increment);
} else {
pool_info
.rewards
.insert(reward_currency, (reward_increment, Zero::zero()));
}
});
}

pub fn add_share(who: &T::AccountId, pool: &T::PoolId, add_amount: T::Share) {
Expand All @@ -103,21 +151,45 @@ impl<T: Config> Pallet<T> {
}

Pools::<T>::mutate(pool, |pool_info| {
let reward_inflation = if pool_info.total_shares.is_zero() {
Zero::zero()
} else {
let proportion = FixedU128::checked_from_rational(add_amount, pool_info.total_shares)
.unwrap_or_else(FixedU128::max_value);
proportion.saturating_mul_int(pool_info.total_rewards)
};

let initial_total_shares = pool_info.total_shares;
pool_info.total_shares = pool_info.total_shares.saturating_add(add_amount);
pool_info.total_rewards = pool_info.total_rewards.saturating_add(reward_inflation);
pool_info.total_withdrawn_rewards = pool_info.total_withdrawn_rewards.saturating_add(reward_inflation);

let mut withdrawn_inflation = Vec::<(T::CurrencyId, T::Balance)>::new();

pool_info
.rewards
.iter_mut()
.for_each(|(reward_currency, (total_reward, total_withdrawn_reward))| {
let reward_inflation = if initial_total_shares.is_zero() {
Zero::zero()
} else {
U256::from(add_amount.to_owned().saturated_into::<u128>())
.saturating_mul(total_reward.to_owned().saturated_into::<u128>().into())
.checked_div(initial_total_shares.to_owned().saturated_into::<u128>().into())
.unwrap_or_default()
.as_u128()
.saturated_into()
};
*total_reward = total_reward.saturating_add(reward_inflation);
*total_withdrawn_reward = total_withdrawn_reward.saturating_add(reward_inflation);

withdrawn_inflation.push((*reward_currency, reward_inflation));
});

ShareAndWithdrawnReward::<T>::mutate(pool, who, |(share, withdrawn_rewards)| {
*share = share.saturating_add(add_amount);
*withdrawn_rewards = withdrawn_rewards.saturating_add(reward_inflation);
// update withdrawn inflation for each reward currency
withdrawn_inflation
.into_iter()
.for_each(|(reward_currency, reward_inflation)| {
let withdrawn_reward = withdrawn_rewards
.get(&reward_currency)
.copied()
.unwrap_or_default()
.saturating_add(reward_inflation);

withdrawn_rewards.insert(reward_currency, withdrawn_reward);
});
});
});
}
Expand All @@ -139,17 +211,30 @@ impl<T: Config> Pallet<T> {
}

Pools::<T>::mutate(pool, |pool_info| {
let proportion = FixedU128::checked_from_rational(remove_amount, share)
.expect("share is gte remove_amount and not zero which checked before; qed");
let withdrawn_rewards_to_remove = proportion.saturating_mul_int(withdrawn_rewards);
let removing_share = U256::from(remove_amount.saturated_into::<u128>());

pool_info.total_shares = pool_info.total_shares.saturating_sub(remove_amount);
pool_info.total_rewards = pool_info.total_rewards.saturating_sub(withdrawn_rewards_to_remove);
pool_info.total_withdrawn_rewards = pool_info
.total_withdrawn_rewards
.saturating_sub(withdrawn_rewards_to_remove);

withdrawn_rewards = withdrawn_rewards.saturating_sub(withdrawn_rewards_to_remove);
// update withdrawn rewards for each reward currency
withdrawn_rewards
.iter_mut()
.for_each(|(reward_currency, withdrawn_reward)| {
let withdrawn_reward_to_remove: T::Balance = removing_share
.saturating_mul(withdrawn_reward.to_owned().saturated_into::<u128>().into())
.checked_div(share.saturated_into::<u128>().into())
.unwrap_or_default()
.as_u128()
.saturated_into();

if let Some((total_reward, total_withdrawn_reward)) =
pool_info.rewards.get_mut(reward_currency)
{
*total_reward = total_reward.saturating_sub(withdrawn_reward_to_remove);
*total_withdrawn_reward =
total_withdrawn_reward.saturating_sub(withdrawn_reward_to_remove);
}
*withdrawn_reward = withdrawn_reward.saturating_sub(withdrawn_reward_to_remove);
});
});

share = share.saturating_sub(remove_amount);
Expand Down Expand Up @@ -177,26 +262,34 @@ impl<T: Config> Pallet<T> {
}

Pools::<T>::mutate(pool, |pool_info| {
let proportion = FixedU128::checked_from_rational(*share, pool_info.total_shares).unwrap_or_default();
let reward_to_withdraw = proportion
.saturating_mul_int(pool_info.total_rewards)
.saturating_sub(*withdrawn_rewards)
.min(
pool_info
.total_rewards
.saturating_sub(pool_info.total_withdrawn_rewards),
);

if reward_to_withdraw.is_zero() {
return;
}

pool_info.total_withdrawn_rewards =
pool_info.total_withdrawn_rewards.saturating_add(reward_to_withdraw);
*withdrawn_rewards = withdrawn_rewards.saturating_add(reward_to_withdraw);

// pay reward to `who`
T::Handler::payout(who, pool, reward_to_withdraw);
let total_shares = U256::from(pool_info.total_shares.to_owned().saturated_into::<u128>());
pool_info
.rewards
.iter_mut()
.for_each(|(reward_currency, (total_reward, total_withdrawn_reward))| {
let withdrawn_reward = withdrawn_rewards.get(reward_currency).copied().unwrap_or_default();

let total_reward_proportion: T::Balance = U256::from(share.to_owned().saturated_into::<u128>())
.saturating_mul(U256::from(total_reward.to_owned().saturated_into::<u128>()))
.checked_div(total_shares)
.unwrap_or_default()
.as_u128()
.unique_saturated_into();

let reward_to_withdraw = total_reward_proportion
.saturating_sub(withdrawn_reward)
.min(total_reward.saturating_sub(*total_withdrawn_reward));

if reward_to_withdraw.is_zero() {
return;
}

*total_withdrawn_reward = total_withdrawn_reward.saturating_add(reward_to_withdraw);
withdrawn_rewards.insert(*reward_currency, withdrawn_reward.saturating_add(reward_to_withdraw));

// pay reward to `who`
T::Handler::payout(who, pool, *reward_currency, reward_to_withdraw);
});
});
});
}
Expand Down
37 changes: 37 additions & 0 deletions rewards/src/migrations.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
use super::*;

pub fn migrate_to_multi_currency_reward<T: Config>(
get_reward_currency: impl Fn(&T::PoolId) -> T::CurrencyId,
) -> Weight {
let mut reads_writes: Weight = 0;
Pools::<T>::translate::<PoolInfoV0<T::Share, T::Balance>, _>(|pool_id, old_pool_info| {
reads_writes += 1;
let currency_id = get_reward_currency(&pool_id);

let mut rewards = BTreeMap::new();
rewards.insert(
currency_id,
(old_pool_info.total_rewards, old_pool_info.total_withdrawn_rewards),
);

Some(PoolInfo {
total_shares: old_pool_info.total_shares,
rewards,
})
});

ShareAndWithdrawnReward::<T>::translate::<(T::Share, T::Balance), _>(
|pool_id, _who, (shares, withdrawn_rewards)| {
reads_writes += 1;
let currency_id = get_reward_currency(&pool_id);

let mut withdrawn = BTreeMap::new();
withdrawn.insert(currency_id, withdrawn_rewards);

Some((shares, withdrawn))
},
);

// Return the weight consumed by the migration.
T::DbWeight::get().reads_writes(reads_writes, reads_writes)
}
14 changes: 9 additions & 5 deletions rewards/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,14 @@ pub type Balance = u64;
pub type Share = u64;
pub type PoolId = u32;
pub type BlockNumber = u64;
pub type CurrencyId = u32;

pub const ALICE: AccountId = 1;
pub const BOB: AccountId = 2;
pub const CAROL: AccountId = 3;
pub const DOT_POOL: PoolId = 1;
pub const NATIVE_COIN: CurrencyId = 0;
pub const STABLE_COIN: CurrencyId = 1;

parameter_types! {
pub const BlockHashCount: u64 = 250;
Expand Down Expand Up @@ -53,21 +56,21 @@ impl frame_system::Config for Runtime {
}

thread_local! {
pub static RECEIVED_PAYOUT: RefCell<HashMap<(PoolId, AccountId), Balance>> = RefCell::new(HashMap::new());
pub static RECEIVED_PAYOUT: RefCell<HashMap<(PoolId, AccountId, CurrencyId), Balance>> = RefCell::new(HashMap::new());
}

pub struct Handler;
impl RewardHandler<AccountId> for Handler {
impl RewardHandler<AccountId, CurrencyId> for Handler {
type Balance = Balance;
type PoolId = PoolId;

fn payout(who: &AccountId, pool: &Self::PoolId, amount: Self::Balance) {
fn payout(who: &AccountId, pool: &Self::PoolId, currency_id: CurrencyId, amount: Self::Balance) {
RECEIVED_PAYOUT.with(|v| {
let mut old_map = v.borrow().clone();
if let Some(before) = old_map.get_mut(&(*pool, *who)) {
if let Some(before) = old_map.get_mut(&(*pool, *who, currency_id)) {
*before += amount;
} else {
old_map.insert((*pool, *who), amount);
old_map.insert((*pool, *who, currency_id), amount);
};

*v.borrow_mut() = old_map;
Expand All @@ -79,6 +82,7 @@ impl Config for Runtime {
type Share = Share;
type Balance = Balance;
type PoolId = PoolId;
type CurrencyId = CurrencyId;
type Handler = Handler;
}

Expand Down
Loading