Difficulty: Intermediate
Yield farming is the practice of deploying crypto assets across DeFi protocols to maximize returns. At its core, yield farming involves providing liquidity or staking tokens in exchange for rewards, typically in the form of additional tokens. The concept exploded in popularity during the 2020 DeFi Summer when Compound launched its COMP token distribution to protocol users, creating the template that thousands of protocols would follow.
The most basic form of yield farming is staking, where users lock tokens in a smart contract and receive rewards proportional to their stake and the duration of staking. A staking contract accumulates rewards (either from token emissions, protocol revenue, or external funding) and distributes them to stakers based on their share of the total staked amount. The rewards-per-token-stored pattern (used by Synthetix and widely adopted) efficiently tracks each user's earned rewards without iterating through all stakers.
Liquidity mining is a specific type of yield farming where protocols distribute governance tokens to users who provide liquidity. For example, a DEX might reward LP token holders with its native governance token, creating a dual yield: trading fees from the AMM pool plus governance token rewards from the staking contract. Users deposit tokens into an AMM, receive LP tokens, stake those LP tokens in a rewards contract, and earn the protocol's token. This stacking of yield sources is what makes yield farming compelling but also complex.
APY (Annual Percentage Yield) calculations in DeFi can be misleading. The advertised APY assumes rewards are continuously compounded, token prices remain stable, and the reward rate does not change as more users enter the farm. In practice, as more capital flows into a farm attracted by high APY, each participant's share of rewards decreases, driving down the effective yield. Additionally, the reward token itself may lose value if farming participants immediately sell their rewards (known as 'farm and dump'), creating downward price pressure that further reduces the real return.
Yield farming risks include impermanent loss (for LP-based farms), smart contract risk (bugs in the staking or reward contracts), rug pulls (malicious team drains the contract), reward token devaluation, and opportunity cost. Sophisticated farmers use yield aggregators like Yearn Finance, which automatically move funds between protocols to optimize returns, compound rewards, and reduce gas costs through batched transactions. Auto-compounding vaults can significantly improve returns compared to manual farming because they reinvest rewards more frequently.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
/// @notice Staking rewards contract based on the Synthetix StakingRewards pattern
contract StakingRewards {
using SafeERC20 for IERC20;
IERC20 public immutable stakingToken; // Token users stake (e.g., LP token)
IERC20 public immutable rewardsToken; // Token distributed as reward
address public owner;
// Reward state
uint256 public rewardRate; // Rewards per second
uint256 public rewardsDuration = 30 days;
uint256 public periodFinish; // When current reward period ends
uint256 public lastUpdateTime;
uint256 public rewardPerTokenStored; // Cumulative rewards per staked token
// Staking state
uint256 public totalStaked;
mapping(address => uint256) public stakedBalance;
mapping(address => uint256) public userRewardPerTokenPaid;
mapping(address => uint256) public rewards;
event Staked(address indexed user, uint256 amount);
event Withdrawn(address indexed user, uint256 amount);
event RewardPaid(address indexed user, uint256 reward);
event RewardAdded(uint256 reward);
modifier updateReward(address account) {
rewardPerTokenStored = rewardPerToken();
lastUpdateTime = lastTimeRewardApplicable();
if (account != address(0)) {
rewards[account] = earned(account);
userRewardPerTokenPaid[account] = rewardPerTokenStored;
}
_;
}
constructor(address _stakingToken, address _rewardsToken) {
stakingToken = IERC20(_stakingToken);
rewardsToken = IERC20(_rewardsToken);
owner = msg.sender;
}
// === VIEW FUNCTIONS ===
function lastTimeRewardApplicable() public view returns (uint256) {
return block.timestamp < periodFinish ? block.timestamp : periodFinish;
}
/// @notice Cumulative reward per staked token
function rewardPerToken() public view returns (uint256) {
if (totalStaked == 0) return rewardPerTokenStored;
return rewardPerTokenStored + (
(lastTimeRewardApplicable() - lastUpdateTime) * rewardRate * 1e18 / totalStaked
);
}
/// @notice How much reward has a user earned so far?
function earned(address account) public view returns (uint256) {
return (
stakedBalance[account] * (rewardPerToken() - userRewardPerTokenPaid[account]) / 1e18
) + rewards[account];
}
// === USER FUNCTIONS ===
function stake(uint256 amount) external updateReward(msg.sender) {
require(amount > 0, "Cannot stake 0");
totalStaked += amount;
stakedBalance[msg.sender] += amount;
stakingToken.safeTransferFrom(msg.sender, address(this), amount);
emit Staked(msg.sender, amount);
}
function withdraw(uint256 amount) external updateReward(msg.sender) {
require(amount > 0, "Cannot withdraw 0");
require(stakedBalance[msg.sender] >= amount, "Insufficient stake");
totalStaked -= amount;
stakedBalance[msg.sender] -= amount;
stakingToken.safeTransfer(msg.sender, amount);
emit Withdrawn(msg.sender, amount);
}
function getReward() external updateReward(msg.sender) {
uint256 reward = rewards[msg.sender];
if (reward > 0) {
rewards[msg.sender] = 0;
rewardsToken.safeTransfer(msg.sender, reward);
emit RewardPaid(msg.sender, reward);
}
}
/// @notice Withdraw stake and claim rewards in one transaction
function exit() external {
withdraw(stakedBalance[msg.sender]);
// Note: updateReward already ran in withdraw
uint256 reward = rewards[msg.sender];
if (reward > 0) {
rewards[msg.sender] = 0;
rewardsToken.safeTransfer(msg.sender, reward);
emit RewardPaid(msg.sender, reward);
}
}
// === OWNER FUNCTIONS ===
/// @notice Fund the contract with rewards for a new period
function notifyRewardAmount(uint256 reward)
external
updateReward(address(0))
{
require(msg.sender == owner, "Not owner");
if (block.timestamp >= periodFinish) {
rewardRate = reward / rewardsDuration;
} else {
uint256 remaining = periodFinish - block.timestamp;
uint256 leftover = remaining * rewardRate;
rewardRate = (reward + leftover) / rewardsDuration;
}
lastUpdateTime = block.timestamp;
periodFinish = block.timestamp + rewardsDuration;
// Ensure the contract has enough reward tokens
uint256 balance = rewardsToken.balanceOf(address(this));
require(
rewardRate <= balance / rewardsDuration,
"Provided reward too high"
);
emit RewardAdded(reward);
}
}
This is the industry-standard staking rewards pattern from Synthetix. The key insight is the rewardPerToken accumulator: instead of tracking rewards per user per second (expensive), it tracks a global cumulative reward-per-token value. Each user's earned rewards are calculated as their stake multiplied by the difference between the current and their last-recorded rewardPerToken value.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
interface IFarm {
function stake(uint256 amount) external;
function withdraw(uint256 amount) external;
function getReward() external;
function earned(address account) external view returns (uint256);
function stakedBalance(address account) external view returns (uint256);
}
interface IDEX {
function swap(address tokenIn, address tokenOut, uint256 amountIn)
external
returns (uint256 amountOut);
}
/// @notice Auto-compounding vault that harvests farm rewards and reinvests
contract AutoCompounder is ERC20 {
IERC20 public immutable stakingToken; // LP token or base token
IERC20 public immutable rewardToken; // Farm reward token
IFarm public immutable farm;
IDEX public immutable dex;
uint256 public constant HARVEST_FEE_BPS = 300; // 3% performance fee
uint256 public constant BPS = 10_000;
address public feeRecipient;
uint256 public lastHarvest;
event Deposited(address indexed user, uint256 assets, uint256 shares);
event Withdrawn(address indexed user, uint256 assets, uint256 shares);
event Harvested(uint256 reward, uint256 reinvested, uint256 fee);
constructor(
address _stakingToken,
address _rewardToken,
address _farm,
address _dex,
address _feeRecipient
) ERC20("Auto-Compound Vault", "acVault") {
stakingToken = IERC20(_stakingToken);
rewardToken = IERC20(_rewardToken);
farm = IFarm(_farm);
dex = IDEX(_dex);
feeRecipient = _feeRecipient;
}
function totalAssets() public view returns (uint256) {
return farm.stakedBalance(address(this));
}
function deposit(uint256 amount) external returns (uint256 shares) {
uint256 total = totalAssets();
uint256 supply = totalSupply();
stakingToken.transferFrom(msg.sender, address(this), amount);
// Stake in the farm
stakingToken.approve(address(farm), amount);
farm.stake(amount);
// Calculate shares
if (supply == 0) {
shares = amount;
} else {
shares = (amount * supply) / total;
}
_mint(msg.sender, shares);
emit Deposited(msg.sender, amount, shares);
}
function withdraw(uint256 shares) external returns (uint256 assets) {
uint256 supply = totalSupply();
assets = (shares * totalAssets()) / supply;
_burn(msg.sender, shares);
// Unstake from farm
farm.withdraw(assets);
stakingToken.transfer(msg.sender, assets);
emit Withdrawn(msg.sender, assets, shares);
}
/// @notice Harvest rewards, swap to staking token, and restake
function harvest() external {
// 1. Claim rewards from farm
farm.getReward();
uint256 rewardBalance = rewardToken.balanceOf(address(this));
if (rewardBalance == 0) return;
// 2. Take performance fee
uint256 fee = (rewardBalance * HARVEST_FEE_BPS) / BPS;
rewardToken.transfer(feeRecipient, fee);
uint256 remaining = rewardBalance - fee;
// 3. Swap rewards to staking token
rewardToken.approve(address(dex), remaining);
uint256 stakingTokenReceived = dex.swap(
address(rewardToken),
address(stakingToken),
remaining
);
// 4. Restake - this increases totalAssets without minting shares,
// effectively distributing yield to all share holders
stakingToken.approve(address(farm), stakingTokenReceived);
farm.stake(stakingTokenReceived);
lastHarvest = block.timestamp;
emit Harvested(rewardBalance, stakingTokenReceived, fee);
}
}
This auto-compounding vault deposits user funds into a farm, periodically harvests reward tokens, swaps them back to the staking token, and restakes. Since new staking tokens are added without minting new shares, each share becomes worth more over time. The harvest() function can be called by anyone (keepers/bots).
Staking, LP Tokens, APY, Rewards