Building a Finite-Reward DeFi Staking Protocol With Solidity and Foundry

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:

  1. How much has each user deposited?
  2. How much has the entire protocol received?
  3. How long has each position been active?
  4. How should rewards be distributed between users?
  5. What happens when the reward pool runs out?
  6. 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

The dashboard displays the global staking statistics, including total protocol stake, reward rate, and remaining rewards. 

Your Staking Position

Users can view their available STAKE balance and the amount currently deposited in the vault. 

Staking Interface

1. Enter Staking Amount

Users can enter a staking amount or use the MAX button to populate their available balance

2. Token Approval Signature

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

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 before staking

Dashboard after staking

Dashboard after staking

Rewards Interface

The rewards section displays the user's available REWARD balance and accrued rewards, with options to claim rewards or exit the staking position.

Wallet Connection

The application provides wallet connection and account management directly from the interface. 

Sepolia transaction on Etherscan

Sepolia transaction on Etherscan

Insufficient STAKE Balance Protection

The frontend validates the user's available STAKE balance before allowing a staking transaction to proceed. When a connected wallet has insufficient STAKE, the Approve & Stake action is disabled, preventing the user from submitting an invalid staking request. 

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

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.