Epoch-based Rewards Pool

View on Github
Smart Contract
DeFi
An example module that manages rewards for multiple tokens on a per-epoch basis.
Author: Kevin

An example smart contract that manages rewards for multiple tokens on a per-epoch basis. Rewards can be added for multiple tokens by anyone for any epoch but only friended modules can increase/decrease claimer shares.

This module is designed to be integrated into a complete system that manages epochs and rewards.


/// An example module that manages rewards for multiple tokens on a per-epoch basis. Rewards can be added for multiple
/// tokens by anyone for any epoch but only friended modules can increase/decrease claimer shares.
///
/// This module is designed to be integrated into a complete system that manages epochs and rewards.
///
/// The flow works as below:
/// 1. A rewards pool is created with a set of reward tokens (fungible assets). If coins are to be used as rewards,
/// developers can use the coin_wrapper module from move-examples/swap to convert coins into fungible assets.
/// 2. Anyone can add rewards to the pool for any epoch for multiple tokens by calling add_rewards.
/// 3. Friended modules can increase/decrease claimer shares for current epoch by calling increase_allocation and
/// decrease_allocation.
/// 4. Claimers can claim their rewards in all tokens for any epoch that has ended by calling claim_rewards, which
/// return a vector of all the rewards. Claiming also removes the claimer's shares from that epoch's rewards as their
/// rewards have all been claimed.
///
/// Although claimers have to be signers, this module can be easily modified to support objects (e.g. NFTs) as claimers.
module rewards_pool::rewards_pool {
use aptos_framework::fungible_asset::{Self, FungibleAsset, FungibleStore, Metadata};
use aptos_framework::primary_fungible_store;
use aptos_framework::object::{Self, Object, ExtendRef};
use aptos_std::pool_u64_unbound::{Self as pool_u64, Pool};
use aptos_std::simple_map::{Self, SimpleMap};
use aptos_std::smart_table::{Self, SmartTable};
use rewards_pool::epoch;
use std::signer;
use std::vector;
/// Rewards can only be claimed for epochs that have ended.
const EREWARDS_CANNOT_BE_CLAIMED_FOR_CURRENT_EPOCH: u64 = 1;
/// The rewards pool does not support the given reward token type.
const EREWARD_TOKEN_NOT_SUPPORTED: u64 = 2;
/// Data regarding the rewards to be distributed for a specific epoch.
struct EpochRewards has store {
/// Total amount of rewards for each reward token added to this epoch.
total_amounts: SimpleMap<Object<Metadata>, u64>,
/// Pool representing the claimer shares in this epoch.
claimer_pool: Pool,
}
/// Data regarding the store object for a specific reward token.
struct RewardStore has store {
/// The fungible store for this reward token.
store: Object<FungibleStore>,
/// We need to keep the fungible store's extend ref to be able to transfer rewards from it during claiming.
store_extend_ref: ExtendRef,
}
#[resource_group_member(group = aptos_framework::object::ObjectGroup)]
struct RewardsPool has key {
/// A mapping to track per epoch rewards data.
epoch_rewards: SmartTable<u64, EpochRewards>,
/// The stores where rewards are kept.
reward_stores: SimpleMap<Object<Metadata>, RewardStore>,
}
/// Create a new rewards pool with the given reward tokens (fungible assets only)
public entry fun create_entry(reward_tokens: vector<Object<Metadata>>) {
create(reward_tokens);
}
/// Create a new rewards pool with the given reward tokens (fungible assets only)
public fun create(reward_tokens: vector<Object<Metadata>>): Object<RewardsPool> {
// The owner of the object doesn't matter as there are no owner-based permissions.
// If developers want to be extra cautious here, they can make the owner @0x0.
// Here the reward pool also doesn't keep an ExtendRef so there would be no way to obtain its signer.
let rewards_pool_constructor_ref = &object::create_object(@rewards_pool);
let rewards_pool_signer = &object::generate_signer(rewards_pool_constructor_ref);
let rewards_pool_addr = signer::address_of(rewards_pool_signer);
let reward_stores = simple_map::new();
vector::for_each(reward_tokens, |reward_token| {
let reward_token: Object<Metadata> = reward_token;
let store_constructor_ref = &object::create_object(rewards_pool_addr);
let store = fungible_asset::create_store(store_constructor_ref, reward_token);
simple_map::add(&mut reward_stores, reward_token, RewardStore {
store,
// The extend ref for the rewards store is kept so we can withdraw rewards from it later when
// claimers claim their rewards.
store_extend_ref: object::generate_extend_ref(store_constructor_ref),
});
});
move_to(rewards_pool_signer, RewardsPool {
epoch_rewards: smart_table::new(),
reward_stores,
});
object::object_from_constructor_ref(rewards_pool_constructor_ref)
}
#[view]
/// Return all the reward tokens supported by the rewards pool.
public fun reward_tokens(rewards_pool: Object<RewardsPool>): vector<Object<Metadata>> acquires RewardsPool {
simple_map::keys(&safe_rewards_pool_data(&rewards_pool).reward_stores)
}
#[view]
/// Return the current shares and total shares of a given claimer for a given rewards pool and epoch.
public fun claimer_shares(
claimer: address,
rewards_pool: Object<RewardsPool>,
epoch: u64,
): (u64, u64) acquires RewardsPool {
let epoch_rewards = smart_table::borrow(&safe_rewards_pool_data(&rewards_pool).epoch_rewards, epoch);
let shares = (pool_u64::shares(&epoch_rewards.claimer_pool, claimer) as u64);
let total_shares = (pool_u64::total_shares(&epoch_rewards.claimer_pool) as u64);
(shares, total_shares)
}
#[view]
/// Return the amounts of claimable rewards for a given claimer, rewards pool, and epoch.
/// The return value is a vector of reward tokens and a vector of amounts.
public fun claimable_rewards(
claimer: address,
rewards_pool: Object<RewardsPool>,
epoch: u64,
): (vector<Object<Metadata>>, vector<u64>) acquires RewardsPool {
assert!(epoch < epoch::now(), EREWARDS_CANNOT_BE_CLAIMED_FOR_CURRENT_EPOCH);
let all_rewards_tokens = reward_tokens(rewards_pool);
let non_empty_reward_tokens = vector[];
let reward_per_tokens = vector[];
let rewards_pool_data = safe_rewards_pool_data(&rewards_pool);
vector::for_each(all_rewards_tokens, |reward_token| {
let reward = rewards(claimer, rewards_pool_data, reward_token, epoch);
if (reward > 0) {
vector::push_back(&mut non_empty_reward_tokens, reward_token);
vector::push_back(&mut reward_per_tokens, reward);
};
});
(non_empty_reward_tokens, reward_per_tokens)
}
/// Allow a claimer to claim the rewards for a past epoch.
/// This returns a vector of rewards for all reward tokens.
public entry fun claim_rewards_entry(
claimer: &signer,
rewards_pool: Object<RewardsPool>,
epoch: u64,
) acquires RewardsPool {
let rewards = claim_rewards(claimer, rewards_pool, epoch);
let claimer_addr = signer::address_of(claimer);
vector::for_each_reverse(rewards, |r| primary_fungible_store::deposit(claimer_addr, r));
}
/// Allow a claimer to claim the rewards for all tokens for a past epoch.
/// This returns a vector of rewards for all reward tokens in the same order as the rewards tokens.
/// If there's no reward for a specific reward token, the corresponding returned reward asset will be of zero
/// amount (created via fungible_asset::zero).
public fun claim_rewards(
claimer: &signer,
rewards_pool: Object<RewardsPool>,
epoch: u64,
): vector<FungibleAsset> acquires RewardsPool {
assert!(epoch < epoch::now(), EREWARDS_CANNOT_BE_CLAIMED_FOR_CURRENT_EPOCH);
let reward_tokens = reward_tokens(rewards_pool);
let rewards = vector[];
let claimer_addr = signer::address_of(claimer);
let rewards_data = unchecked_mut_rewards_pool_data(&rewards_pool);
vector::for_each(reward_tokens, |reward_token| {
let reward = rewards(claimer_addr, rewards_data, reward_token, epoch);
let reward_store = simple_map::borrow(&rewards_data.reward_stores, &reward_token);
if (reward == 0) {
vector::push_back(
&mut rewards,
fungible_asset::zero(fungible_asset::store_metadata(reward_store.store)),
);
} else {
// Withdraw the reward from the corresponding store.
let store_signer = &object::generate_signer_for_extending(&reward_store.store_extend_ref);
vector::push_back(&mut rewards, fungible_asset::withdraw(store_signer, reward_store.store, reward));
// Update the remaining amount of rewards for the epoch.
let epoch_rewards = smart_table::borrow_mut(&mut rewards_data.epoch_rewards, epoch);
let total_token_rewards = simple_map::borrow_mut(&mut epoch_rewards.total_amounts, &reward_token);
*total_token_rewards = *total_token_rewards - reward;
};
});
// Remove the claimer's allocation in the epoch as they have now claimed all rewards for that epoch.
let epoch_rewards = smart_table::borrow_mut(&mut rewards_data.epoch_rewards, epoch);
let all_shares = pool_u64::shares(&epoch_rewards.claimer_pool, claimer_addr);
if (all_shares > 0) {
pool_u64::redeem_shares(&mut epoch_rewards.claimer_pool, claimer_addr, all_shares);
};
rewards
}
/// Add rewards to the specified rewards pool. This can be called with multiple reward tokens.
public fun add_rewards(
rewards_pool: Object<RewardsPool>,
fungible_assets: vector<FungibleAsset>,
epoch: u64,
) acquires RewardsPool {
let rewards_data = unchecked_mut_rewards_pool_data(&rewards_pool);
let reward_stores = &rewards_data.reward_stores;
vector::for_each(fungible_assets, |fa| {
let amount = fungible_asset::amount(&fa);
let reward_token = fungible_asset::metadata_from_asset(&fa);
assert!(simple_map::contains_key(reward_stores, &reward_token), EREWARD_TOKEN_NOT_SUPPORTED);
// Deposit the rewards into the corresponding store.
let reward_store = simple_map::borrow(reward_stores, &reward_token);
fungible_asset::deposit(reward_store.store, fa);
// Update total amount of rewards for this token for this epoch.
let total_amounts = &mut epoch_rewards_or_default(&mut rewards_data.epoch_rewards, epoch).total_amounts;
if (simple_map::contains_key(total_amounts, &reward_token)) {
let current_amount = simple_map::borrow_mut(total_amounts, &reward_token);
*current_amount = *current_amount + amount;
} else {
simple_map::add(total_amounts, reward_token, amount);
};
});
}
/// This should only be called by system modules to increase the shares of a claimer for the current epoch.
public(friend) fun increase_allocation(
claimer: address,
rewards_pool: Object<RewardsPool>,
amount: u64,
) acquires RewardsPool {
let epoch_rewards = &mut unchecked_mut_rewards_pool_data(&rewards_pool).epoch_rewards;
let current_epoch_rewards = epoch_rewards_or_default(epoch_rewards, epoch::now());
pool_u64::buy_in(&mut current_epoch_rewards.claimer_pool, claimer, amount);
}
/// This should only be called by system modules to decrease the shares of a claimer for the current epoch.
public(friend) fun decrease_allocation(
claimer: address,
rewards_pool: Object<RewardsPool>,
amount: u64,
) acquires RewardsPool {
let epoch_rewards = &mut unchecked_mut_rewards_pool_data(&rewards_pool).epoch_rewards;
let current_epoch_rewards = epoch_rewards_or_default(epoch_rewards, epoch::now());
pool_u64::redeem_shares(&mut current_epoch_rewards.claimer_pool, claimer, (amount as u128));
}
fun rewards(
claimer: address,
rewards_pool_data: &RewardsPool,
reward_token: Object<Metadata>,
epoch: u64,
): u64 {
// No rewards (in any tokens) have been added for this epoch.
if (!smart_table::contains(&rewards_pool_data.epoch_rewards, epoch)) {
return 0
};
let epoch_rewards = smart_table::borrow(&rewards_pool_data.epoch_rewards, epoch);
// No rewards have been added for this reward token.
if (!simple_map::contains_key(&epoch_rewards.total_amounts, &reward_token)) {
return 0
};
// Return the claimer's shares of the current total rewards for the epoch.
let total_token_rewards = *simple_map::borrow(&epoch_rewards.total_amounts, &reward_token);
let claimer_shares = pool_u64::shares(&epoch_rewards.claimer_pool, claimer);
pool_u64::shares_to_amount_with_total_coins(&epoch_rewards.claimer_pool, claimer_shares, total_token_rewards)
}
inline fun safe_rewards_pool_data(
rewards_pool: &Object<RewardsPool>,
): &RewardsPool acquires RewardsPool {
borrow_global<RewardsPool>(object::object_address(rewards_pool))
}
inline fun epoch_rewards_or_default(
epoch_rewards: &mut SmartTable<u64, EpochRewards>,
epoch: u64,
): &mut EpochRewards acquires RewardsPool {
if (!smart_table::contains(epoch_rewards, epoch)) {
smart_table::add(epoch_rewards, epoch, EpochRewards {
total_amounts: simple_map::new(),
claimer_pool: pool_u64::create(),
});
};
smart_table::borrow_mut(epoch_rewards, epoch)
}
inline fun unchecked_mut_rewards_pool_data(
rewards_pool: &Object<RewardsPool>,
): &mut RewardsPool acquires RewardsPool {
borrow_global_mut<RewardsPool>(object::object_address(rewards_pool))
}
#[test_only]
friend rewards_pool::rewards_pool_tests;
}