paint-brush
How to Implement a Stake and Reward Contract in Solidityby@alcueca
8,915 reads
8,915 reads

How to Implement a Stake and Reward Contract in Solidity

by Alberto Cuesta Cañada July 19th, 2023
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

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. In this article, I’ll show you how many the staking contracts out there are built.
featured image - How to Implement a Stake and Reward Contract in Solidity
Alberto Cuesta Cañada  HackerNoon profile picture


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 my first articles, more than four years ago, was about how to implement staking smart contracts. It is one of my most popular articles, and yet it is hopelessly wrong. I’ve had it in my mind to rewrite it for a long time, and you are now finally reading it.


In this article, I’ll show you how many the staking contracts out there are built, using a simple implementation. Then I’ll show two more advanced versions where the rewards are given for holding a given ERC20 token, or for depositing in an ERC4626 tokenized vault.

About Staking

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. Compound and Curve have been very successful at using rewards to drive business, with a whole generation of other blockchain applications developed around their tokenomic designs.


However, it was k06a implementation of a standalone staking contract that has proven to be the most prolific, with hundreds of deployments and variants. Look at any staking contract after Unipool was released, and the chances are that it is a derivation from it.

Simple Rewards Contract

The Unipool staking contract was immensely influential, and k06a is a world-class developer, but for educational purposes I decided to implement the algorithm again, in a clearer way.


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.


Actions in SimpleRewards.sol

The Math

This article from Dan Robinson offers an excellent description of the maths behind the staking contract, as well as a link to the original paper. I’m going to skip most of the mathematical notation and instead explain what it is that they do in easier terms.


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.

Implementation

The implementation should be easy to understand:

Precision

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.

Differences with Unipool

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.

ERC20 Rewards

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 making the staking contract an ERC20 token itself, and embedding the stake/withdraw operations in the mint, burn and transfer functions.


Embedding rewards in an ERC20

Features

  • There is no separate staking token, holding is staking.
  • Users can mint or receive staking tokens to start accruing rewards.
  • When users transfer away or burn their staking tokens, they stop accruing rewards.
  • After a rewards interval has finished, it is possible to start another via governance.


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.

ERC4626 Staking Contract

The tokenized vault standard has been enthusiastically adopted, and rewarding users for depositing assets is a logical addition. This can be done by having an ERC4626 implementation inherit from the ERC20 Staking contract from the previous section.


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.


The only relevant change to make an ERC4626 contract be powered by rewards

Features

  • There is no separate staking token, holding or depositing is staking.
  • Users can deposit, mint or receive staking tokens to start accruing rewards.
  • When users transfer away, redeem or withdraw they stop accruing rewards.
  • After a rewards interval has finished, it is possible to start another via governance.


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.

Using This Code on Mainnet

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.

Conclusion

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 Yield Protocol, the rest was done for fun. This article was written because I believe it will be helpful to some, and that are all the rewards I’m after. Enjoy.