Yield Strategies Integration
This page is the developer reference for integrating with Stake DAO V2 yield strategies (RewardVaults). It covers the essential concepts, a quickstart guide, considerations, core interfaces, examples, deep dives into the internals, and a FAQ.
Essential Concepts
- RewardVault (ERC4626): The user-facing vault that accepts LP tokens and mints 1:1 shares. Shares track the same decimals as the asset.
- Gauge: The external staking contract the vault targets. The Accountant uses gauges to identify which vault and rewards to claim.
- Accountant: Manages main protocol rewards (e.g., CRV, BAL) and handles fee logic. Rewards can be claimed across multiple vaults at once.
- Extra rewards: Additional incentives (e.g., CVX, LDO) tracked per vault and claimed directly from the RewardVault.
- Harvest policy:
HARVESTclaims rewards on every action,CHECKPOINTtracks rewards until a harvest occurs. - Strategy + Allocator + Sidecars: Internal components that route deposits/withdrawals and harvest rewards. Integrators should not call these directly.
Quickstart (TL;DR)
1. Resolve addresses
RewardVaultaddressgaugeaddress (used for main rewards)asset(LP token)Accountantaddress
2. Approve and deposit
Approve the vault to spend the LP token, then call deposit() or mint() (1:1).
3. Claim rewards
- Main rewards:
Accountant.claim()using gauge addresses - Extra rewards:
RewardVault.claim()using reward token list
Integration Considerations
- 1:1 share ratio:
assets == sharesfor all ERC4626 conversions.previewDeposit,previewWithdraw,convertToShares, andconvertToAssetsare identity functions. - Asset never sits in the vault: Deposits are transferred to allocation targets.
IERC20(asset).balanceOf(vault)can be zero. - Harvest policy matters: In
CHECKPOINT, rewards may be claimable only after a harvest. InHARVEST, rewards are immediately pulled. - Main vs extra rewards: Main rewards go through the Accountant (fees apply). Extra rewards are per-vault and fee-free.
- Claim uses gauges:
Accountant.claim()takes gauge addresses, not vault addresses. - Pauses/shutdowns: Protocols can be paused (deposits blocked, withdrawals allowed). A shutdown gauge pushes funds back to the vault before withdrawals.
- Delegated calls are permissioned:
deposit(account, ...)andclaim(account, ...)require a ProtocolController permission (router/aggregator use case).
Core Interfaces
RewardVault (ERC4626 + extra rewards)
interface IRewardVault is IERC4626 {
function deposit(uint256 assets, address receiver, address referrer) external returns (uint256 shares);
function deposit(address account, address receiver, uint256 assets, address referrer) external returns (uint256 shares);
function withdraw(uint256 assets, address receiver, address owner) external returns (uint256 shares);
function claim(address[] calldata tokens, address receiver) external returns (uint256[] memory amounts);
function claim(address account, address[] calldata tokens, address receiver)
external
returns (uint256[] memory amounts);
function getRewardTokens() external view returns (address[] memory);
function earned(address account, address token) external view returns (uint128);
function gauge() external view returns (address);
function ACCOUNTANT() external view returns (IAccountant);
function PROTOCOL_ID() external view returns (bytes4);
}Accountant (main protocol rewards)
interface IAccountant {
function claim(address[] calldata gauges, bytes[] calldata harvestData) external;
function claim(address[] calldata gauges, bytes[] calldata harvestData, address receiver) external;
function claim(address[] calldata gauges, address account, bytes[] calldata harvestData, address receiver) external;
function harvest(address[] calldata gauges, bytes[] calldata harvestData, address receiver) external;
function getPendingRewards(address vault, address account) external view returns (uint256);
function REWARD_TOKEN() external view returns (address);
function SCALING_FACTOR() external view returns (uint128);
}ProtocolController (discovery + permissions)
interface IProtocolController {
function vault(address gauge) external view returns (address);
function asset(address gauge) external view returns (address);
function rewardReceiver(address gauge) external view returns (address);
function accountant(bytes4 protocolId) external view returns (address);
function strategy(bytes4 protocolId) external view returns (address);
function allocator(bytes4 protocolId) external view returns (address);
function allowed(address target, address caller, bytes4 selector) external view returns (bool);
function isPaused(bytes4 protocolId) external view returns (bool);
function isShutdown(address gauge) external view returns (bool);
}Examples
1. Deposit LP tokens (basic)
IERC20(asset).approve(rewardVault, amount);
uint256 shares = IRewardVault(rewardVault).deposit(amount, msg.sender);2. Deposit with referrer + custom receiver
IERC20(asset).approve(rewardVault, amount);
address referrer = 0x1234...;
uint256 shares = IRewardVault(rewardVault).deposit(amount, receiver, referrer);3. Withdraw (standard ERC4626)
uint256 assets = IRewardVault(rewardVault).withdraw(amount, receiver, owner);4. Claim main rewards (no harvest)
address[] memory gauges = new address[](1);
gauges[0] = gauge;
IAccountant(accountant).claim(gauges, new bytes[](0), receiver);5. Harvest + claim in one call
address[] memory gauges = new address[](1);
gauges[0] = gauge;
bytes[] memory harvestData = new bytes[](1);
harvestData[0] = ""; // protocol-specific, often empty
IAccountant(accountant).claim(gauges, harvestData, receiver);6. Claim extra rewards from the vault
address[] memory extraTokens = IRewardVault(rewardVault).getRewardTokens();
uint256[] memory amounts = IRewardVault(rewardVault).claim(extraTokens, receiver);7. Claim both main + extra in the same transaction (contract example)
function claimAll(address rewardVault, address accountant, address gauge, address receiver) external {
address[] memory gauges = new address[](1);
gauges[0] = gauge;
IAccountant(accountant).claim(gauges, new bytes[](0), receiver);
address[] memory extraTokens = IRewardVault(rewardVault).getRewardTokens();
IRewardVault(rewardVault).claim(extraTokens, receiver);
}8. Read the harvest policy (HARVEST vs CHECKPOINT)
IStrategy.HarvestPolicy policy = RewardVault(rewardVault).POLICY();9. Compute full claimable main rewards
getPendingRewards() only returns rewards accrued since the last checkpoint-triggering action. To compute the total:
Accountant.VaultData memory v = accountant.vaults(rewardVault);
Accountant.AccountData memory a = accountant.accounts(rewardVault, user);
uint256 pending = accountant.getPendingRewards(rewardVault, user);
uint256 scaling = accountant.SCALING_FACTOR();
uint256 claimable = pending + (v.integral - a.integral) * a.balance / scaling;10. Discover vaults via ProtocolController
address vault = IProtocolController(controller).vault(gauge);
address asset = IProtocolController(controller).asset(gauge);
address rewardReceiver = IProtocolController(controller).rewardReceiver(gauge);11. Address book (on-chain constants)
import { CurveEthereum } from "@stake-dao/address-book";
address accountant = CurveEthereum.ACCOUNTANT;
address protocolController = CurveEthereum.PROTOCOL_CONTROLLER;Deep Dives
System architecture
Stake DAO V2 separates user entrypoints from protocol-specific logic:
- RewardVault: ERC4626 entrypoint + extra rewards accounting
- Accountant: main rewards + fee logic + cross-vault aggregation
- Strategy: protocol-specific deposit/withdraw/harvest executor
- Allocator: determines where assets should go (locker vs sidecars)
- Sidecars: optional adapters for extra yield sources
- RewardReceiver: forwards extra rewards into the vault
Integrators should call RewardVault and Accountant only.
Reward model: main vs extra rewards
Main rewards (Accountant):
- Single reward token per protocol (e.g., CRV for Curve)
- Distributed via integral accounting (
SCALING_FACTOR = 1e27) - Subject to protocol fee and harvest fee
Extra rewards (RewardVault):
- Stored per vault, each token has its own distribution schedule
- Fee-free
- Claimable via
RewardVault.claim()
Harvest policy semantics
| Policy | When rewards are pulled | Claim timing | Fees |
|---|---|---|---|
HARVEST | Every user action | Immediately claimable | Protocol fee only |
CHECKPOINT | On explicit harvest | Claimable if Accountant holds balance | Protocol + harvest fee reserved |
In CHECKPOINT, reward integrals update only when new rewards exceed MIN_MEANINGFUL_REWARDS (1e18) to avoid precision loss.
Deposit/withdraw flow (vault perspective)
- Vault checkpoints extra rewards for the receiver.
- Allocator returns target addresses + amounts.
- Vault transfers assets to targets.
- Strategy deposits/withdraws into the underlying protocol.
- Accountant checkpoints balances and main rewards.
RewardReceiver (extra rewards)
Extra rewards emitted by gauges are sent to a dedicated RewardReceiver:
- Validates reward tokens against the vault
- Calls
RewardVault.depositRewards()to start a 7-day distribution period - Can forward legacy rewards to older reward receivers if configured
Pauses and shutdowns
- Paused protocol: deposits blocked, withdrawals allowed.
- Shutdown gauge: strategy can be shutdown to move assets back to the vault. Users then withdraw directly from the vault.
Building a new protocol integration (advanced)
If you are adding a new yield source to Stake DAO V2:
- Define a protocol ID (
bytes4ofkeccak256("PROTOCOL")). - Implement a Strategy for deposit/withdraw/harvest.
- Implement an Allocator for target selection.
- Implement Sidecars if additional yield sources are needed.
- Register components and vaults in the ProtocolController.
FAQ
What is the difference between main and extra rewards?
Main rewards are pooled in the Accountant and subject to protocol/harvest fees. Extra rewards are distributed directly by each RewardVault and are fee-free.
Why does the vault not hold the LP token?
Deposits are immediately allocated to the locker/sidecars for yield. The vault only tracks balances and rewards.
How do I know which policy a vault uses?
Read RewardVault.POLICY() and compare against IStrategy.HarvestPolicy.
I called getPendingRewards but claimable is higher. Why?
getPendingRewards(vault, user) only covers rewards accrued since the last checkpoint. Use the integral formula to compute the full claimable amount.
Do I need to harvest before claiming?
Only in CHECKPOINT mode if the Accountant does not yet hold the reward tokens. In HARVEST mode, claim immediately after any vault action.
Can I batch claims across multiple vaults?
Yes. Accountant.claim() accepts arrays of gauges, enabling multi-vault claims in one call.
Are vault shares transferable?
Yes. RewardVault shares are standard ERC20 tokens, and transfers trigger reward checkpoints for both sides.
Are extra rewards auto-compounded?
No. Extra rewards are distributed to users; you can re-deposit manually if you want compounding.
Where can I find vault addresses?
Use the address book, the strategies API endpoint, or the Contract Addresses page filters.