Skip to main content

🪙 Staking

Introduction​

The Staking module is an abstract solution that allows users to stake tokens and earn rewards based on their shares. This module is designed to be flexible and modular, so it can be easily customized to meet the needs of your specific application.

Implementation​

The module holds two primary contracts: the AValueDistributor and the AStaking. The first one handles the distribution calculations according to the algorithm that ensures the distribution is proportional to the shares held by each user and takes into account changes in the cumulative sum over time. The formula being used is:

where:

  • r(u, k, n) = rewards earned by user u from k to n seconds
  • S = amount staked by user u for time k to n - 1
  • Ti = total staked at time i (assume Ti > 0)
  • R = reward rate per second (total rewards / duration)

1 Abstract Value Distributor contract​

The AValueDistributor contract defines the core logic for distributing value among users based on their shares. This contract can be used as a base contract for implementing various distribution mechanisms, such as token staking, profit sharing, or dividend distribution.

It includes the following public functions:

FunctionDescription
totalSharesProvides the total number of all the shares added to the contract
cumulativeSumProvides the current cumulative sum of value that has been distributed
updatedAtProvides the timestamp of the last update
userDistributionProvides the distribution details for a specific user, including the user's shares, cumulative sum and value owed
getOwedValueProvides the amount of value owed to a specific user at the current time

There are also key internal functions for the shares and value manipulations:

FunctionDescription
_addSharesAdds shares to a user's distribution
_removeSharesRemoves shares from a user's distribution
_distributeValueDistributes value to a specific user

The contract includes _afterAddShares, _afterRemoveShares and _afterDistributeValue hooks for performing any required additional logic, such as transferring tokens, when shares are added or removed, or when value is distributed.

The core function affecting the distribution calculation is _getValueToDistribute. It returns the value to be distributed for a given period. It will usually be required to override this function to provide custom distribution mechanics in the inherited contracts.

2 Abstract Staking contract​

The AStaking contract inherits from the Abstract Value Distributor contract and adds user-oriented functionality for staking tokens and earning rewards. It allows setting ERC20 tokens as the shares token and the rewards token (which cannot be further changed), as well as setting the rate and the staking start time during the contract initialization using the internal __AStaking_init function. All of the initialized values are available using sharesToken , rewardsToken , rate and stakingStartTime functions.

When shares and rewards tokens are the same, users may accidentally withdraw other users' shares as a reward if the rewards token balance is improperly handled.

The rate and the staking start time can be changed with the help of the following internal functions:

FunctionDescription
_setRateSets the rate of rewards distribution per second
_setStakingStartTimeSets the staking start time

A chosen rate determines the rate at which rewards are earned and distributed to stakers based on their shares.

Ensure that the rate value is set correctly to match the decimal precision of the rewards token to ensure accurate rewards distribution.

The use of the staking start time entity introduces the modifier stakingStarted that throws if someone tries to manipulate the shares or rewards but the staking has not started yet.

The key functions for staking and claiming rewards:

FunctionDescription
stakeStakes the specified amount of tokens
unstakeUnstakes the specified amount of tokens
claimClaims the specified amount of rewards
withdrawWithdraws all the staked tokens together with the rewards if there are any. It also returns the amount of shares being withdrawn and the total value of rewards owed to a user

Example​

As long as our AStaking contract is abstract you need to inherit it as the first step and initialize the needed values in any suitable way. Let's do it in the constructor.

import "@solarity/solidity-lib/staking/AStaking.sol";

contract Staking is AStaking {
constructor(
address sharesToken_,
address rewardsToken_,
uint256 rate_,
uint256 stakingStartTime_
) {
__AStaking_init(
sharesToken_,
rewardsToken_,
rate_,
stakingStartTime_
);
}
}

Remember that the rate value should match the decimal precision of the rewards token to ensure accurate rewards distribution.

Another important step is to override the _getValueToDistribute function to provide the custom distribution mechanic. Let's set the distribution to 1 token with _rate decimals per minute.

function _getValueToDistribute(
uint256 timeUpTo_,
uint256 timeLastUpdate_
) internal view virtual override returns (uint256) {
uint256 stakingStartTime_ = stakingStartTime();

if (
stakingStartTime_ < timeUpTo_ ||
stakingStartTime_ < timeLastUpdate_
) {
return 0;
}

return _rate * (
(timeUpTo_ - _stakingStartTime) / 60 -
(timeLastUpdate_ - _stakingStartTime) / 60
);
}

Now you can interact with the contract, moving your tokens in any desired way and earning rewards.

address sharesToken_ = 0x..;
address rewardsToken18_ = 0x..;

Staking staking_ = new Staking(
sharesToken_,
rewardsToken18_,
1 ether,
block.timestamp
);

// Stakes 1 token
staking_.stake(1 ether);

// Reverts with "ValueDistributor: amount has to be more than 0"
staking_.stake(0);

// Unstakes 0.1 token
staking_.unstake(0.1 ether);

// Reverts with "ValueDistributor: amount has to be more than 0"
staking_.unstake(0);

// Reverts with "ValueDistributor: insufficient amount"
staking_.unstake(1 ether);

// Claims 1 wei
staking_.claim(1 wei);

// Reverts with "ValueDistributor: amount has to be more than 0"
staking_.claim(0);

// Claims all the earned rewards
staking.claim(staking.getOwedValue(0x..));

// Reverts with "ValueDistributor: insufficient amount"
staking.claim(staking.getOwedValue(0x..) + 1);

// Unstakes all the user's staked tokens
// Claims all the rewards earned by user
staking_.withdraw();