Skip to content
Contract · DeFi

Staking

Synthetix-style single-asset staking with reward emissions. Stake ERC20 tokens to earn ERC20 rewards.

All templates
DeFi
~2.4M gas
Ethereum
Post-deploy checklist
  1. Confirm stakingToken ≠ rewardsToken unless you intend same-token compounding
  2. Fund the contract with reward tokens before setting a high rate
  3. Align reward decimals with the rate math (off-by-one silently underpays)
  4. Guard notifyRewardAmount / setRewardRate behind Ownable or a role
  5. Verify source and publish emission schedule off-chain
Configure & deploy

ERC20 users deposit. Must already be deployed and verified if possible.

ERC20 paid out as emissions. Transfer balance into the staking contract after deploy.

Use cases
  • Protocol emissions
  • Liquidity mining programs
  • Loyalty rewards
TokenStaking.solsolidity
// SPDX-License-Identifier: MIT
// Compatible with OpenZeppelin Contracts ^5.0 (Wizard / Contracts MCP patterns)
// Compiled with Solidity ^0.8.24 — Ethereum Toolset browser solc
pragma solidity ^0.8.24;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

// Synthetix-style single-asset staking with reward emissions.
contract TokenStaking is Ownable, ReentrancyGuard {
    using SafeERC20 for IERC20;

    IERC20 public immutable stakingToken;
    IERC20 public immutable rewardsToken;

    uint256 public rewardRate; // rewards per second
    uint256 public lastUpdate;
    uint256 public rewardPerTokenStored;

    uint256 private _totalSupply;
    mapping(address => uint256) private _balances;
    mapping(address => uint256) public userRewardPerTokenPaid;
    mapping(address => uint256) public rewards;

    constructor(address initialOwner, IERC20 _staking, IERC20 _rewards)
        Ownable(initialOwner)
    { stakingToken = _staking; rewardsToken = _rewards; }

    modifier updateReward(address account) {
        rewardPerTokenStored = rewardPerToken();
        lastUpdate = block.timestamp;
        if (account != address(0)) {
            rewards[account] = earned(account);
            userRewardPerTokenPaid[account] = rewardPerTokenStored;
        }
        _;
    }

    function rewardPerToken() public view returns (uint256) {
        if (_totalSupply == 0) return rewardPerTokenStored;
        return rewardPerTokenStored + ((block.timestamp - lastUpdate) * rewardRate * 1e18) / _totalSupply;
    }

    function earned(address a) public view returns (uint256) {
        return (_balances[a] * (rewardPerToken() - userRewardPerTokenPaid[a])) / 1e18 + rewards[a];
    }

    function stake(uint256 amount) external nonReentrant updateReward(msg.sender) {
        require(amount > 0, "amount=0");
        _totalSupply += amount;
        _balances[msg.sender] += amount;
        stakingToken.safeTransferFrom(msg.sender, address(this), amount);
    }

    function withdraw(uint256 amount) public nonReentrant updateReward(msg.sender) {
        _totalSupply -= amount;
        _balances[msg.sender] -= amount;
        stakingToken.safeTransfer(msg.sender, amount);
    }

    function getReward() public nonReentrant updateReward(msg.sender) {
        uint256 r = rewards[msg.sender];
        if (r > 0) { rewards[msg.sender] = 0; rewardsToken.safeTransfer(msg.sender, r); }
    }

    function setRewardRate(uint256 _rate) external onlyOwner updateReward(address(0)) { rewardRate = _rate; }
}