DEV Community

Cover image for πŸͺ™ Day 27 of #30DaysOfSolidity β€” Build a Staking & Yield Farming Platform in Solidity
Saurav Kumar
Saurav Kumar

Posted on

πŸͺ™ Day 27 of #30DaysOfSolidity β€” Build a Staking & Yield Farming Platform in Solidity

Turn your tokens into a source of passive income πŸ’°
Learn how staking and yield farming work by building your own reward distribution system on Ethereum!


🧭 Overview

In this project, we’ll build a Staking Rewards System β€” where users deposit tokens and earn periodic rewards in another token.

It’s just like a digital savings account that pays interest in tokens β€” demonstrating one of the core mechanisms in DeFi (Decentralized Finance).


🧱 Project Structure

day-27-staking-rewards/ β”œβ”€ foundry.toml β”œβ”€ src/ β”‚ β”œβ”€ MockERC20.sol β”‚ └─ StakingRewards.sol β”œβ”€ script/ β”‚ └─ Deploy.s.sol └─ test/ └─ StakingRewards.t.sol 
Enter fullscreen mode Exit fullscreen mode

We’ll use Foundry for smart contract testing and deployment β€” it’s fast, lightweight, and ideal for local blockchain development.


βš™οΈ Step 1 β€” Setup

Install Foundry:

curl -L https://foundry.paradigm.xyz | bash foundryup 
Enter fullscreen mode Exit fullscreen mode

Initialize the project:

forge init day-27-staking-rewards cd day-27-staking-rewards 
Enter fullscreen mode Exit fullscreen mode

πŸ’° Step 2 β€” Create a Mock Token

We’ll create a mock ERC20 token for both staking and rewards.

// SPDX-License-Identifier: MIT pragma solidity ^0.8.17; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract MockERC20 is ERC20 { constructor(string memory name, string memory symbol, uint256 initialSupply) ERC20(name, symbol) { _mint(msg.sender, initialSupply); } function mint(address to, uint256 amount) external { _mint(to, amount); } } 
Enter fullscreen mode Exit fullscreen mode

🏦 Step 3 β€” Build the StakingRewards Contract

Here’s the core of our yield farming system.

// SPDX-License-Identifier: MIT pragma solidity ^0.8.17; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; contract StakingRewards is Ownable, ReentrancyGuard { using SafeERC20 for IERC20; IERC20 public immutable stakeToken; IERC20 public immutable rewardToken; uint256 public totalSupply; mapping(address => uint256) public balances; uint256 public rewardPerTokenStored; uint256 public lastUpdateTime; uint256 public rewardRate; uint256 private constant PRECISION = 1e18; mapping(address => uint256) public userRewardPerTokenPaid; mapping(address => uint256) public rewards; event Staked(address indexed user, uint256 amount); event Withdrawn(address indexed user, uint256 amount); event RewardPaid(address indexed user, uint256 reward); event RewardRateUpdated(uint256 oldRate, uint256 newRate); constructor(address _stakeToken, address _rewardToken) { stakeToken = IERC20(_stakeToken); rewardToken = IERC20(_rewardToken); lastUpdateTime = block.timestamp; } modifier updateReward(address account) { _updateRewardPerToken(); if (account != address(0)) { rewards[account] = earned(account); userRewardPerTokenPaid[account] = rewardPerTokenStored; } _; } function _updateRewardPerToken() internal { if (totalSupply == 0) { lastUpdateTime = block.timestamp; return; } uint256 time = block.timestamp - lastUpdateTime; uint256 rewardAccrued = time * rewardRate; rewardPerTokenStored += (rewardAccrued * PRECISION) / totalSupply; lastUpdateTime = block.timestamp; } function notifyRewardAmount(uint256 reward, uint256 duration) external onlyOwner updateReward(address(0)) { require(duration > 0, "duration>0"); rewardToken.safeTransferFrom(msg.sender, address(this), reward); uint256 newRate = reward / duration; emit RewardRateUpdated(rewardRate, newRate); rewardRate = newRate; } function stake(uint256 amount) external nonReentrant updateReward(msg.sender) { require(amount > 0, "amount>0"); totalSupply += amount; balances[msg.sender] += amount; stakeToken.safeTransferFrom(msg.sender, address(this), amount); emit Staked(msg.sender, amount); } function withdraw(uint256 amount) public nonReentrant updateReward(msg.sender) { require(amount > 0, "amount>0"); totalSupply -= amount; balances[msg.sender] -= amount; stakeToken.safeTransfer(msg.sender, amount); emit Withdrawn(msg.sender, amount); } function getReward() public nonReentrant updateReward(msg.sender) { uint256 reward = rewards[msg.sender]; if (reward > 0) { rewards[msg.sender] = 0; rewardToken.safeTransfer(msg.sender, reward); emit RewardPaid(msg.sender, reward); } } function exit() external { withdraw(balances[msg.sender]); getReward(); } function earned(address account) public view returns (uint256) { uint256 _balance = balances[account]; uint256 _rewardPerToken = rewardPerTokenStored; if (totalSupply != 0) { uint256 time = block.timestamp - lastUpdateTime; uint256 pending = time * rewardRate; _rewardPerToken += (pending * PRECISION) / totalSupply; } return (_balance * (_rewardPerToken - userRewardPerTokenPaid[account])) / PRECISION + rewards[account]; } } 
Enter fullscreen mode Exit fullscreen mode

πŸ§ͺ Step 4 β€” Write Tests (Foundry)

Here’s a simple test case to verify our staking logic:

// SPDX-License-Identifier: MIT pragma solidity ^0.8.17; import "forge-std/Test.sol"; import "../src/MockERC20.sol"; import "../src/StakingRewards.sol"; contract StakingRewardsTest is Test { MockERC20 stake; MockERC20 reward; StakingRewards staking; address alice = address(0xA11CE); function setUp() public { stake = new MockERC20("Stake Token", "STK", 0); reward = new MockERC20("Reward Token", "RWD", 0); staking = new StakingRewards(address(stake), address(reward)); stake.mint(alice, 1000 ether); reward.mint(address(this), 1000 ether); reward.approve(address(staking), type(uint256).max); } function testStakeAndEarn() public { staking.notifyRewardAmount(100 ether, 100); vm.prank(alice); stake.approve(address(staking), 100 ether); vm.prank(alice); staking.stake(100 ether); vm.warp(block.timestamp + 50); uint256 earned = staking.earned(alice); assertApproxEqRel(earned, 50 ether, 1e16); } } 
Enter fullscreen mode Exit fullscreen mode

Run tests:

forge test 
Enter fullscreen mode Exit fullscreen mode

πŸ’‘ Step 5 β€” Deploy Script

// SPDX-License-Identifier: MIT pragma solidity ^0.8.17; import "forge-std/Script.sol"; import "../src/MockERC20.sol"; import "../src/StakingRewards.sol"; contract DeployScript is Script { function run() public { vm.startBroadcast(); MockERC20 stake = new MockERC20("Stake Token", "STK", 1_000_000 ether); MockERC20 reward = new MockERC20("Reward Token", "RWD", 1_000_000 ether); StakingRewards staking = new StakingRewards(address(stake), address(reward)); vm.stopBroadcast(); } } 
Enter fullscreen mode Exit fullscreen mode

Deploy:

forge script script/Deploy.s.sol --rpc-url <RPC_URL> --private-key <PRIVATE_KEY> --broadcast 
Enter fullscreen mode Exit fullscreen mode

πŸ” Security Tips

βœ… Use nonReentrant modifier
βœ… Validate inputs before transfers
βœ… Keep reward logic owner-only
βœ… Test for edge cases like 0 stake or high reward rates


πŸš€ What You Built

  • A complete staking & yield farming platform
  • Users can stake, earn rewards, and withdraw anytime
  • Reward distribution is fair and time-based
  • Built with Foundry, OpenZeppelin, and best Solidity practices

🧠 Next Challenges

  • Add multiple staking pools
  • Introduce NFT-based reward boosts
  • Build a React or Next.js frontend using Ethers.js
  • Automate reward top-ups with Chainlink Keepers

πŸ”— GitHub Repository

πŸ‘‰ GitHub: Day 27 β€” Staking & Yield Farming (Solidity)

Top comments (0)