Skip to content

ERC-20 staking vault with streaming rewards, gasless staking via permit and UUPS upgradeable contracts

Notifications You must be signed in to change notification settings

Kazopl/erc20-staking-vault

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

17 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Advanced ERC-20 + Staking Vault

License: MIT Solidity

A production-grade ERC-20 token with EIP-2612 Permit and a Synthetix-style staking vault with streaming rewards. Built with Foundry, featuring comprehensive testing (unit, fuzz, invariant) and UUPS upgradeability.

Architecture

┌─────────────────────────────────────────────────────────────────────────┐
│                           SYSTEM OVERVIEW                               │
└─────────────────────────────────────────────────────────────────────────┘

┌──────────────────┐         ┌──────────────────────────────────────────┐
│                  │         │              StakingVault                │
│    VaultToken    │◄───────►│  ┌────────────────────────────────────┐  │
│    (ERC-20)      │         │  │         Streaming Rewards          │  │
│                  │         │  │  • Time-weighted distribution      │  │
│  • EIP-2612      │  stake  │  │  • Proportional to stake share     │  │
│  • Permit        │─────────►  │  • Continuous accrual              │  │
│  • Burnable      │         │  └────────────────────────────────────┘  │
│  • Capped Supply │◄────────│                                          │
│  • Access Control│ withdraw│  ┌────────────────────────────────────┐  │
│                  │         │  │        Emergency Features          │  │
└────────┬─────────┘         │  │  • Emergency withdraw (penalty)    │  │
         │                   │  │  • Pausable operations             │  │
         │                   │  │  • ERC-20 recovery                 │  │
   ┌─────▼─────┐             │  └────────────────────────────────────┘  │
   │   Users   │             └──────────────────────────────────────────┘
   └───────────┘

┌─────────────────────────────────────────────────────────────────────────┐
│                        UPGRADEABILITY (UUPS)                            │
├─────────────────────────────────────────────────────────────────────────┤
│  ┌─────────────┐      ┌─────────────────────┐      ┌─────────────┐      │
│  │ ERC1967     │ ───► │  Implementation V1  │ ───► │ Impl V2     │      │
│  │ Proxy       │      │  (current logic)    │      │ (upgrade)   │      │
│  └─────────────┘      └─────────────────────┘      └─────────────┘      │
│        │                                                                │
│        └─── Storage preserved across upgrades                           │
└─────────────────────────────────────────────────────────────────────────┘

Features

VaultToken (ERC-20)

  • EIP-2612 Permit: Gasless approvals via signatures
  • Role-Based Access Control: MINTER_ROLE, UPGRADER_ROLE
  • Capped Supply: Configurable maximum supply
  • Burnable: Users can burn their tokens
  • UUPS Upgradeable: Safe upgrade pattern with storage gaps

StakingVault

  • Streaming Rewards: Synthetix-style continuous reward distribution
  • Permit Staking: Stake without separate approval transaction
  • Emergency Withdraw: Exit with penalty, forfeit rewards
  • Pausable: Admin can pause/unpause operations
  • Configurable: Adjustable reward duration and penalty
  • UUPS Upgradeable: Safe upgrade pattern

Installation

Prerequisites

Setup

# Clone the repository
git clone https://github.com/Kazopl/erc20-staking-vault.git
cd erc20-staking-vault

# Install dependencies
forge install OpenZeppelin/[email protected]
forge install OpenZeppelin/[email protected]

# Build
forge build

# Run tests
forge test

Environment Setup

Create a .env file:

# Private key for deployment (without 0x prefix)
PRIVATE_KEY=your_private_key_here

# RPC URLs (get free keys from Alchemy or Infura)
SEPOLIA_RPC_URL=https://eth-sepolia.g.alchemy.com/v2/YOUR_API_KEY

# Etherscan API key for contract verification
ETHERSCAN_API_KEY=your_etherscan_api_key

Testing

# Run all tests
forge test

# Run with verbosity
forge test -vvv

# Run specific test file
forge test --match-path test/unit/VaultToken.t.sol

# Run fuzz tests
forge test --match-path "test/fuzz/*"

# Run invariant tests
forge test --match-path "test/invariant/*"

# Gas report
forge test --gas-report

# Coverage
forge coverage

Test Categories

Category Description Files
Unit Individual function tests test/unit/*.t.sol
Fuzz Property-based testing with random inputs test/fuzz/*.t.sol
Invariant System-wide property verification test/invariant/*.t.sol

Deployment

Deploy to Sepolia

# Load environment variables
source .env

# Deploy all contracts
forge script script/DeployAll.s.sol:DeployAll \
  --rpc-url $SEPOLIA_RPC_URL \
  --private-key $PRIVATE_KEY \
  --broadcast \
  --verify \
  --etherscan-api-key $ETHERSCAN_API_KEY

Post-Deployment Interactions

# Add rewards to vault
VAULT_ADDRESS=0x... TOKEN_ADDRESS=0x... REWARD_AMOUNT=1000 \
forge script script/Interactions.s.sol:AddRewards \
  --rpc-url $SEPOLIA_RPC_URL \
  --private-key $PRIVATE_KEY \
  --broadcast

# Check vault status
VAULT_ADDRESS=0x... forge script script/Interactions.s.sol:CheckStatus \
  --rpc-url $SEPOLIA_RPC_URL

Upgrade Contracts

# Upgrade VaultToken to V2
TOKEN_PROXY=0x... forge script script/Upgrade.s.sol:UpgradeVaultToken \
  --rpc-url $SEPOLIA_RPC_URL \
  --private-key $PRIVATE_KEY \
  --broadcast \
  --verify

Reward Distribution Math

The staking vault uses a time-weighted reward distribution mechanism:

rewardPerToken = rewardPerTokenStored +
    ((lastTimeRewardApplicable - lastUpdateTime) * rewardRate * PRECISION) / totalStaked

earned(user) = (stakedBalance[user] * (rewardPerToken - userRewardPerTokenPaid[user])) / PRECISION
    + rewards[user]

Key Properties

  • Rewards accrue continuously per second
  • Distribution proportional to stake share
  • Late stakers earn from time of stake only
  • Early exit forfeits unclaimed rewards (emergency withdraw)

Security

Threat Model

Threat Mitigation
Reentrancy ReentrancyGuard on all state-changing functions
Flash Loan Attacks Rewards calculated over time, not instantaneous
Admin Misuse Role-based access, multi-sig recommended for mainnet
Reward Drain Reward pool tracked separately, covered by invariants
Upgrade Attacks UPGRADER_ROLE required, timelock recommended
Integer Overflow Solidity 0.8.24 built-in overflow checks
Permit Replay Nonce tracking, domain separator

Access Control Roles

Role VaultToken StakingVault
DEFAULT_ADMIN_ROLE Grant/revoke roles, set max supply Grant/revoke roles, set penalty, recover tokens
MINTER_ROLE Mint new tokens -
UPGRADER_ROLE Upgrade contract Upgrade contract
REWARDS_MANAGER_ROLE - Add rewards, set duration
PAUSER_ROLE - Pause/unpause operations

Invariants Verified

  1. totalStaked == Σ stakedBalance[users]
  2. token.balanceOf(vault) >= totalStaked
  3. totalSupply <= maxSupply
  4. rewardPerToken monotonically non-decreasing
  5. earned(user) >= 0 for all users

Gas Optimization

Function Gas (approx)
stake() ~85,000
withdraw() ~65,000
claimRewards() ~55,000
stakeWithPermit() ~110,000
emergencyWithdraw() ~70,000

Run forge test --gas-report for detailed breakdown.

Project Structure

erc20-staking-vault/
├── src/
│   ├── VaultToken.sol           # ERC-20 with Permit
│   ├── StakingVault.sol         # Main staking contract
│   ├── interfaces/
│   │   ├── IVaultToken.sol
│   │   └── IStakingVault.sol
│   └── upgrades/
│       ├── VaultTokenV2.sol     # V2 with blacklist
│       └── StakingVaultV2.sol   # V2 with lock period
├── test/
│   ├── BaseTest.sol             # Shared test setup
│   ├── unit/
│   │   ├── VaultToken.t.sol
│   │   └── StakingVault.t.sol
│   ├── fuzz/
│   │   ├── VaultTokenFuzz.t.sol
│   │   └── StakingVaultFuzz.t.sol
│   └── invariant/
│       └── StakingVaultInvariant.t.sol
├── script/
│   ├── DeployAll.s.sol          # Full deployment
│   ├── DeployVaultToken.s.sol
│   ├── DeployStakingVault.s.sol
│   ├── Upgrade.s.sol            # Upgrade scripts
│   └── Interactions.s.sol       # Post-deploy interactions
├── foundry.toml
└── README.md

Upgrade Path

The contracts use the UUPS (Universal Upgradeable Proxy Standard) pattern:

V1 Features → V2 Features

VaultToken:
├── Basic ERC-20         → + Blacklist functionality
└── Permit support       → + Per-address transfer restrictions

StakingVault:
├── Streaming rewards    → + Lock period enforcement
└── Emergency withdraw   → + Time-until-unlock tracking

License

MIT License - see LICENSE for details.

Acknowledgments