Contract · DeFi
Staking
Synthetix-style single-asset staking with reward emissions. Stake ERC20 tokens to earn ERC20 rewards.
DeFi
~2.4M gas
Ethereum
Post-deploy checklist
- Confirm stakingToken ≠ rewardsToken unless you intend same-token compounding
- Fund the contract with reward tokens before setting a high rate
- Align reward decimals with the rate math (off-by-one silently underpays)
- Guard notifyRewardAmount / setRewardRate behind Ownable or a role
- 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; }
}
