Token staking is a DeFi tool that allows users to be rewarded for keeping their tokens in a contract. It is one of the most important DeFi primitives and the base of thousands of tokenomic models.
One of
In this article, I’ll show you how many the staking contracts out there are built, using a
Token staking is a process that involves holding assets in a contract to support protocol operations such as market making. In exchange, the asset holders are rewarded with tokens which could be of the same type that they deposited, or not.
The concept of rewarding users for providing services in a blockchain protocol is one of the fundamentals of token economies, and has been present since the ICO Boom or even before.
However, it was
The
The Simple Rewards contract allows users to stake
a stakingToken
, and are rewarded with a rewardsToken
, which they must claim
. They can withdraw
their stake at any time, but rewards stop accruing for them. It is a permissionless contract that will distribute rewards during an interval defined on deployment. That is all that it is.
This
The rewards are only distributed during a finite time period, and they are first distributed uniformly through time, and then proportionally to the tokens staked by each holder.
For example, if we want to distribute 1 million reward tokens, and the rewards will be distributed over 10,000 seconds, we will distribute exactly 100 reward tokens per second. If on any given second there are only two stakers, staking one and three tokens each, then the first staker will get 25 reward tokens for that second, and the other staker will get 75 reward tokens.
This being on a blockchain, distributing reward tokens each second would be complex and expensive. Instead we accumulate a counter for the rewards that a staker would have gotten for a single token until the present time, and update this accumulator each time that a transaction happens in the contract.
The formula to update the accumulator on each transaction is the time passed since the last update, times the reward rate defined on creation, divided by the total amount staked at the time of the update.
currentRewardsPerToken = accumulatedRewardsPerToken + elapsed * rate / totalStaked
The rewardsPerToken accumulator tells us how much would a staker get if they would have staked a single token when the interval period started. That is useful, but we want to allow stakers to stake also once the rewards interval has started, and we want to allow them to stake more than once.
To achieve that, we store for each user their rewards at the time of their last transaction, and what the rewardsPerToken accumulator was at the time of their last transaction. From that data, at any point in time, we can calculate their rewards as:
currentUserRewards =
accumulatedUserRewards +
userStake * (currentRewardsPerToken - userRecordedRewardsPerToken)
Each user transaction will update their accumulated rewards and record the current rewardsPerToken for that user. This process allows users to stake and unstake as many times as they wish.
The implementation should be easy to understand:
The rewardsPerToken variable can be very small if the totalStaked is very large in comparison to the rate and the elapsed time since the last update. For this reason, the rewardsPerToken is scaled up with 18 extra decimals when stored.
The code might look different, but the functionality is very similar. I added the functionality to have any staking and rewards token, and any duration for the rewards interval. I removed the functionality of being able to extend the rewards interval. I also removed the functionality that enables claiming rewards and unstaking in a single function call.
A drawback of a standalone staking contract is that you need a separate transaction to stake. Consider the scenario where you want to use staking to incentivize adding liquidity to some contract. The user will add liquidity and get some tokens in the first transaction, then he will need a second transaction to stake the liquidity tokens obtained.
This inconvenience can be solved by using a batching mechanism, but a second drawback is that the staking position is not liquid. If someone stakes their tokens, they cannot use that stake as collateral for borrowing, for example. They can’t trade their stake either.
Both concerns are solved by
This contract has been implemented with reusability in mind, which means that the code is more complex, but also more versatile and efficient. Note that this contract is meant to be inherited from, since it doesn’t have any public methods that result in minting or burning tokens.
The tokenized vault standard has been enthusiastically adopted, and
In my implementation, I copied the solmate ERC4626 implementation and changed the imports and the constructor. I also added two lines to claim all rewards automatically when a user withdraws fully, emulating Unipool’s exit
function.
This could have been a two line contract, if you could change the parent of a contract that you import. I don’t know if there is a way to do that, so I had to resort to copying the code over.
None of the code in the staking repository is hugely innovative. The SimpleRewards.sol contract is a rewrite of Unipool.sol. The ERC20Rewards.sol contract is a very light update on the ERC20Rewards.sol contract that has been in use by Yield for two years now. The ERC4626Rewards.sol contract is the solmate ERC4626 contract, but inheriting from ERC20Rewards.sol.
I’ve unit tested SimpleRewards.sol and ERC20Rewards.sol because without that I wouldn’t know that the refactors work. I haven’t unit tested ERC4626Rewards.sol because it is a simple mash of two contract.
None of this code has been audited in its current form. If you plan to use it in production, please do that. If you are an up-and-coming auditor and want to audit this code, please do. I will upload any audits that I receive, and reply to any issues that are raised.
The Unipool.sol contract enables protocols to incentivize asset allocation. This basic functionality helped thousands of projects on their journey, and is likely to stay as a building block of decentralized applications for years to come.
In this article, we have reimplemented Unipool.sol with an emphasis on clarity. We have provided another two contracts that reward holding tokens or depositing in tokenized vaults.
Some of this code was implemented to enable rewards on