A fully on-chain staking rewards system. Built using Solidity and Foundry. Users can stake ERC20 tokens. You earn rewards every second. You can claim rewards anytime. You can withdraw your stake easily.
This project demonstrates portfolio-quality smart contract engineering skills. It focuses on:
- Reward math (accRewardPerShare, rewardDebt).
- Efficient state updates (Synthetix-style).
- Accurate per-second reward accounting.
- Local testing using Foundry.
- Deployment on Sepolia Testnet.
-
Stake ERC20 tokens Users deposit staking tokens into the contract.
-
Earn rewards every second Reward rate is fixed (e.g., 1 token per second).
-
Claim rewards anytime You can claim without unstaking.
-
Partial or full withdraw Withdraw 100% or partially. Rewards are auto-claimed.
-
Safe reward math Uses
accRewardPerShare+rewardDebtlogic. -
Fully tested with Foundry Includes stake, claim, pending reward, and withdraw logic.
StakingSimple.sol
├── stake() // deposit tokens
├── withdraw() // unstake tokens + claim rewards
├── claim() // claim only rewards
├── pendingReward() // view pending rewards
├── updatePool() // update reward accumulator
└── rewardDebt mapping // track user reward position
The contract uses standard DeFi reward calculations. This ensures accurate tracking, no loops, and infinite scalability.
accRewardPerShare += (rewardGenerated * 1e18) / totalStaked;
pending = (userStake * accRewardPerShare) / 1e18 - rewardDebt[user];
Run all tests to verify the system.
forge test -vvv
Test coverage includes:
- Stake increases pool balance.
- Pending rewards increase over time.
- Claim distributes correct reward.
- Withdraw returns stake + reward.
- Reward debt updates correctly.
Deploy Mock Tokens
forge script script/MockERC20.s.sol --broadcast --rpc-url $RPC_URL --private-key $PK
Output example:
- Stake token: 0x3Cd...
- Reward token: 0x41F...
Deploy Staking Contract
forge script script/DeployStaking.s.sol --broadcast --rpc-url $RPC_URL --private-key $PK
Output example:
- Contract: 0x6eB...
Fund Reward Pool
cast send <rewardToken> "transfer(address,uint256)" <stakingContract> 500ether
Approve stake:
cast send <stakeToken> "approve(address,uint256)" <stakingContract> 100ether
Stake tokens:
cast send <stakingContract> "stake(uint256)" 100ether
View pending rewards:
cast call <stakingContract> "pendingReward(address)" <yourAddress>
Claim rewards:
cast send <stakingContract> "claim()"
Withdraw stake:
cast send <stakingContract> "withdraw(uint256)" 50ether
- Solidity 0.8.19
- Foundry (forge, cast, anvil)
- OpenZeppelin ERC20
- Sepolia Testnet