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)