DeFi staking protocols appear simple from the outside: deposit tokens, wait, and receive rewards.
The difficult part is making the accounting behave correctly when multiple users are staking, withdrawing, claiming rewards, changing reward rates, and interacting with a reward pool that contains a finite number of tokens.
I built StakeVault to explore these problems in a complete Web3 application.
The protocol combines Solidity smart contracts, Foundry testing, and a React frontend deployed on Ethereum Sepolia.
The Core Problem
A staking protocol has to answer several questions:
- How much has each user deposited?
- How much has the entire protocol received?
- How long has each position been active?
- How should rewards be distributed between users?
- What happens when the reward pool runs out?
- How can the frontend represent global protocol state separately from individual wallet state?
StakeVault addresses these through separate user accounting and global accounting.
Global and Individual Staking State
Consider a protocol where User A deposits 410 STAKE.
The global protocol state becomes:
Total Staked = 410 STAKE
User A’s individual state is also:
Staked Amount = 410 STAKE
Now User B deposits 10 STAKE.
The result is:
Total Staked = 420 STAKE
User A = 410 STAKE
User B = 10 STAKE
This distinction is important when building the frontend.
The global totalStaked value represents the entire staking pool. It does not represent the connected wallet.
Reward Accounting
StakeVault uses cumulative reward-per-token accounting.
Conceptually:
rewardPerToken =
rewardPerTokenStored
+ (newRewards × PRECISION / totalStaked)
A user’s earned rewards are then calculated according to their stake and the change in cumulative rewards per token.
This allows the protocol to support multiple users without maintaining an independent reward calculation process for every block.
The Finite Reward Pool
The most important design decision in StakeVault is that rewards are finite.
A simplified staking protocol might calculate:
reward = elapsedTime × rewardRate
without considering whether the vault actually possesses enough REWARD tokens.
That can create a mismatch between the rewards users are promised and the tokens that can actually be distributed.
StakeVault introduces:
rewardRemaining
The lifecycle is:
Fund reward pool
↓
rewardRemaining
↓
Rewards emitted
↓
Users accrue rewards
↓
Rewards claimed
↓
Pool decreases
↓
Pool reaches zero
↓
Emissions stop
Additional REWARD tokens can then be funded into the vault.
Smart Contract Architecture
The system contains three primary contracts.
StakeToken: The ERC-20 token deposited by users.
RewardToken: The ERC-20 token distributed as staking rewards.
StakingVault: The protocol’s core contract.
It manages:
- User staking positions
- Total protocol stake
- Reward calculations
- Reward distribution
- Withdrawals
- Reward claims
- Reward funding
- Reward rate configuration
- Emergency pause functionality
Token Approval
ERC-20 staking requires an approval step before the vault can transfer tokens from the user.
The frontend therefore follows:
User enters amount
↓
Check STAKE balance
↓
Check allowance
↓
Approve if necessary
↓
Call stake()
The balance check is particularly important for user experience.
If a wallet contains zero STAKE, there is no reason to request an approval transaction for a staking amount the user cannot actually provide.
Frontend Architecture
The frontend is built with:
- React
- Vite
- Ethers.js
- Wagmi
It provides wallet connection and exposes the most important protocol state.
Users can see:
Total Staked
Reward Rate
Remaining Rewards
alongside their own:
Available STAKE
Staked Amount
Available REWARD
Accrued Rewards
This makes the distinction between protocol-level and wallet-level state visible directly in the UI.
Testing with Foundry
The smart contracts were tested using Foundry.
The current test suite contains 80 passing tests covering:
- ERC-20 behavior
- Staking
- Multiple staking operations
- Withdrawals
- Reward accrual
- Proportional distribution
- Multiple-user calculations
- Reward claiming
- Exit functionality
- Reward funding
- Reward rate updates
- Pause functionality
- Reward pool exhaustion
- Accounting invariants
- Edge cases
Tests can be executed with:
forge test
Deployment
The protocol is deployed on Ethereum Sepolia.
Chain ID: 11155111
Contracts:
StakeToken
0x6De64f2A0D3C1c93cECB67eF06D4a22ff6e00999
RewardToken
0x1D1A21d7468040b306d06E3f1c6c30eeD249F380
StakingVault
0x9A8a5DdEC15F4B8822BF10E849B5C465D6cf2C1e
The live application is available here.
Source code here.
Project hash:
4ba73c3497f46ea64b0dd4e49ad8d02653085e82
What This Project Demonstrates
StakeVault was designed as more than a staking contract.
It demonstrates how the following pieces fit together:
ERC-20 Tokens
↓
Smart Contract Accounting
↓
Reward Distribution
↓
Foundry Testing
↓
Wallet Integration
↓
React Frontend
↓
Ethereum Sepolia
The project also highlights an important principle in DeFi development: protocol accounting and frontend presentation must distinguish global state from user-specific state.
That distinction becomes especially important when multiple wallets interact with the same staking pool.
Screenshots of the Project in Action
StakeVault Dashboard

Your Staking Position

Staking Interface
1. Enter Staking Amount

2. Token Approval Signature
After clicking Approve & Stake, your wallet should first request approval for the vault to spend the STAKE tokens.

3. Approval Confirmed
After confirming the approval transaction, there should then be a second transaction for:

Dashboard before staking

Dashboard after staking

Rewards Interface

Wallet Connection

Sepolia transaction on Etherscan

Insufficient STAKE Balance Protection

StakeVault was built as part of the EtherAuthority Web3 training/internship program.