Developing strategy vaults
Yearn Finance lost $11 million in 2021 in an attack on DAI vault strategy. Attacker used flash loan to manipulate Curve pool price, vault executed harvest at wrong moment and locked in loss instead of profit. This is not theoretical vulnerability — this is production incident that changed how strategy vaults are built.
Strategy vault is an aggregator contract that accepts user deposits (ERC-4626 standard), deploys capital to one or multiple DeFi strategies, automatically reinvests returns and allows switching strategies without user involvement. Complexity is in correctly managing strategy lifecycle and protecting harvest from manipulation.
Strategy vault architecture
ERC-4626 as base standard
ERC-4626 (Tokenized Vault Standard) standardizes interface: deposit, withdraw, mint, redeem, previewDeposit, previewWithdraw, totalAssets. This lets aggregators (Yearn, Beefy, automatic rebalancers) work with vault without custom integration.
Critical detail of ERC-4626: share price manipulation on first deposit. If vault is empty, attacker deposits tiny amount (1 wei), then donation attack sends large sum directly to vault (bypassing deposit). Share price spikes, next depositor gets 0 shares due to rounding. Protection via virtual shares (OpenZeppelin ERC-4626 adds _decimalsOffset()):
function _convertToShares(uint256 assets, Math.Rounding rounding)
internal view virtual override returns (uint256)
{
return assets.mulDiv(
totalSupply() + 10 ** _decimalsOffset(),
totalAssets() + 1,
rounding
);
}
10x decimals offset makes donation attack economically unfeasible.
Pluggable strategy pattern
Architecture core — separation of vault (holds capital, manages share token) and strategy (deploys capital, generates returns). Vault keeps list of approved strategies with allocation weights.
interface IStrategy {
function asset() external view returns (address);
function vault() external view returns (address);
function totalAssets() external view returns (uint256);
function harvest() external returns (uint256 profit, uint256 loss);
function withdraw(uint256 amount) external returns (uint256 withdrawn);
function emergencyExit() external;
}
Each strategy is separate contract. Adding new strategy doesn't require vault upgrade. This is critical for extensibility and reduces audit risk of new strategies (smaller scope).
Allocation management and automatic switching
Vault stores debtRatio for each strategy — percentage of total assets strategy should hold. On harvest controller checks:
- If strategy holds less than target — vault gives more capital
- If strategy holds more than target — vault withdraws excess
- If strategy in loss — vault reduces debtRatio, increases for more profitable ones
Automatic switching built on two mechanics:
- APY comparison: The Graph subgraph or Chainlink data feeds for APY of different protocols
- Risk-adjusted scoring: APY / (volatility × risk_factor) — not always best strategy is one with maximum yield
Protecting harvest from manipulation
This is the most technically complex place. Yearn incident showed: if harvest executes at moment of anomalous prices in pool — vault locks in losses.
Solutions:
TWAP check on harvest: Before locking profit, vault compares current asset price with TWAP. If deviation >X% — harvest delayed.
Harvest as privileged operation: harvest() called not by anyone, but by keeper (EOA or Gelato/Keep3r automation) — with additional anomaly checks.
Slippage control during strategy swap: Strategy selling reward tokens for base asset (compound strategy) should check minAmountOut via Uniswap v3 quoter.
function _sellRewards(uint256 rewardAmount) internal returns (uint256 baseReceived) {
uint256 expectedOut = quoter.quoteExactInputSingle(
REWARD_TOKEN, BASE_ASSET, POOL_FEE, rewardAmount, 0
);
// Don't sell if price anomalously bad (>2% from quote)
uint256 minOut = expectedOut * 9800 / 10000;
baseReceived = router.exactInputSingle(
ISwapRouter.ExactInputSingleParams({
tokenIn: REWARD_TOKEN,
tokenOut: BASE_ASSET,
fee: POOL_FEE,
recipient: address(this),
amountIn: rewardAmount,
amountOutMinimum: minOut,
sqrtPriceLimitX96: 0
})
);
}
Risk controls
Emergency exit — function available to guardian multisig, withdraws all capital from active strategies and returns to vault idle. Used when vulnerability or market anomaly discovered. Strategy enters emergency mode and blocks new deposits.
Debt limit per strategy: Vault never deploys >X% of capital to one strategy, even if debtRatio set higher. Hard cap at contract level.
Withdrawal queue: On large withdrawal, vault first withdraws from idle balance, then from least profitable strategies, then from main ones. Minimizes disruption for other depositors.
Development stack
- Solidity 0.8.x + OpenZeppelin 5.x — core vault and ERC-4626
- Foundry — tests, fork-tests on mainnet (Ethereum, Arbitrum, Polygon)
- Echidna — invariant testing: total assets ≥ total debt, share price monotonically grows
- The Graph — indexing APY, deposit/withdraw events for frontend
- Gelato Network / Keep3r — automated keeper calls for harvest
- Tenderly — monitoring and alerts on anomalies
Working process
Analytics (3-5 days). Determine target protocols for strategies (Aave, Compound, Curve, Balancer), vault base asset, acceptable risk levels, fee structure (management fee, performance fee).
Architecture (3-5 days). Storage layout vault + strategy interfaces + governance mechanism for adding strategies.
Development (4-8 weeks). Vault core + 2-3 initial strategies + keeper automation. Priority — first complete cycle of one simple strategy (Aave deposit), then complexity.
Testing (1-2 weeks). Fork-tests, invariant testing, attack simulation (donation attack, harvest manipulation, flash loan).
Audit + deployment. For vault with real TVL — external audit mandatory.
Timeline estimates
Basic vault with one strategy and ERC-4626 — 2-3 weeks. Full multi-strategy system with automatic switching and keeper — 2-3 months. Cost calculated individually.







