Staking yield programs are a core mechanism in decentralized finance (DeFi) and Web3, allowing users to lock their tokens in a smart contract to earn rewards. These rewards typically come from protocol fees, token inflation, or external revenue streams. A simple staking contract tracks user deposits, calculates accrued rewards based on a predefined rate, and enables secure withdrawals. Understanding this foundational structure is essential before exploring more complex models like liquidity mining or veTokenomics.
Setting Up Simple Staking Yield Programs
Setting Up Simple Staking Yield Programs
This guide explains how to build and deploy a basic on-chain staking smart contract that distributes yield to token holders.
The core logic involves two key mappings and a reward calculation. First, a mapping like mapping(address => uint256) public stakedBalances tracks each user's deposit. Second, a uint256 public rewardRate defines the yield per token per second. Rewards are calculated based on the time elapsed since the user's last claim, using the formula: pendingRewards = stakedBalances[user] * rewardRate * (block.timestamp - lastClaimTime[user]). This block.timestamp-based approach is simple but requires careful management of reward funding.
A basic Solidity implementation includes functions for stake, withdraw, and claimRewards. Critical security considerations include using the Checks-Effects-Interactions pattern to prevent reentrancy attacks and ensuring the contract holds sufficient reward token balance. For example, the stake function should update the user's balance before transferring tokens (effect before interaction). Always use established libraries like OpenZeppelin's SafeERC20 for token transfers and ReentrancyGuard for protection.
To deploy and test, use a development framework like Hardhat or Foundry. You'll need a mock ERC-20 token for both the staking and reward assets. Key tests should verify: accurate reward calculation over time, multiple users staking concurrently, and that rewards cannot be claimed on unstaked tokens. Tools like Chainlink's VRF can be integrated later for verifiable random number generation if your program includes lottery-style rewards, but start with a deterministic rate.
Once deployed, you must fund the staking contract with the reward token. The sustainability of the program depends on the emission rate and the treasury's ability to replenish rewards. For transparency, consider emitting events for all key actions (Staked, Withdrawn, RewardPaid). This basic contract serves as the foundation for more advanced features like tiered APY, time-locks, or governance-weighted rewards as seen in protocols like Curve Finance or Synthetix.
Prerequisites and Setup
Before deploying a staking contract, you need a configured development environment and a fundamental understanding of the core smart contract concepts involved.
A functional development environment is the first prerequisite. You will need Node.js (v18 or later) and npm or yarn installed. The most common framework for smart contract development is Hardhat or Foundry. For this guide, we'll use Hardhat. Initialize a new project with npx hardhat init and select the TypeScript template. Essential dependencies include @openzeppelin/contracts for secure, audited base contracts and dotenv for managing private keys. A basic hardhat.config.ts file must be configured with a network like Sepolia for testing, requiring an RPC URL from a provider like Alchemy or Infura and a funded wallet's private key stored in a .env file.
Understanding the core contract architecture is crucial. A simple staking program typically involves two main contracts: a staking token (often an ERC-20) and the staking contract itself. The staking contract holds user deposits, tracks stakes with a mapping (e.g., mapping(address => uint256) public stakes), and distributes rewards. Rewards can be calculated using a time-weighted method, where yield accrues per second based on a fixed APR, or a reward token distribution model. You must decide on key parameters: the staking token address, reward rate (e.g., 10% APR), and reward distribution token if separate from the stake token.
Security and testing are non-negotiable prerequisites. Always use OpenZeppelin's libraries for Ownable access control and ReentrancyGuard to prevent recursive withdrawal attacks. Write comprehensive tests in a file like Staking.test.ts. Test critical functions: stake(), withdraw(), claimRewards(), and getUserReward(). Use Hardhat's loadFixture for test setup and simulate time warping with time.increase() to test reward accrual over days or weeks. Before any mainnet deployment, audit the contract logic for edge cases like zero-value transactions, reward calculation rounding errors, and proper event emission.
Finally, prepare for deployment and interaction. Compile contracts with npx hardhat compile. Deploy to a testnet first using a script: npx hardhat run scripts/deploy.ts --network sepolia. This script should deploy the staking token (if needed) and then the staking contract, passing the token address and reward parameters to the constructor. After deployment, you will interact with the contract using Ethers.js. You'll need the contract's ABI and address to write scripts that call stake() or withdraw(), or to integrate a frontend. Always verify your contract source code on block explorers like Etherscan to build user trust.
Setting Up Simple Staking Yield Programs
A guide to building a foundational ERC-20 staking contract with yield distribution, covering core architecture, security considerations, and gas optimization.
A basic staking contract allows users to deposit an ERC-20 token to earn rewards, typically in a different token. The core architecture revolves around three primary state variables: a mapping to track user stakes (balances), a total staked amount (totalStaked), and a reward-per-token accumulator (rewardPerTokenStored). This accumulator is the key to fair, pro-rata reward distribution, ensuring users earn rewards based on their share of the total stake and the duration of their stake, regardless of when they claim.
The contract must implement two main user-facing functions: stake(uint256 amount) and withdraw(uint256 amount). Before any state change in these functions, it is critical to call an internal updateReward(address account) function. This function calculates the accrued rewards for the user since their last interaction by comparing the current global rewardPerTokenStored to the value stored per-user (userRewardPerTokenPaid), and credits it to their rewards balance. This "pull" payment pattern defers the gas cost of reward transfers to a separate claimRewards() function.
Reward distribution is typically managed by a privileged owner role via a notifyRewardAmount(uint256 reward) function. This function transfers the reward tokens into the contract and calculates a new reward rate, often over a defined duration (e.g., 7 days). A common vulnerability here is the "reward dilution" attack, where a large new reward is notified immediately after a deposit, giving the depositor a disproportionate share. Mitigations include locking rewards for a period or basing rates on the existing staked balance.
Security is paramount. Use OpenZeppelin's ReentrancyGuard for the stake, withdraw, and claim functions. Employ Checks-Effects-Interactions pattern: validate inputs, update contract state, and only then make external calls (like token transfers). For the staked token, use safeTransferFrom; for reward distribution, consider using transfer and handling potential failures gracefully, as some tokens (like USDT) do not return a boolean on mainnet.
Gas optimization significantly impacts user experience. Key techniques include using a single storage slot for a user's stake and reward data via a struct, minimizing on-chain calculations by performing math off-chain where possible (e.g., in the frontend), and allowing users to exit() by combining withdraw and claim in one transaction. Always test with forked mainnet simulations using tools like Foundry or Hardhat to estimate real gas costs and identify edge cases.
For production, consider extending this base architecture. Implement a timelock or multi-sig for the notifyRewardAmount function. Add emergency functions to pause staking or migrate funds. Explore more complex models like veTokenomics (vote-escrow) or integrating with liquidity pool (LP) tokens for DeFi-native yield. The complete code for a basic contract is available in the OpenZeppelin Staking Examples repository.
Key Concepts for Staking Logic
Core technical components for building secure and efficient staking yield programs, from reward calculation to slashing conditions.
Step-by-Step Implementation
A practical guide to building and deploying a secure, basic staking contract on Ethereum using Solidity and Foundry.
This guide implements a simple ERC-20 staking contract where users can deposit a specified token to earn rewards. The core logic involves tracking user stakes, calculating rewards based on time staked and a fixed annual percentage yield (APY), and allowing secure withdrawals. We'll use Solidity 0.8.20 for the smart contract and Foundry for development and testing, as it provides a fast, local environment. The contract will have three main functions: stake(uint256 amount), withdraw(uint256 amount), and claimRewards(). Security considerations like reentrancy guards and proper access control are essential from the start.
First, set up the development environment. Install Foundry by running curl -L https://foundry.paradigm.xyz | bash and then foundryup. Create a new project with forge init simple-staking. In the src/ directory, create SimpleStaking.sol. The contract needs to import OpenZeppelin's IERC20.sol, ReentrancyGuard.sol, and Ownable.sol for security and standard interfaces. Define state variables: IERC20 public immutable stakingToken; and IERC20 public immutable rewardsToken; (they can be the same), a uint256 public rewardRate; (e.g., representing 10% APY), and a mapping mapping(address => Stake) public userStakes; where Stake is a struct holding amount and lastUpdateTime.
The stake function should transfer tokens from the user to the contract using stakingToken.transferFrom(msg.sender, address(this), amount); and update the user's stake record. Crucially, you must calculate and credit any pending rewards before updating the stake amount or timestamp to prevent reward loss. Implement an internal _updateReward(address user) function that calculates the time elapsed since the user's lastUpdateTime, multiplies it by their staked amount and the rewardRate, and adds it to a pending rewards balance. This function should be called at the beginning of stake, withdraw, and claimRewards.
For the withdraw function, after updating rewards, check that the user's staked amount is sufficient, then reduce their stake and safely transfer the tokens back using stakingToken.transfer(msg.sender, amount);. Use the nonReentrant modifier from ReentrancyGuard on both stake and withdraw to prevent reentrancy attacks. The claimRewards function calls _updateReward, transfers the accumulated reward tokens to the user, and resets their pending reward balance to zero. Always use the Checks-Effects-Interactions pattern: validate conditions, update state variables, and then make external calls.
Testing is critical. In the test/ directory, create SimpleStaking.t.sol. Use Foundry's cheatcodes to simulate users and time warps. Write tests for: successful staking and reward accrual, failed withdrawals with insufficient balance, reward calculation accuracy after forge.warp() to advance time, and reentrancy attack attempts. Run tests with forge test -vv. For deployment, create a script in script/SimpleStaking.s.sol to broadcast a transaction deploying the contract and initializing it with the token addresses and reward rate. You can deploy to a testnet like Sepolia using forge script script/SimpleStaking.s.sol --rpc-url $SEPOLIA_RPC --private-key $PK --broadcast.
After deployment, consider next steps for a production system. This basic contract lacks features like a dynamic reward pool that needs replenishing, slashing for validators, or delegation. For more complex programs, look at established staking codebases like Synthetix's StakingRewards.sol or Compound's Comptroller. Always get an audit before deploying to mainnet. The full code for this guide is available on the Chainscore Labs GitHub.
Reward Calculation Models
Comparison of common reward distribution models for on-chain staking programs.
| Model | Fixed APR | Rebasing | Reward Token Distribution |
|---|---|---|---|
Mechanism | Static interest rate on principal | Token supply adjusts to reflect yield | Separate reward token sent periodically |
User Experience | Balance appears static, rewards visible separately | Staked token balance increases automatically | Requires manual claim of separate token |
Implementation Complexity | Low (requires reward tracking logic) | Medium (requires rebase logic & index tracking) | Medium (requires reward token & distribution scheduler) |
Gas Cost for Claiming | Medium (claim function call) | None (rewards auto-compound) | High (ERC-20 transfer on claim) |
Example Protocols | Lido (stETH rewards), Aave (aTokens) | Compound (cTokens), Olympus (sOHM) | Curve (CRV rewards), Synthetix (SNX rewards) |
Suitable For | Simple programs, predictable yields | Auto-compounding, DeFi integrations | Bootstrapping new token ecosystems |
Inflation Control | Predictable, set by program | Directly inflates staked token supply | Controlled by separate reward token emission |
Common Security Risks and Mitigations
Key vulnerabilities and defensive strategies for developers building on-chain staking and yield distribution contracts.
Troubleshooting and Testing
Common issues and solutions for developers building and testing staking smart contracts, from deployment errors to reward calculation logic.
Incorrect reward calculations typically stem from logic errors in the reward distribution function or inaccurate time tracking.
Common causes:
- Timestamp manipulation: Using
block.timestampfor duration without a secure start time can be manipulated by miners. Use a fixedstartTimevariable set at deployment. - Reward per second/token precision: Performing division before multiplication can lead to rounding errors, especially with small reward rates. Always multiply rewards by the staked amount before dividing by total supply or time denominator.
- Accumulated rewards not resetting: Ensure a user's pending rewards are minted or transferred before updating their staked balance in functions like
withdraw()orstake(), otherwise the rewards are lost.
Example fix for precision:
solidity// Bad: Division first, precision loss uint256 reward = (timeStaked * rewardRate) / PRECISION / totalSupply * userStake; // Good: Multiplication first uint256 reward = (timeStaked * rewardRate * userStake) / (PRECISION * totalSupply);
Advanced Considerations and Next Steps
After implementing a basic staking contract, several critical considerations remain before deploying to a mainnet. This section covers security, upgradeability, and advanced reward mechanisms.
Security is the paramount concern for any staking contract holding user funds. Beyond standard testing, you must conduct a professional audit. Firms like OpenZeppelin and Trail of Bits specialize in smart contract reviews. Key vulnerabilities to mitigate include reentrancy attacks, integer overflows/underflows (use Solidity 0.8.x's built-in checks), and front-running on critical functions like claimRewards. Implement access controls using libraries like OpenZeppelin's Ownable or role-based AccessControl for administrative functions.
To future-proof your program, consider contract upgradeability. A common pattern is the Transparent Proxy, where user interactions go through a proxy contract pointing to a logic contract. This allows you to deploy a new logic contract and update the proxy's pointer, preserving user stakes and token balances. However, upgradeability adds complexity; you must carefully manage storage layout compatibility. The OpenZeppelin Upgrades Plugins help manage this process securely for frameworks like Hardhat and Foundry.
For more sophisticated reward distribution, explore time-based vesting or multi-token rewards. Instead of instant claims, you can lock rewards in a vesting contract that releases tokens linearly over time. To distribute multiple reward tokens (e.g., both a protocol token and a stablecoin), you can adapt the staking contract to track and distribute from separate reward pools. The StakingRewards.sol contract from Synthetix is a canonical reference for a flexible, audited single-reward staking implementation.
Finally, monitoring and analytics are essential for maintenance. Use off-chain indexers or subgraphs (e.g., The Graph) to track key metrics: total value locked (TVL), average reward rate, number of stakers, and contract events. Set up alerts for unusual activity. For governance, you may integrate with a DAO framework like OpenZeppelin Governor to let token holders vote on parameter changes such as the reward emission rate or fee structures.
Frequently Asked Questions
Common technical questions and solutions for developers implementing on-chain staking contracts.
The two primary architectural patterns are single-asset staking and liquidity pool (LP) token staking.
Single-Asset Staking involves users depositing a single token (e.g., a project's native token) to earn rewards in the same or another token. This is common for governance or inflationary reward models. The contract tracks user shares, often using a rewardPerTokenStored and userRewardPerTokenPaid pattern to calculate accrued rewards efficiently.
LP Token Staking requires users to deposit an LP token from an AMM like Uniswap V2/V3. This incentivizes liquidity provision. The contract must be compatible with the LP token's interface and handle the accounting for the underlying pair. Security is critical, as the staking contract holds significant liquidity.
Development Resources and Tools
Resources for developers implementing simple staking yield programs on EVM-compatible chains. These cards focus on audited building blocks, concrete contract patterns, and testing workflows that reduce risk when deploying token-based staking systems.