Introduction
Staking is a popular concept in the DeFi world that allows users to lock up their tokens in a smart contract to earn rewards. In this blog, we’ll dive into the mechanics of a basic staking contract written in Solidity, exploring how it functions and why it is beneficial for both developers and users. We’ll also touch on some of the technical details to give you a clearer picture of how staking contracts are implemented.
The Staking Contract
Here’s a look at the core staking contract implemented in Solidity:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.6;
import "./Openzeppelin/IERC20.sol";
import "./Openzeppelin/ReentrancyGuard.sol";
error TransferFailed();
error NeedsMoreThanZero();
contract Staking is ReentrancyGuard {
IERC20 public s_stakingToken;
IERC20 public s_rewardsToken;
// Reward rate per second
uint256 public constant REWARD_RATE = 100;
uint256 public s_lastUpdateTime;
uint256 public s_rewardPerTokenStored;
mapping(address => uint256) public s_userRewardPerTokenPaid; //depends on pool changes
mapping(address => uint256) public s_rewards; //stores the pending rewards for each user.
uint256 private s_totalSupply;
mapping(address => uint256) public s_balances; //tracks the amount of tokens each user has staked.
event Staked(address indexed user, uint256 indexed amount);
event Withdrawn(address indexed user, uint256 indexed amount);
event RewardsClaimed(address indexed user, uint256 indexed amount);
constructor(address stakingToken, address rewardsToken) {
s_stakingToken = IERC20(stakingToken);
s_rewardsToken = IERC20(rewardsToken);
}
function rewardPerToken() public view returns (uint256) {
if (s_totalSupply == 0) {
return s_rewardPerTokenStored;
}
return s_rewardPerTokenStored + (((block.timestamp - s_lastUpdateTime) * REWARD_RATE * 1e18) / s_totalSupply);
}
function earned(address account) public view returns (uint256) {
return ((s_balances[account] * (rewardPerToken() - s_userRewardPerTokenPaid[account])) / 1e18) + s_rewards[account];
}
function stake(uint256 amount) external updateReward(msg.sender) nonReentrant moreThanZero(amount) {
s_totalSupply += amount;
s_balances[msg.sender] += amount;
emit Staked(msg.sender, amount);
bool success = s_stakingToken.transferFrom(msg.sender, address(this), amount);
if (!success) {
revert TransferFailed();
}
}
function withdraw(uint256 amount) external nonReentrant updateReward(msg.sender) {
s_totalSupply -= amount;
s_balances[msg.sender] -= amount;
emit Withdrawn(msg.sender, amount);
bool success = s_stakingToken.transfer(msg.sender, amount);
if (!success) {
revert TransferFailed();
}
}
function claimReward() external nonReentrant updateReward(msg.sender) {
uint256 reward = s_rewards[msg.sender];
s_rewards[msg.sender] = 0;
emit RewardsClaimed(msg.sender, reward);
bool success = s_rewardsToken.transfer(msg.sender, reward);
if (!success) {
revert TransferFailed();
}
}
modifier updateReward(address account) {
s_rewardPerTokenStored = rewardPerToken();
s_lastUpdateTime = block.timestamp;
s_rewards[account] = earned(account);
s_userRewardPerTokenPaid[account] = s_rewardPerTokenStored;
_;
}
modifier moreThanZero(uint256 amount) {
if (amount == 0) {
revert NeedsMoreThanZero();
}
_;
}
function getStaked(address account) public view returns (uint256) {
return s_balances[account];
}
}
Key Features
1. Reward Calculation
The rewardPerToken
function calculates the reward earned per token based on the time elapsed and the total supply of staked tokens. This ensures that rewards are proportional to the time tokens have been staked.
2. Staking Tokens
Users can stake tokens using the stake
function. This function updates the total supply and user balances, and transfers tokens from the user to the contract. It also emits a Staked
event to log the action.
3. Withdrawing Tokens
The withdraw
function allows users to withdraw their staked tokens. It updates the total supply and user balances, then transfers tokens back to the user. An Withdrawn
event is emitted to record the action.
4. Claiming Rewards
Users can claim their accumulated rewards using the claimReward
function. This function transfers the rewards from the contract to the user and emits a RewardsClaimed
event.
Why Stake?
Staking offers several benefits:
- Earn Passive Income: Users earn rewards for locking up their tokens, which can be a source of passive income.
- Support the Network: In some DeFi projects, staking helps support the network or protocol, contributing to its stability and security.
- Participate in Governance: Staking can also provide governance rights, allowing users to participate in decision-making processes.
Technical Insights
Reentrancy Guard: The contract inherits from
ReentrancyGuard
to prevent reentrancy attacks, ensuring that functions that transfer tokens cannot be called recursively.Error Handling: Custom errors (
TransferFailed
andNeedsMoreThanZero
) are used for better gas efficiency and error handling compared to traditionalrequire
statements.Modifiers: The
updateReward
modifier updates reward calculations before executing functions likestake
,withdraw
, andclaimReward
, ensuring that rewards are always accurate.
Conclusion
This staking contract provides a straightforward yet effective way for users to stake their tokens and earn rewards. By understanding the contract’s components and functionality, you can appreciate the role of staking in the DeFi ecosystem and how smart contracts facilitate this process.
Explore the Code
If you’re interested in exploring the code or contributing to its development, check out the GitHub repository where the full contract and additional details are available.
Feel free to experiment with and extend this contract to fit more complex use cases or integrate it into your DeFi projects. Happy coding🚀!
Top comments (1)
Hello,
I hope you're doing well. I am seeking guidance on how to unstake my tokens from a staking contract, as the staking platform is currently offline and the developer appears to have abandoned the project. Unfortunately, there is no active community support, and I have not received any responses to my inquiries.
The contract is located on Arbiscan, but it is not verified and lacks an ABI. Furthermore, it is a proxy contract, which complicates direct interaction with it. There are no "read" or "write" functions available, only bytecode is accessible.
Given these circumstances, I would like to know if there is any way to manually interact with or unstake my tokens directly from the contract. Any advice or insights you can provide would be greatly appreciated.
Thank you for your time and assistance.
Best regards,
Hadi