Skip to content

Commit

Permalink
Rewards: added gap mechanism (#1119)
Browse files Browse the repository at this point in the history
* add history storage to the pallet-rewards

* simplify DistributionId trait

* add gap mechanism

* stash

* fix deferred mechanism with storage

* deferred mechanism as a subpallet

* add gap mechanism

* test passing except double movement

* initialize groups by default

* clean TODOs and unwraps

* clean deferred mechanism

* simplify gap mechanism

* update deferred to use its own pallet

* doc updates

* add tests for deferred mechanism

* minor change

* minor doc update

* Minor refactoring

* simplify was_distribution condition

* rename movement group params
  • Loading branch information
lemunozm committed Dec 15, 2022
1 parent c9eb91d commit d5f7159
Show file tree
Hide file tree
Showing 12 changed files with 928 additions and 645 deletions.
17 changes: 7 additions & 10 deletions pallets/rewards/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
The Rewards pallet provides functionality for pull-based reward distributions,
implementing [these traits](https://reference.centrifuge.io/cfg_traits/rewards/index.html) as interface.

![image](https://user-images.githubusercontent.com/15687891/205727900-d578e336-5355-4b6a-8644-bbba004b2387.png)

The user can stake an amount to claim a proportional reward.
The staked amount is reserved/held from the user account for that currency when it's deposited
and unreserved/released when it's withdrawed.
Expand All @@ -16,11 +18,7 @@ in order to change the reward distribution of the associated accounts.

The pallet itself can be seen/understood as a wrapper for pull-based reward distributions.
The exact reward functionality of this pallet is configurable using a mechanism.
Mechanisms implement the reward methods. Current mechanisms:
- [base](https://solmaz.io/2019/02/24/scalable-reward-changing/) mechanism with support for
currency movement.
- [deferred](https://centrifuge.hackmd.io/@Luis/SkB07jq8o) mechanism with support for
currency movement.
Mechanisms implement the reward methods.

**NOTE**: This pallet does not export any extrinsics, it's supposed to be used by other pallets directly or through the
[rewards traits](https://reference.centrifuge.io/cfg_traits/rewards/index.html) this pallet implements.
Expand All @@ -29,12 +27,11 @@ currency movement.

- [`pallet-rewards` API documentation](https://reference.centrifuge.io/pallet_rewards/)
- [Rewards traits API documentation](https://reference.centrifuge.io/cfg_traits/rewards/index.html)
- [Python example](deferred_python_example.py) for the deferred mechanism.
- [The specifications](https://centrifuge.hackmd.io/@Luis/BJz0Ur2Mo) of the reward system.
- Mechanisms:
- [base](https://solmaz.io/2019/02/24/scalable-reward-changing/) mechanism with support for
currency movement.
- [deferred](https://centrifuge.hackmd.io/@Luis/SkB07jq8o) mechanism with support for
currency movement.
- [base](https://solmaz.io/2019/02/24/scalable-reward-changing/) mechanism.
- [deferred](https://centrifuge.hackmd.io/@Luis/SkB07jq8o) mechanism.
- [gap](https://centrifuge.hackmd.io/@Luis/rkJXBz08s) mechanism

## Getting started

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
class DeferredPullBasedDistribution:
class PullBasedDistribution:
"Constant Time Deferred Reward Distribution with Changing Stake Sizes and Deferred Reward"

def __init__(self):
Expand Down Expand Up @@ -75,19 +75,20 @@ def withdraw_reward(self, address):
addr1 = 0x1
addr2 = 0x2

contract = DeferredPullBasedDistribution()
contract = PullBasedDistribution()

contract.deposit_stake(addr1, 100)
contract.deposit_stake(addr2, 50)

contract.distribute(10)

contract.withdraw_stake(addr1, 100)
contract.deposit_stake(addr1, 50)

contract.distribute(10)

# Expected to not be rewarded because the participant withdrawed stake before the second distribution
# Expected to not be rewarded because the participant withdrawed stake before the second distribution (0)
print(contract.withdraw_reward(addr1))

# Expected to be rewarded with the entire first reward distribution (10)
# Expected to be rewarded with the third part of the reward (3.3)
print(contract.withdraw_reward(addr2))
101 changes: 101 additions & 0 deletions pallets/rewards/gap_rewards_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
class PullBasedDistribution:
"Constant Time Reward Distribution with Changing Stake Sizes and Required Gap"

def __init__(self):
# Per group
self.total_stake = 0
self.total_pending_stake = 0
self.reward_per_token = 0
self.reward_per_token_history = {}
self.current_distribution_id = 0

# Per account
self.stake = {0x1: 0, 0x2: 0}
self.reward_tally = {0x1: 0, 0x2: 0}
self.pending_stake = {0x1: 0, 0x2: 0}
self.distribution_id = {0x1: 0, 0x2: 0}

def __update_state(self, address):
"Ensure a valid state for the account before using it"
if self.distribution_id[address] != self.current_distribution_id:
self.stake[address] += self.pending_stake[address]
self.reward_tally[address] += (self.pending_stake[address]
* self.reward_per_token_history[self.distribution_id[address]])
self.distribution_id[address] = self.current_distribution_id
self.pending_stake[address] = 0

def distribute(self, reward):
"Distribute `reward` proportionally to active stakes"
if self.total_stake > 0:
self.reward_per_token += reward / self.total_stake

prev_distribution_id = self.current_distribution_id
self.reward_per_token_history[prev_distribution_id] = self.reward_per_token
self.current_distribution_id += 1
self.total_stake += self.total_pending_stake
self.total_pending_stake = 0

def deposit_stake(self, address, amount):
"Increase the stake of `address` by `amount`"
self.__update_state(address)

self.pending_stake[address] += amount
self.total_pending_stake += amount

def withdraw_stake(self, address, amount):
"Decrease the stake of `address` by `amount`"
if amount > self.stake[address] + self.pending_stake[address]:
raise Exception("Requested amount greater than staked amount")

self.__update_state(address)

pending_amount = min(amount, self.pending_stake[address])
self.pending_stake[address] -= pending_amount
self.total_pending_stake -= pending_amount

computed_amount = amount - pending_amount
self.stake[address] -= computed_amount
self.reward_tally[address] -= self.reward_per_token * computed_amount
self.total_stake -= computed_amount

def compute_reward(self, address):
"Compute reward of `address`. Inmutable"
stake = self.stake[address]
reward_tally = self.reward_tally[address]
if self.distribution_id[address] != self.current_distribution_id:
stake += self.pending_stake[address]
reward_tally += self.pending_stake[address] * self.reward_per_token_history[self.distribution_id[address]]

return stake * self.reward_per_token - reward_tally

def withdraw_reward(self, address):
"Withdraw rewards of `address`"
self.__update_state(address)

reward = self.compute_reward(address)

self.reward_tally[address] = self.stake[address] * self.reward_per_token

return reward

# Example
addr1 = 0x1
addr2 = 0x2

contract = PullBasedDistribution()

contract.deposit_stake(addr1, 100)
contract.deposit_stake(addr2, 50)

contract.distribute(0) # Still nothing to reward here

contract.withdraw_stake(addr1, 100)
contract.deposit_stake(addr1, 50)

contract.distribute(10)

# Expected to not be rewarded because the participant withdrawed stake before the second distribution (0)
print(contract.withdraw_reward(addr1))

# Expected to be rewarded with the entire first reward distribution (10)
print(contract.withdraw_reward(addr2))
50 changes: 20 additions & 30 deletions pallets/rewards/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,11 @@
//!
//! The exact reward functionality of this pallet is given by the mechanism used when it's
//! configured. Current mechanisms:
//! - [base](https://solmaz.io/2019/02/24/scalable-reward-changing/) mechanism with support for
//! - [base](https://solmaz.io/2019/02/24/scalable-reward-changing/) mechanism.
//! currency movement.
//! - [deferred](https://centrifuge.hackmd.io/@Luis/SkB07jq8o) mechanism with support for
//! - [deferred](https://centrifuge.hackmd.io/@Luis/SkB07jq8o) mechanism.
//! currency movement.
//! - [gap](https://centrifuge.hackmd.io/@Luis/rkJXBz08s) mechanism.
//!

#[cfg(test)]
Expand All @@ -75,16 +76,14 @@ use frame_support::{
},
PalletId,
};
use mechanism::{DistributionId, MoveCurrencyError, RewardMechanism};
use mechanism::{MoveCurrencyError, RewardMechanism};
pub use pallet::*;
use sp_runtime::{traits::AccountIdConversion, TokenError};
use sp_std::fmt::Debug;

type RewardCurrencyOf<T, I> = <<T as Config<I>>::RewardMechanism as RewardMechanism>::Currency;
type RewardGroupOf<T, I> = <<T as Config<I>>::RewardMechanism as RewardMechanism>::Group;
type RewardAccountOf<T, I> = <<T as Config<I>>::RewardMechanism as RewardMechanism>::Account;
type DistributionIdOf<T, I> =
<<T as Config<I>>::RewardMechanism as RewardMechanism>::DistributionId;
type BalanceOf<T, I> = <<T as Config<I>>::RewardMechanism as RewardMechanism>::Balance;

#[frame_support::pallet]
Expand Down Expand Up @@ -162,12 +161,6 @@ pub mod pallet {
ValueQuery,
>;

#[pallet::storage]
pub(super) type LastDistributionId<T: Config<I>, I: 'static = ()>
where
DistributionIdOf<T, I>: TypeInfo + MaxEncodedLen + FullCodec + Default,
= StorageValue<_, DistributionIdOf<T, I>, ValueQuery>;

// --------------------------

#[pallet::event]
Expand Down Expand Up @@ -221,29 +214,26 @@ pub mod pallet {
impl<T: Config<I>, I: 'static> GroupRewards for Pallet<T, I>
where
RewardGroupOf<T, I>: FullCodec + Default,
DistributionIdOf<T, I>: FullCodec + Default,
{
type Balance = BalanceOf<T, I>;
type GroupId = T::GroupId;

fn reward_group(group_id: Self::GroupId, reward: Self::Balance) -> DispatchResult {
LastDistributionId::<T, I>::try_mutate(|distribution_id| {
Groups::<T, I>::try_mutate(group_id, |group| {
T::RewardMechanism::reward_group(group, reward, distribution_id.next_id()?)?;
Groups::<T, I>::try_mutate(group_id, |group| {
let reward = T::RewardMechanism::reward_group(group, reward)?;

T::Currency::mint_into(
T::RewardCurrency::get(),
&T::PalletId::get().into_account_truncating(),
reward,
)?;
T::Currency::mint_into(
T::RewardCurrency::get(),
&T::PalletId::get().into_account_truncating(),
reward,
)?;

Self::deposit_event(Event::GroupRewarded {
group_id,
amount: reward,
});
Self::deposit_event(Event::GroupRewarded {
group_id,
amount: reward,
});

Ok(())
})
Ok(())
})
}

Expand Down Expand Up @@ -399,11 +389,11 @@ pub mod pallet {
Err(Error::<T, I>::CurrencyInSameGroup)?;
}

Groups::<T, I>::try_mutate(prev_group_id, |prev_group| -> DispatchResult {
Groups::<T, I>::try_mutate(next_group_id, |next_group| {
T::RewardMechanism::move_currency(currency, prev_group, next_group)
Groups::<T, I>::try_mutate(prev_group_id, |from_group| -> DispatchResult {
Groups::<T, I>::try_mutate(next_group_id, |to_group| {
T::RewardMechanism::move_currency(currency, from_group, to_group)
.map_err(|e| match e {
MoveCurrencyError::Arithmetic(error) => error.into(),
MoveCurrencyError::Internal(error) => error.into(),
MoveCurrencyError::MaxMovements => {
Error::<T, I>::CurrencyMaxMovementsReached.into()
}
Expand Down
57 changes: 17 additions & 40 deletions pallets/rewards/src/mechanism.rs
Original file line number Diff line number Diff line change
@@ -1,88 +1,59 @@
use cfg_traits::ops::ensure::EnsureAddAssign;
use frame_support::traits::tokens::Balance;
use sp_runtime::{traits::Get, ArithmeticError};
use sp_runtime::{traits::Get, ArithmeticError, DispatchError, DispatchResult};

pub mod base;
pub mod deferred;

pub trait DistributionId: Sized {
fn next_id(&mut self) -> Result<Self, ArithmeticError>;
}

impl DistributionId for () {
fn next_id(&mut self) -> Result<Self, ArithmeticError> {
Ok(())
}
}

macro_rules! distribution_id_impl {
($number:ty) => {
impl DistributionId for $number {
fn next_id(&mut self) -> Result<Self, ArithmeticError> {
self.ensure_add_assign(1)?;
Ok(*self)
}
}
};
}

distribution_id_impl!(u8);
distribution_id_impl!(u16);
distribution_id_impl!(u32);
distribution_id_impl!(u64);
distribution_id_impl!(u128);
pub mod gap;

pub trait RewardMechanism {
type Group;
type Account;
type Currency;
type Balance: Balance;
type DistributionId: DistributionId;
type MaxCurrencyMovements: Get<u32>;

/// Reward the group mutating the group entity.
fn reward_group(
group: &mut Self::Group,
amount: Self::Balance,
distribution_id: Self::DistributionId,
) -> Result<(), ArithmeticError>;
) -> Result<Self::Balance, DispatchError>;

/// Add stake to the account and mutates currency and group to archieve that.
fn deposit_stake(
account: &mut Self::Account,
currency: &mut Self::Currency,
group: &mut Self::Group,
amount: Self::Balance,
) -> Result<(), ArithmeticError>;
) -> DispatchResult;

/// Remove stake from the account and mutates currency and group to archieve that.
fn withdraw_stake(
account: &mut Self::Account,
currency: &mut Self::Currency,
group: &mut Self::Group,
amount: Self::Balance,
) -> Result<(), ArithmeticError>;
) -> DispatchResult;

/// Computes the reward for the account
fn compute_reward(
account: &Self::Account,
currency: &Self::Currency,
group: &Self::Group,
) -> Result<Self::Balance, ArithmeticError>;
) -> Result<Self::Balance, DispatchError>;

/// Claims the reward, mutating the account to reflect this action.
/// Once a reward is claimed, next calls will return 0 until the group will be rewarded again.
fn claim_reward(
account: &mut Self::Account,
currency: &Self::Currency,
group: &Self::Group,
) -> Result<Self::Balance, ArithmeticError>;
) -> Result<Self::Balance, DispatchError>;

/// Move a currency from one group to another one.
fn move_currency(
currency: &mut Self::Currency,
prev_group: &mut Self::Group,
next_group: &mut Self::Group,
from_group: &mut Self::Group,
to_group: &mut Self::Group,
) -> Result<(), MoveCurrencyError>;

/// Returns the balance of an account
Expand All @@ -94,12 +65,18 @@ pub trait RewardMechanism {

#[derive(Clone, PartialEq, Debug)]
pub enum MoveCurrencyError {
Arithmetic(ArithmeticError),
Internal(DispatchError),
MaxMovements,
}

impl From<DispatchError> for MoveCurrencyError {
fn from(e: DispatchError) -> MoveCurrencyError {
Self::Internal(e)
}
}

impl From<ArithmeticError> for MoveCurrencyError {
fn from(e: ArithmeticError) -> MoveCurrencyError {
Self::Arithmetic(e)
Self::Internal(DispatchError::Arithmetic(e))
}
}
Loading

0 comments on commit d5f7159

Please sign in to comment.