A decentralized staking application built with Solidity and Foundry that allows users to stake ERC20 tokens and earn ETH rewards.
This staking application enables users to:
- Deposit a fixed amount of ERC20 tokens for staking
- Earn ETH rewards after completing a staking period
- Withdraw their staked tokens at any time
- Claim rewards periodically after each staking period completes
The contract follows security best practices including the Checks-Effects-Interactions (CEI) pattern and uses OpenZeppelin's battle-tested contracts for access control.
The application consists of two main contracts:
A simple ERC20 token contract used for staking. Users can mint tokens to participate in the staking program.
Key Features:
- Standard ERC20 implementation
- Public
mint()function for token creation
The main staking contract that manages deposits, withdrawals, and reward distribution.
Key Features:
- Fixed staking amount requirement
- Configurable staking period
- ETH-based rewards
- One active stake per user
- Owner-controlled staking period updates
stakingToken: Address of the ERC20 token used for stakingstakingPeriod: Minimum time (in seconds) tokens must be staked before rewards can be claimedfixedStakingAmount: Required amount of tokens for each stakerewardPerPeriod: ETH reward amount distributed per completed staking perioduserBalance: Mapping of user addresses to their staked token balanceelapsePeriod: Mapping of user addresses to their last reward claim timestamp
Deposits tokens for staking. Requirements:
- Deposit amount must equal
fixedStakingAmount - User must not have an active stake
- User must have approved the contract to spend their tokens
Withdraws all staked tokens back to the user. Resets the user's balance to zero.
Claims ETH rewards after completing a staking period. Requirements:
- User must have an active stake equal to
fixedStakingAmount - Staking period must have elapsed since last claim
- Contract must have sufficient ETH balance
Owner-only function to update the staking period requirement.
Fallback function to receive ETH deposits for reward distribution.
ChangedStakingPeriod(uint256 newStakingPeriod_): Emitted when the staking period is updatedDepositedTokens(address userAddress, uint256 depositAmount_): Emitted when tokens are depositedWithdrawnTokens(address userAddress, uint256 withdrawAmount_): Emitted when tokens are withdrawnEtherSent(uint256 receivedAmount_): Emitted when ETH is received by the contract
- Foundry
- Solidity ^0.8.30
- Clone the repository:
git clone <repository-url>
cd staking-app- Install dependencies:
forge install- Build the contracts:
forge buildRun all tests:
forge testRun tests with verbose output:
forge test -vvvRun a specific test:
forge test --match-test testShouldDepositTokensCorrectlyThe test suite covers:
- ✅ Contract deployment
- ✅ Token deposits (correct and incorrect amounts)
- ✅ Multiple deposit prevention
- ✅ Token withdrawals
- ✅ Reward claiming (with and without sufficient ETH)
- ✅ Staking period updates (owner and non-owner)
- ✅ ETH reception by contract
- ✅ Edge cases and error conditions
// Deploy StakingToken
StakingToken token = new StakingToken("Staking Token", "STK");
// Deploy StakingApp
StakingApp stakingApp = new StakingApp(
address(token),
ownerAddress,
30 days, // staking period
100 ether, // fixed staking amount
5 ether // reward per period
);// 1. Mint tokens
token.mint(100 ether);
// 2. Approve contract to spend tokens
token.approve(address(stakingApp), 100 ether);
// 3. Deposit tokens
stakingApp.depositTokens(100 ether);
// 4. Wait for staking period to complete
// (In production, this happens naturally over time)
// 5. Claim rewards
stakingApp.claimRewards();
// 6. Withdraw tokens (optional)
stakingApp.withDrawTokens();// Owner sends ETH to contract for rewards
(bool success,) = address(stakingApp).call{value: 1000 ether}("");
require(success, "Transfer failed");- CEI Pattern: The
claimRewards()function follows the Checks-Effects-Interactions pattern to prevent reentrancy attacks - Access Control: Uses OpenZeppelin's
Ownablefor secure ownership management - Fixed Amount: Prevents manipulation by requiring a fixed staking amount
- Single Stake: Users can only have one active stake at a time
- Safe Transfers: Uses standard ERC20
transferFromandtransferfunctions
forge fmtforge snapshotanvilDeploy using Foundry scripts:
forge script script/Deploy.s.sol:DeployScript --rpc-url <your_rpc_url> --private-key <your_private_key> --broadcastUNLICENSED
This is a learning project. Feel free to fork and experiment!