Staking
Introduction
Staking is an analogue of a bank deposit, providing passive earnings through the simple storage of cryptocurrency tokens. The percentage of income may vary depending on the term of the deposit.
Anyone can create a Staking program and run it on the Gear Network. An example is available on GitHub.
This article explains the programming interface, data structure, basic functions, and their purposes. The program can be used as-is or modified to suit specific scenarios.
Mathematics
Tokens can be deposited into the staking program and later claimed, along with rewards.
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 out of 150. In the second week, he staked 50 tokens out of 50. Here’s how to determine his reward:
The formula can be generalized as:
where:
- - reward for the user for the time interval ;
- - rewards minted per minute;
- - total staked amount of tokens at time ;
- - tokens staked by the user at time .
To apply the formula, for each user and time interval, and for each time interval must be stored. Calculating a reward involves executing a loop for each time interval, consuming significant gas and storage. A more efficient approach is feasible:
If for a user is constant for , then:
This can be further simplified:
So, the equation to calculate the reward that a user will receive from t=a to t=b under the condition that the number of tokens staked is constant:
Based on that equation, the implementation in the program can be written as:
(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).
The admin can view the Stakers list (GetStakers
message) and update the reward that will be distributed (UpdateStaking
message).
Users first stake tokens (Stake
message), and then can receive rewards on demand (GetReward
message). Users can withdraw part of the staked 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 stakerstransactions
- map of the transactionscurrent_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 the fungible token contract through the 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
/src/lib.rs"
fn update_reward(&mut self)
Calculates the maximum possible reward. The reward that the depositor would have received if 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 the 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 tokens
async fn withdraw(&mut self, amount: u128) -> Result<StakingEvent, Error>
These functions are called in async fn main()
through the 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>`");
}
Program 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 specific values from the state, write a separate crate. In this crate, specify functions that will return the desired values from the IoStaking
state. For example, see 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. A user can complete a pending transaction by sending a message exactly the same as the previous one, indicating the transaction id. The idempotency of the fungible token contract allows restarting a transaction without duplicate changes, ensuring the state consistency of these two programs.
Conclusion
The source code of this example of a staking program 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 Vara, refer to this article: Program testing.