// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; /** * SimpleStaking — Educational staking contract for Base Sepolia * * Users stake native ETH, accrue rewards linearly over time, then withdraw * their principal and claim rewards. This is an educational example — do * NOT use in production. Reward funds must be sent to the contract by the * owner via fundRewards(). * * Deploy on Base Sepolia (chainId 84532) with Remix or Foundry. * Constructor arg: rewardRatePerSecondPerWei — a very small integer. * A sensible default for a lab: 1 (1 wei of reward per staked wei per second * scaled by 1e18 — see math below). Start with rewardRate = 1e9 for * visible rewards in short lab sessions. */ contract SimpleStaking { address public owner; uint256 public rewardRate; // reward wei per staked wei per second, scaled 1e18 uint256 public totalStaked; struct StakeInfo { uint256 amount; uint256 rewardDebt; // accumulated rewards not yet claimed uint256 lastUpdate; // last timestamp rewards were accrued } mapping(address => StakeInfo) public stakes; event Staked(address indexed user, uint256 amount); event Unstaked(address indexed user, uint256 amount); event RewardsClaimed(address indexed user, uint256 amount); event Funded(uint256 amount); modifier onlyOwner() { require(msg.sender == owner, "not owner"); _; } constructor(uint256 _rewardRate) { owner = msg.sender; rewardRate = _rewardRate; } function _accrue(address user) internal { StakeInfo storage s = stakes[user]; if (s.amount > 0 && s.lastUpdate > 0) { uint256 elapsed = block.timestamp - s.lastUpdate; s.rewardDebt += (s.amount * rewardRate * elapsed) / 1e18; } s.lastUpdate = block.timestamp; } function pendingRewards(address user) external view returns (uint256) { StakeInfo memory s = stakes[user]; if (s.amount == 0 || s.lastUpdate == 0) return s.rewardDebt; uint256 elapsed = block.timestamp - s.lastUpdate; return s.rewardDebt + (s.amount * rewardRate * elapsed) / 1e18; } function stake() external payable { require(msg.value > 0, "zero stake"); _accrue(msg.sender); stakes[msg.sender].amount += msg.value; totalStaked += msg.value; emit Staked(msg.sender, msg.value); } function unstake(uint256 amount) external { StakeInfo storage s = stakes[msg.sender]; require(s.amount >= amount && amount > 0, "invalid amount"); _accrue(msg.sender); s.amount -= amount; totalStaked -= amount; (bool ok, ) = msg.sender.call{value: amount}(""); require(ok, "transfer failed"); emit Unstaked(msg.sender, amount); } function claimRewards() external { _accrue(msg.sender); uint256 reward = stakes[msg.sender].rewardDebt; require(reward > 0, "no rewards"); require(address(this).balance >= totalStaked + reward, "insufficient reward pool"); stakes[msg.sender].rewardDebt = 0; (bool ok, ) = msg.sender.call{value: reward}(""); require(ok, "transfer failed"); emit RewardsClaimed(msg.sender, reward); } function fundRewards() external payable onlyOwner { emit Funded(msg.value); } function setRewardRate(uint256 _rate) external onlyOwner { rewardRate = _rate; } }