Staking
Introduction
Staking is an analogue of a bank deposit, receiving passive earnings due to simple storage of cryptomonets. The percentage of income may be different – it all depends on the term of the deposit.
Anyone can create their own Staking program and run it on the Gear Network. To do this, Gear created an example which is available on GitHub.
This article explains the programming interface, data structure, basic functions and explains their purpose. It can be used as is or modified to suit your own scenarios.
Mathematics
You can deposit tokens into the staking program and later claim that token (or another fungible token) for a reward.
Staking involves depositing fungible tokens into a program to earn rewards. These rewards, minted at regular intervals (e.g., per minute), are distributed equitably among all stakers.
How staking works:
Consider Alice, staking 100 tokens and Bob, staking 50 tokens. If reward tokens are minted every minute, and after one week Alice decides to unstake her tokens, the total tokens in the staking program remain 150. The duration of Alice's staking period is 7 days and the reward tokens accumulated can be calculated based on this timeframe:
A week later, Bob chooses to unstake his 50 tokens. In the initial week, he staked 50 tokens from 150. In the second week, he staked 50 tokens from 50. Here’s how to determine his reward:
It is possible to generalize the formula:
where:
- - reward for user for time interval ;
- - rewards minted per minute;
- - total staked amount of tokens at time ;
- - token staked by user at time ;
To apply the formula, one needs to store for each user and time interval, and for each time interval. To calculate a reward, a for loop must be executed for each time interval. This process consumes a significant amount of gas and storage. A more efficient approach is feasible:
Let for a user is constant for . Then:
That equation can be further simplified:
So, the equation to calculate the amount of reward that a user will receive from t=a to t=b under the condition the number of tokens he staked is constant:
Based on that equation the implementation in the program can be written:
(staker.balance * self.tokens_per_stake) / DECIMALS_FACTOR + staker.reward_allowed - staker.reward_debt - staker.distributed
Program description
The admin initializes the program by transmitting information about the staking token, reward token and distribution time (InitStaking
message).
Admin can view the Stakers list (GetStakers
message). The admin can update the reward that will be distributed (UpdateStaking
message).
The user first makes a bet (Stake
message), and then he can receive his reward on demand (GetReward
message). The user can withdraw part of the amount (Withdraw
message).
Source files
staking/src/lib.rs
- contains functions of the 'staking' program.staking/io/src/lib.rs
- contains Enums and structs that the program receives and sends in the reply.
Structs
The program has the following structs:
struct Staking {
owner: ActorId,
staking_token_address: ActorId,
reward_token_address: ActorId,
tokens_per_stake: u128,
total_staked: u128,
distribution_time: u64,
produced_time: u64,
reward_total: u128,
all_produced: u128,
reward_produced: u128,
stakers: HashMap<ActorId, Staker>,
transactions: BTreeMap<ActorId, Transaction<StakingAction>>,
current_tid: TransactionId,
}
where:
owner
- the owner of the staking programstaking_token_address
- address of the staking token programreward_token_address
- address of the reward token programtokens_per_stake
- the calculated value of tokens per staketotal_staked
- total amount of depositsdistribution_time
- time of distribution of rewardreward_total
- the reward to be distributed within distribution timeproduced_time
- time ofreward_total
updateall_produced
- the reward received before the updatereward_total
reward_produced
- the reward produced so farstakers
- 'map' of the 'stakers'transactions
- 'map' of the 'transactions'current_tid
- current transaction identifier.
pub struct InitStaking {
pub staking_token_address: ActorId,
pub reward_token_address: ActorId,
pub distribution_time: u64,
pub reward_total: u128,
}
where:
staking_token_address
- address of the staking token programreward_token_address
- address of the reward token programdistribution_time
- time of distribution of rewardreward_total
- the reward to be distributed within distribution time
pub struct Staker {
pub balance: u128,
pub reward_allowed: u128,
pub reward_debt: u128,
pub distributed: u128,
}
where:
balance
- staked amountreward_allowed
- the reward that could have been received from the withdrawn amountreward_debt
- The reward that the depositor would have received if he had initially paid this amountdistributed
- total remuneration paid
Enums
pub enum StakingAction {
Stake(u128),
Withdraw(u128),
UpdateStaking(InitStaking),
GetReward,
}
pub enum StakingEvent {
StakeAccepted(u128),
Updated,
Reward(u128),
Withdrawn(u128),
}
Functions
The staking program interacts with fungible token contract through function transfer_tokens()
. This function sends a message (the action is defined in the enum FTAction
) and gets a reply (the reply is defined in the enum FTEvent
).
/// Transfers `amount` tokens from `sender` account to `recipient` account.
/// Arguments:
/// * `token_address`: token address
/// * `from`: sender account
/// * `to`: recipient account
/// * `amount_tokens`: amount of tokens
async fn transfer_tokens(
&mut self,
token_address: &ActorId,
from: &ActorId,
to: &ActorId,
amount_tokens: u128,
) -> Result<(), Error> {
let payload = LogicAction::Transfer {
sender: *from,
recipient: *to,
amount: amount_tokens,
};
let transaction_id = self.current_tid;
self.current_tid = self.current_tid.saturating_add(99);
let payload = FTokenAction::Message {
transaction_id,
payload,
};
let result = msg::send_for_reply_as(*token_address, payload, 0, 0)?.await?;
if let FTokenEvent::Err = result {
Err(Error::TransferTokens)
} else {
Ok(())
}
}
Calculates the reward produced so far
fn produced(&mut self) -> u128
Updates the reward produced so far and calculates tokens per stake
fn update_reward(&mut self)
Calculates the maximum possible reward.
The reward that the depositor would have received if he had initially paid this amount
fn get_max_reward(&self, amount: u128) -> u128
Calculates the reward of the staker that is currently available.
The return value cannot be less than zero according to the algorithm
fn calc_reward(&mut self) -> Result<u128, Error>
Updates the staking program.
Sets the reward to be distributed within distribution time
fn update_staking(&mut self, config: InitStaking) -> Result<StakingEvent, Error>
Stakes the tokens
async fn stake(&mut self, amount: u128) -> Result<StakingEvent, Error>
Sends reward to the staker
async fn send_reward(&mut self) -> Result<StakingEvent, Error>
Withdraws the staked the tokens
async fn withdraw(&mut self, amount: u128) -> Result<StakingEvent, Error>
These functions are called in async fn main()
through enum StakingAction
.
This is the entry point to the program, and the program is waiting for a message in StakingAction
format.
#[gstd::async_main]
async fn main() {
let staking = unsafe { STAKING.get_or_insert(Staking::default()) };
let action: StakingAction = msg::load().expect("Could not load Action");
let msg_source = msg::source();
let _reply: Result<StakingEvent, Error> = Err(Error::PreviousTxMustBeCompleted);
let _transaction_id = if let Some(Transaction {
id,
action: pend_action,
}) = staking.transactions.get(&msg_source)
{
if action != *pend_action {
msg::reply(_reply, 0)
.expect("Failed to encode or reply with `Result<StakingEvent, Error>`");
return;
}
*id
} else {
let transaction_id = staking.current_tid;
staking.current_tid = staking.current_tid.saturating_add(1);
staking.transactions.insert(
msg_source,
Transaction {
id: transaction_id,
action: action.clone(),
},
);
transaction_id
};
let result = match action {
StakingAction::Stake(amount) => {
let result = staking.stake(amount).await;
staking.transactions.remove(&msg_source);
result
}
StakingAction::Withdraw(amount) => {
let result = staking.withdraw(amount).await;
staking.transactions.remove(&msg_source);
result
}
StakingAction::UpdateStaking(config) => {
let result = staking.update_staking(config);
staking.transactions.remove(&msg_source);
result
}
StakingAction::GetReward => {
let result = staking.send_reward().await;
staking.transactions.remove(&msg_source);
result
}
};
msg::reply(result, 0).expect("Failed to encode or reply with `Result<StakingEvent, Error>`");
}
Programm metadata and state
Metadata interface description:
pub struct StakingMetadata;
impl Metadata for StakingMetadata {
type Init = In<InitStaking>;
type Handle = InOut<StakingAction, Result<StakingEvent, Error>>;
type Others = ();
type Reply = ();
type Signal = ();
type State = Out<IoStaking>;
}
To display the full program state information, the state()
function is used:
#[no_mangle]
extern fn state() {
let staking = unsafe { STAKING.take().expect("Unexpected error in taking state") };
msg::reply::<IoStaking>(staking.into(), 0)
.expect("Failed to encode or reply with `IoStaking` from `state()`");
}
To display only necessary certain values from the state, you need to write a separate crate. In this crate, specify functions that will return the desired values from the IoStaking
state. For example - staking/state:
#[gmeta::metawasm]
pub mod metafns {
pub type State = IoStaking;
pub fn get_stakers(state: State) -> Vec<(ActorId, Staker)> {
state.stakers
}
pub fn get_staker(state: State, address: ActorId) -> Option<Staker> {
state
.stakers
.iter()
.find(|(id, _staker)| address.eq(id))
.map(|(_, staker)| staker.clone())
}
}
Consistency of program states
The Staking
program interacts with the fungible
token contract. Each transaction that changes the states of Staking and the fungible token is stored in the state until it is completed. User can complete a pending transaction by sending a message exactly the same as the previous one with indicating the transaction id. The idempotency of the fungible token contract allows to restart a transaction without duplicate changes which guarantees the state consistency of these 2 programs.
Conclusion
A source code of the program example is available on GitHub: staking/src/lib.rs
.
See also examples of the program testing implementation based on gtest:
For more details about testing programs written on Gear, refer to this article: Program testing.