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)