Back to Blog
Yearn vault security: V2 vs V3 architecture, exploits, and defense patterns
AuditDeFiWeb3 Security

Yearn vault security: V2 vs V3 architecture, exploits, and defense patterns

16 min
Yearn Finance has quietly become one of the most studied vault architectures in DeFi. Two live generations now coexist: V2 (Vyper Vault.vy with a Solidity BaseStrategy harness) and V3 (Vyper VaultV3.vy plus the chain-wide Solidity TokenizedStrategy proxy at 0xBB51273D6c746910C7C06fe718f30c936170feD0).
What makes Yearn a useful case study isn't just the code — it's the track record. Every confirmed loss in Yearn's history traces to legacy v1 contracts, external-protocol oracle dependence, or integrators using pricePerShare() as collateral valuation. The V2 and V3 cores have survived without a direct exploit.
This post is a dense, audit-focused reference for teams reviewing Yearn-based protocols or designing similar vault systems.

Zealynx take: Why Yearn's architecture matters for audit clients

If you're building a yield aggregator, an ERC-4626 wrapper, or anything that integrates with Yearn, the lessons baked into V2 → V3 map directly onto your threat model. Multi-strategy debt isolation, locked-profit degradation, and internal balance tracking aren't academic — they're codified answers to real incidents.
At Zealynx, we audit vault stacks, tokenized strategies, and DeFi aggregators inspired by Yearn's design. If your protocol touches strategy-level code that reads pool state, mints to self, or uses delegatecall proxies, the patterns below are a checklist to run against your own repo.

V2 vault mechanics: contract-level precision

Yearn V2 vault architecture
Vault.vy is a monolithic Vyper contract (implements: ERC20, not ERC-4626). Its invariants sit in a small constants block:
  • MAXIMUM_STRATEGIES = 20
  • DEGRADATION_COEFFICIENT = 1e18
  • MAX_BPS = 10_000
  • SECS_PER_YEAR = 31_556_952
StrategyParams holds nine fields per registered strategy: performanceFee, activation, debtRatio, minDebtPerHarvest, maxDebtPerHarvest, lastReport, totalDebt, totalGain, totalLoss. The fixed-length withdrawalQueue: address[20] is iterated on every withdraw call with no per-call override.

Share pricing and the anti-sandwich primitive

Share value uses free funds:
1_shareValue(shares) = shares * (totalAssets - _calculateLockedProfit()) / totalSupply
_calculateLockedProfit() linearly unlocks the last reported gain at lockedProfitDegradation (default ~6 hours). A depositor who enters one block after a profitable report() cannot capture the locked portion. The deposit invariant is deliberately deposit-unfavorable when profit is fresh.

The report lifecycle

report(gain, loss, _debtPayment) is the strategy → vault accounting entry. Order matters:
  1. Loss reduces strategy.debtRatio and vault.debtRatio proportionally
  2. _assessFees mints management, strategist, and governance fees as shares at pre-update PPS
  3. Credit is transferred between vault and strategy
  4. self.lockedProfit = gain - totalFees replaces any residual locked profit
creditAvailable caps flow by min(strategy_debtLimit - strategy_totalDebt, vault_debtLimit - vault_totalDebt, totalIdle), bounded by minDebtPerHarvest and maxDebtPerHarvest.

The maxLoss footgun

withdraw(maxShares, recipient, maxLoss=1) iterates withdrawalQueue, breaks on ZERO_ADDRESS (queue terminator), and calls Strategy.withdraw(amountNeeded) on each. Critically the default maxLoss = 1 bp (0.01%): any strategy with unrealized or near-realized loss ≥ 0.01% causes assert totalLoss <= maxLoss * (value + totalLoss) / MAX_BPS to revert.
Code4rena's Popcorn contest (finding #581) documented this as a silent denial-of-service against integrators during Aave bad-debt episodes. If you're building on top of Yearn, this is the first thing to audit.

V3 modular architecture and the Tokenized Strategy proxy pattern

V3 modular architecture and Tokenized Strategy proxy
VaultV3.vy (Vyper 0.3.7, API_VERSION = "3.0.4") is an ERC-4626-native allocator whose scope shrank dramatically. StrategyParams is only four fields — activation, last_report, current_debt, max_debt — because fees live in an external Accountant, debt allocation in an external Debt Allocator, and per-strategy performance logic inside each Tokenized Strategy.
total_idle is manually tracked ("Replacing balanceOf(this) to avoid price_per_share manipulation") — the explicit anti-donation attack invariant.

The 14-role bitmap

V3 replaces V2's five-address governance with a 14-bit Vyper enum Flag covering roles like ADD_STRATEGY_MANAGER, DEBT_MANAGER, REPORTING_MANAGER, EMERGENCY_MANAGER, and PROFIT_UNLOCK_MANAGER. The role-manager API exposes set_role, add_role, remove_role, plus set_open_role(role) that makes a single role permissionless.
yAudit's June-2023 report explicitly warns: "a mistake, such as opening a role that should remain closed, could potentially result in irreversible and damaging effects on the entire vault." In practice only REPORTING_MANAGER is a defensible candidate for opening.

Profit unlocking via mint-to-self

V3 replaces V2's locked-profit scalar with share-level accounting. On process_report the vault mints shares_to_lock = convertToShares(gain + refunds - fees) to itself, and vests them via:
1_unlocked_shares() = profit_unlocking_rate * (block.timestamp - last_profit_update) / MAX_BPS_EXTENDED
_total_supply() = total_supply - _unlocked_shares(), so PPS increases monotonically over the unlock window. Losses burn locked shares first before touching realized PPS.

The TokenizedStrategy delegatecall pattern

This is the V3 innovation auditors must internalize. A tokenized strategy is itself a fully ERC-4626-compliant vault, deployed directly by a strategist, that delegates all standard plumbing to a single immutable TokenizedStrategy implementation deployed at the same deterministic CREATE2 address on every chain (0xBB51273D6c746910C7C06fe718f30c936170feD0).
Mechanism: BaseStrategy.fallback() delegatecalls to that address. Storage is namespaced under bytes32(uint256(keccak256("yearn.base.strategy.storage")) - 1) — an ERC-7201-adjacent pattern.
The strategist's only job is to implement three callbacks:
  • _deployFunds(uint256)
  • _freeFunds(uint256)
  • _harvestAndReport() → uint256
Optional: _tend, _tendTrigger, availableDepositLimit, availableWithdrawLimit, _emergencyWithdraw.

V3 footguns auditors must catch

From storm0x's "V3 Strategies GOTCHAS":
  1. Selector collisions between strategy-author functions and TokenizedStrategy's ABI silently route the wrong way through the fallback
  2. Inheriting contracts that declare their own ERC-20 / Ownable state (OpenZeppelin bases are notably dangerous) corrupts StrategyData via slot collision
  3. Direct _mint from strategy code breaks the profit-locking invariant
  4. Assembly writes to the namespaced storage slot must be categorically forbidden in review
Action item: run a selector-collision linter against every strategy plus grep for sstore with the reserved slot constant.

Risk surface: what actually breaks vaults

Yearn risk surface map

Smart contract and accounting risks

Reentrancy at the V2 Vault level is blocked by Vyper's keyed @nonreentrant locks; V3 uses a single-byte entered flag in StrategyData on TokenizedStrategy. The live attack surface is cross-contract reentrancy: a strategist override that calls into another tokenized strategy during _deployFunds / _freeFunds can bypass the per-slot guard because each strategy has its own entered flag.
Rounding and double-rounding are the most common audit finding class. yAudit's review caught double-rounding in mint(), off-by-one in _redeem's loss share, and boundary rounding in withdraw(). Recurring lesson: incorrect estimatedTotalAssets / _harvestAndReport is the #1 root cause of silent loss.

Donation and inflation attacks

V2's early versions read token.balanceOf(self) in totalAssets(), making donations inflate PPS — mitigated from API 0.4.4 forward by internal totalIdle tracking. V3 extends this to both the vault and each TokenizedStrategy.
Crucially, V3 does not use OpenZeppelin's virtual-shares-and-decimals-offset approach and does not burn dead shares. Defense is operational: deployment playbook sets deposit_limit = 0 at init, governance seeds a non-trivial first deposit, then opens deposits in a batched multisig transaction.
Permissionless factory vaults break this assumption — anyone can deploy an empty vault, and the first naïve depositor is exposed to the classical 1-wei-deposit-plus-donation skew. See our deeper treatment of this pattern in AMM security foundations.

Oracle and read-only reentrancy

Yearn's core contracts impose no TWAP enforcement; oracle choice is entirely strategy-author discretion. The most dangerous pattern is ICurvePool.get_virtual_price() or ICurvePool.get_dy(i, j, dx) inside _harvestAndReport on pools that transfer raw ETH or ERC-777 tokens during remove_liquidity.
ChainSecurity's April-2022 disclosure established that Curve pool state is transiently inconsistent inside the ETH callback. Yearn's mitigation pattern: a reentrancy "poke" via pool.remove_liquidity(0, [0,0]) immediately before reading get_virtual_price. Our deep dive on Curve Finance core mechanics covers this in detail.
The reusable heuristic: never derive totalAssets from any function whose return value is a function of live pool state readable during a callback.

Governance risk

Yearn's main multisig ychad.eth is a 6-of-9 Gnosis Safe. Under Governance 2.0 power distributes across yTeams. Yearn does not interpose an on-chain Timelock between the multisig and core vault parameters — strategy additions, debt-ratio changes, and registry endorsements all execute atomically on multisig approval.
This is a trust assumption auditors and integrators should surface explicitly. The catastrophic path: compromise six signers → addStrategy(evilStrategy, 10000, 0, MAX, 0)harvest() on existing strategies → evilStrategy._deployFunds exfiltrates. V3's requirement that ADD_STRATEGY_MANAGER plus MAX_DEBT_MANAGER both act mitigates single-role compromise but not full multisig takeover.

Historical exploits: three lessons, three different failure modes

yDAI v1 — February 2021 (~$11M pool loss)

StrategyDAICurve on yDAI v1 deposited into Curve 3pool. Root cause in one sentence: v1's publicly-callable earn() pushed vault balances into Curve using a 1% slippage bound and a 0% withdrawal fee, so a flash-loan-driven 3pool imbalance could sandwich the deposit path.
Attack: flash-loan ~116,000 ETH from dYdX → imbalance Curve 3pool → call earn() on yDAI → reverse the imbalance → redeem yDAI shares. Five iterations. Yearn multisig halted in ~11 minutes, saving 24M of 35M DAI.
Architectural response: V2's whole design — non-public harvest, multi-strategy isolation, locked-profit degradation, totalIdle airdrop protection, emergencyShutdown — is the codified post-mortem.

Cream Finance — October 2021 (130M,but130M, but 0 Yearn loss)

Cream priced yUSD collateral using yUSDVault.pricePerShare(), a share-based oracle manipulable via donation. Attacker donated the underlying crvY LP to the yUSD vault (boosting totalAssets without increasing totalSupply), roughly doubling PPS. Cream instantly marked the collateral up; attacker borrowed ~$130M against the inflated valuation.
Yearn lost $0; the yUSD vault functioned as designed but was used as a composability oracle by another protocol without donation-resistance. Lesson for integrators: never treat pricePerShare as an oracle without TWAP, min/max caps, or donation-resistance.

Get the DeFi Protocol Security Checklist

15 vulnerabilities every DeFi team should check before mainnet. Used by 30+ protocols.

No spam. Unsubscribe anytime.

yUSDT v1 — April 2023 (~11.6Mphantommint, 11.6M phantom-mint, ~1.6M realized)

yUSDT v1 was deployed in 2020 with a misconfigured Fulcrum iUSDC token address where iUSDT was intended. The bug sat dormant for ~3 years because vault TVL was negligible. Attacker used flash-loan capital to interact with the legacy iUSDC, inflating the vault's internal accounting. A small USDT deposit produced disproportionate yUSDT shares.
V2 vaults were unaffected. Lesson: deprecated contracts remain attack surfaces indefinitely if not emptied and paused; config errors can hibernate for years. Relevant if you're running a pre-audit checklist on an older repo.

Defense patterns: what to bake in

Rate limits by exact value

  • V2: performanceFee ≤ MAX_BPS/2 = 5000, sum(strategy.debtRatio) ≤ MAX_BPS, default lockedProfitDegradation ≈ 6-hour unlock
  • V3 TokenizedStrategy: MAX_FEE = 5000, profitMaxUnlockTime ≤ SECONDS_PER_YEAR, default 10 days
  • V3 vault: minimum_total_idle floor, pluggable IDepositLimitModule / IWithdrawLimitModule

maxLoss semantics auditors must memorize

FunctionDefault maxLossBehavior
V2 Vault.withdraw1 bpRevert on exceed
V3 Vault.withdraw0 bpsRevert on any loss
V3 Vault.redeemMAX_BPS = 10_000Accept any loss silently
Integrators calling redeem() with defaults can eat 100% loss without reverting — a silent-loss bug class in vaults-of-vaults wrappers. If you're building a defense-in-depth workflow, this is a must-check.

Emergency mechanisms

V2 setEmergencyShutdown(bool) is callable by governance in either direction and by guardian only for true. V3 shutdown_vault() is EMERGENCY_MANAGER-only and irreversible — coupled with open roles, a compromised EMERGENCY_MANAGER permanently bricks write operations.

Strategy risk scoring

Yearn's Strategy Risk Score is a 1–5 scale across eight dimensions: audit coverage, code review frequency, complexity, protocol safety, team knowledge, testing score, TVL impact, and longevity. Higher-risk strategies are capped at lower debt ratios until they demonstrate in-production safety. The framework explicitly acknowledges Yearn "cannot rely on a traditional waterfall process" due to deploy cadence — the score is the compensating control.
This mirrors how we think about audit ROI: security as a continuous lifecycle rather than a one-time gate.

Comparative posture: Yearn vs Beefy, Morpho, Sommelier

Yearn V3 sits at a specific point on the yield-aggregator design axis: maximum on-chain flexibility + internal-balance anti-donation + immutable strategies + opt-in timelocks.
  • Beefy: simpler (single-strategy per vault, keeper-compounded). "Harvest on deposit" removes the MEV-sandwich window
  • Morpho MetaMorpho: hardwires timelocks (≥24h V1; 0–3 weeks configurable V2) with Owner/Curator/Allocator/Guardian separation — stronger user-visible safety than Yearn's opt-in role separation
  • Sommelier: off-chain strategist-signed instructions relayed through a Cosmos validator set, guarded by allowedRebalanceDeviation and shareLockPeriod
Architectural correspondence: Yearn's DEBT_MANAGER ≈ Morpho's Allocator ≈ Sommelier's Strategist; Yearn's MAX_DEBT_MANAGER ≈ Morpho's Curator; EMERGENCY_MANAGER ≈ Morpho's Guardian/Sentinel.

Conclusion: heuristics distilled

Yearn's multi-year defense evolution is a readable record of how yield-aggregator security thinking matured. V1's monolithic public-earn() contracts with spot-DEX pricing were replaced by V2's multi-strategy debt-allocation model, then V3 decomposed the remaining logic into an immutable shared Solidity TokenizedStrategy implementation plus periphery modules.
Concrete heuristics for auditors and designers:
  • Never derive totalAssets from live AMM/pool state readable during a callback
  • Never use pricePerShare as an oracle without donation-resistance and TWAP
  • Verify selector collisions between strategy code and TokenizedStrategy
  • Forbid raw sstore to the namespaced storage slot
  • Seed any fresh 4626 vault before opening deposits
  • Treat set_open_role on anything except REPORTING_MANAGER as catastrophic
  • Validate maxLoss defaults in every withdraw/redeem call path
  • Recognize that every confirmed Yearn-ecosystem loss originated in legacy v1, integrator oracle misuse, or strategy-author oracle choice — not the vault core itself
The V3 architecture codifies the lessons, but the weakest link moves to wherever an author's _harvestAndReport reads a manipulable price, or to wherever an integrator reads PPS without caveat.

Partner with Zealynx

At Zealynx, we audit DeFi vault stacks, ERC-4626 wrappers, and tokenized strategy systems inspired by Yearn's design. Whether you're forking V3 patterns, integrating with existing Yearn vaults, or building a novel aggregator, we map the threat model onto your specific code — including oracle choice in _harvestAndReport, selector collisions in delegatecall proxies, and silent-loss maxLoss paths in integrator wrappers.
Browse our writeups on Balancer's protocol architecture, Liquity protocol internals, and our broader security engine of DeFi writeup — then reach out.

Get in touch

Planning a vault audit, a fork of V3 patterns, or an integration that reads pricePerShare? We've reviewed enough of these stacks to know where the silent bugs live. Book a scoping call and we'll walk your specific threat model.

FAQ: Yearn vault security

1. What is a tokenized strategy in Yearn V3, and why does it matter for audits?
A tokenized strategy is an ERC-4626-compliant strategy contract that delegates all standard plumbing (ERC-20 logic, profit-locking, fee accrual, reporting, access control) to a single immutable TokenizedStrategy implementation deployed at the same deterministic CREATE2 address on every chain (0xBB51273D6c746910C7C06fe718f30c936170feD0). The strategist only writes three callbacks: _deployFunds, _freeFunds, and _harvestAndReport.
For auditors this matters because the shared implementation is audited once and inherited by every strategy — but strategists can accidentally break the model through selector collisions with the TokenizedStrategy ABI, inheritance that introduces colliding storage slots, or direct _mint calls that violate profit-locking invariants.
2. What is locked-profit degradation and how does it prevent sandwich attacks?
Locked-profit degradation is Yearn V2's mechanism for vesting a strategy's reported gain over time instead of realizing it instantly. After a profitable report(), the gain is stored in lockedProfit and linearly unlocked at a rate of ~6 hours (default DEGRADATION_COEFFICIENT). Share price is calculated as (totalAssets - lockedProfit) / totalSupply, so a depositor entering one block after a profitable report cannot sandwich the share price.
V3 replaces the scalar with share-level accounting: profit shares are minted to the vault itself and vested via an unlock rate, producing the same anti-sandwich property but with cleaner accounting.
3. Why is using pricePerShare as an oracle dangerous?
pricePerShare can change within a single block during process_report, and in V2 versions prior to API 0.4.4 it could be inflated via direct token donations to the vault (since totalAssets read balanceOf(self)). An integrator using pricePerShare as collateral valuation — as Cream Finance did with yUSD in October 2021 — can be manipulated via flash-loan donations into marking collateral at 2x its real value, enabling massive over-borrowing.
Safe integration requires TWAP enforcement, min/max caps, and donation-resistant valuation. Yearn's own documentation explicitly warns against pricePerShare as an oracle.
4. What is the maxLoss footgun in Yearn vault withdrawals?
maxLoss is the maximum loss (in basis points) a user will tolerate when withdrawing. The footgun is that defaults vary across functions: V2 Vault.withdraw defaults to 1 bp (revert on any real loss), V3 Vault.withdraw defaults to 0 bps (revert on any loss at all), but V3 Vault.redeem defaults to MAX_BPS = 10_000 — meaning it silently accepts up to 100% loss.
For integrators building vaults-of-vaults or wrappers that call redeem() without passing an explicit maxLoss, this is a silent-loss bug class. Always set maxLoss explicitly at integration boundaries.
5. What is read-only reentrancy and how does it affect Yearn strategies?
Read-only reentrancy occurs when a contract can be called back during a state-mutating operation while its storage is in a transiently inconsistent state. In Curve pools that transfer raw ETH during remove_liquidity, an external contract can reenter during the ETH callback and read get_virtual_price() while pool balances and totalSupply are temporarily out of sync.
If a Yearn strategy reads get_virtual_price() inside _harvestAndReport, an attacker with permissionless report() access can produce a phantom-profit report. The mitigation is a reentrancy "poke" like pool.remove_liquidity(0, [0,0]) before reading, which reverts if the pool is mid-callback. Balancer's equivalent is balancerVault.reentrancyGuardEntered().
6. Does Yearn have an on-chain timelock protecting vault parameters?
No. This is a frequently misunderstood property. The 6-of-9 ychad.eth Gnosis Safe can execute strategy additions, debt-ratio changes, and registry endorsements atomically once six signers approve. V3's role separation (requiring both ADD_STRATEGY_MANAGER and MAX_DEBT_MANAGER to act) mitigates single-role compromise but not a full multisig takeover.
Integrators should surface this trust assumption explicitly. Contrast with Morpho's MetaMorpho, which hardwires timelocks of ≥24 hours (V1) or 0–3 weeks configurable (V2) between curator actions and execution — a stronger user-visible safety property.

Glossary

TermDefinition
ERC-4626Tokenized vault standard providing a unified API for yield-bearing vaults with deposit, withdrawal, and share accounting.
Vault accountingInternal tracking of token balances within a protocol's central contract, separate from actual ERC-20 transfers.
Locked profit degradationYearn V2 mechanism that vests strategy gains linearly over time to prevent PPS sandwich attacks.
Inflation attackFirst-depositor exploit that manipulates share price via direct donations to an empty vault.
Strategy driftDivergence between a strategy's intended behavior and its real on-chain execution.

Get the DeFi Protocol Security Checklist

15 vulnerabilities every DeFi team should check before mainnet. Used by 30+ protocols.

No spam. Unsubscribe anytime.

oog
zealynx

Smart Contract Security Digest

Monthly exploit breakdowns, audit checklists, and DeFi security research — straight to your inbox

© 2026 Zealynx