Introduction
YieldBasis is one of the most anticipated DeFi protocols of 2025. It promises to eliminate impermanent loss for liquidity providers by maintaining LP positions at constant 2× leverage via a dedicated Curve-native AMM. With deep integration into the Curve ecosystem and crvUSD, the protocol attracted significant attention from both the DeFi and security communities.
I (Talfao) participated in the contest together with my colleague Kalogerone. We finished 4th place, identifying a High severity issue that ended up having only three duplicates. Interestingly, we initially assumed this bug would be discovered by nearly everyone.
The contest itself was one of the most interesting ones I have participated in — mainly because the architecture of YieldBasis is exceptionally well designed.
The Protocol: How YieldBasis Works (In Brief)
To understand the vulnerability, a basic mental model of YieldBasis's architecture is necessary.
-
LT.vy (Liquidity Token) The core vault. Users deposit assets and receive LT shares representing their LP position.
LT shares have a unique property: the staker's balance rebases to reflect profits or losses from the leveraged AMM strategy.
Unstaked tokens do not rebase — instead, they accumulate or lose value through the fee stream.
-
LiquidityGauge.vy (staker in the
LT.vy) An ERC4626 vault that wraps LT shares. Users stake their LT shares into the gauge to earn YB token emissions.The gauge itself is registered as the
stakerinLT.vy, meaning the gauge's address is the one whosebalanceOfrebases. -
Negative Rebase
When the leveraged strategy incurs losses,
LT.vyreduces the staker's balance (the gauge's balance).However, this reduction does not occur automatically. It is only applied when the contract is interacted with, for example via:
depositwithdrawtransfer
This final detail is the core of the vulnerability.
The Vulnerability: Stale balanceOf[staker]
The Core Problem
LiquidityGauge.vy implements the ERC4626 standard. Whenever a user deposits or redeems shares, it must compute the exchange rate between gauge shares and underlying LT shares.
This rate depends on _total_assets():
def _total_assets() -> uint256: return staticcall _ASSET.balanceOf(self)
The function simply reads the gauge's LT share balance via balanceOf(self).
The issue is that balanceOf[staker] in LT.vy is lazily updated.
The balance only becomes correct when certain functions execute:
_transferdepositwithdrawemergency_withdraw
These functions internally call _calculate_values(), which applies the current token_reduction — the mechanism implementing negative rebasing.
If no interaction occurs before a gauge deposit or redemption, the gauge reads a stale balance that does not reflect accumulated losses.
The Consequences
This staleness results in theft of value from other gauge shareholders.
When a user redeems gauge shares while balanceOf[staker] is stale, they receive more LT shares than they should. The remaining shareholders absorb the entire loss.
A Concrete Example
The following scenario illustrates the issue.
| Step | State |
|---|---|
| Initial | Gauge has 1,000 shares total. User A and User B each hold 500. The gauge holds 1,000 LT shares (balanceOf[staker] = 1000). |
| Losses occur | The strategy incurs losses. The gauge balance should rebase to 900 LT shares, but because no interaction occurs, balanceOf[staker] still reports 1,000. |
| User B redeems | User B notices the stale balance and redeems their 500 shares. The gauge calculates: 500 / 1000 * 1000 = 500 LT shares. |
| During transfer | LT.vy._transfer(staker, userB, 500) executes. Inside _transfer, _calculate_values() applies the pending loss and updates balanceOf[staker] to 900. |
| After transfer | The transfer subtracts 500 from the updated balance: 900 - 500 = 400. |
Result
User B receives 500 LT shares instead of the correct 450.
User A's remaining 500 gauge shares are now backed by 400 LT shares, instead of 450.
User B effectively extracted 50 LT shares from User A.
Why Was This Missed?
The interesting question is why this bug was overlooked, especially since stale state reads are a well-known class of vulnerability.
1. The Complexity Was Elsewhere
YieldBasis is a sophisticated system.
The protocol contains:
- leveraged AMM mechanics
- rebalancing logic
- virtual pool accounting
- governance systems
- gauges and controllers
- emission mechanics
Many of these components were audited independently by different firms, which highlights one of the major risks of isolated audit scope.
2. Cross-Contract State Dependencies Are Hard
The bug exists at the boundary between two contracts.
LiquidityGauge.vybehaves correctly in isolation.LT.vyrebasing logic is also correct in isolation.
The vulnerability only appears when the two interact.
The gauge assumes the LT balance is fresh, while LT updates balances lazily on interaction.
3. It Requires Understanding Rebase Timing
To detect this issue, an auditor must understand:
- when
_calculate_values()executes - what triggers
token_reduction - how
balanceOf[staker]is updated
This requires following the flow across multiple contracts and understanding the temporal dependency between them.
The Fix
The mitigation is conceptually straightforward.
Before relying on balanceOf[staker], the system must ensure the value reflects the current state — either by forcing a state synchronization or by designing the accounting system so that rebases are not lazily applied.
Lessons for Auditors
1. Never blindly trust balanceOf when rebasing is involved.
If you audit an ERC4626 vault, always verify whether the underlying token behaves like a standard ERC20. If balances can rebase or update lazily for specific holders, every balanceOf read becomes suspect.
2. Focus on contract boundaries.
The most dangerous vulnerabilities often appear where two correct systems interact incorrectly. Always analyze the assumptions each contract makes about external state.
3. Audit the "boring" contracts too.
In this case, the gauge was considered a simple wrapper compared to the complex AMM logic. Naturally, auditors focused their effort on the mathematically complex components.
The vulnerability existed in the accounting layer above them.
4. Think about timing.
In DeFi, state is only as current as the last transaction that modified it.
Whenever you see a staticcall to read external state, ask:
When was this value last updated, and could it be stale?
Conclusion
This finding reinforced something we at CODESPECT strongly believe:
Integration surfaces are often the most overlooked attack vectors.
When contracts depend on each other — whether internally within the protocol or through external integrations — the interaction layer becomes critical.
In this case, the vulnerability was created by a simple combination of factors:
- a stale
balanceOfread - a rebasing balance updated only on interaction
- a vault contract trusting that external state was synchronized
Individually, these are harmless design decisions. Together, they created a high-severity loss-of-funds vulnerability that was missed by multiple professional audit firms and discovered by only four accounts out of hundreds of contest participants.
Security is rarely about finding the most clever exploit.
It is about challenging assumptions — especially the ones that seem too obvious to question.
This vulnerability was identified by Talfao and Kalogerone, competing as part of CODESPECT in the YieldBasis Sherlock audit contest (Contest #1102). For smart contract security audits and penetration testing, reach out to us at CODESPECT.
