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 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 Initialize the project:
forge init day-27-staking-rewards cd day-27-staking-rewards π° 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); } } π¦ 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]; } } π§ͺ 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); } } Run tests:
forge test π‘ 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(); } } Deploy:
forge script script/Deploy.s.sol --rpc-url <RPC_URL> --private-key <PRIVATE_KEY> --broadcast π 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
Top comments (0)