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.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 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 β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- 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
- 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
- Foundry
- Git
# Clone the repository
git clone https://github.com/Kazopl/erc20-staking-vault.git
cd erc20-staking-vault
# Install dependencies
forge install OpenZeppelin/openzeppelin-contracts@v5.0.1
forge install OpenZeppelin/openzeppelin-contracts-upgradeable@v5.0.1
# Build
forge build
# Run tests
forge testCreate 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# 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| 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 |
# 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# 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 VaultToken to V2
TOKEN_PROXY=0x... forge script script/Upgrade.s.sol:UpgradeVaultToken \
--rpc-url $SEPOLIA_RPC_URL \
--private-key $PRIVATE_KEY \
--broadcast \
--verifyThe 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]
- 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)
| 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 |
| 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 |
totalStaked == Ξ£ stakedBalance[users]token.balanceOf(vault) >= totalStakedtotalSupply <= maxSupplyrewardPerTokenmonotonically non-decreasingearned(user) >= 0for all users
| 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.
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
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
MIT License - see LICENSE for details.
- OpenZeppelin Contracts
- Foundry
- Cyfrin - Template patterns and best practices
- Synthetix - Staking reward math inspiration