Aave V3 Risk For Erc4626 Vaults Using “Virtual Shares” Mispricing
Written by
Cipher Stone
The problem I stumbled into
I spent a weekend trying to wire an ERC4626 vault into a DeFi portfolio and then feed its “share price” into an Aave v3 risk dashboard. The weird part was simple: the vault’s displayed conversion rate looked sane, but the health factor predictions were consistently optimistic.
The culprit was a subtle mechanism inside many ERC4626 vaults: virtual shares. Virtual shares are a small, artificial amount of “share tokens” minted conceptually to prevent edge cases like division-by-zero or extreme rounding at the beginning of a vault. They also change how preview math works, especially when deposits/withdrawals happen near the boundaries.
When those preview calculations are used incorrectly—particularly when you compute an Aave collateral value from the vault’s preview conversion rate—you can end up assuming more collateral value than actually backs the vault shares on-chain.
This post documents what I built and exactly how the mispricing happens, with runnable code.
What I built (and why)
I created a tiny simulator for a vault-like contract that implements the ERC4626 idea of convertToAssets() and convertToShares() but includes a configurable virtualShares value. Then I compared:
- “Real” share price computed from actual reserves (assets held by the vault / total real shares).
- Preview share price computed using preview formulas that incorporate virtual shares.
Finally, I mapped the difference into a crude “Aave-like” collateral model:
collateral value = shares * assumed share price, then apply an LTV (loan-to-value).
This reproduces the exact failure mode: optimistic risk when you use preview-based conversion rates.
The core math that breaks
ERC4626 defines:
assetsPerShare(conceptually) astotalAssets / totalSupplyconvertToAssets(shares)asshares * totalAssets / totalSupplyconvertToShares(assets)asassets * totalSupply / totalAssets
Now introduce virtual shares. A common pattern is:
effectiveTotalSupply = totalSupply + virtualSharesconvertToAssets(shares) = shares * totalAssets / (totalSupply + virtualShares)
That means share value (in assets) is lower than the naive computation would suggest, especially when totalSupply is small.
If you compute collateral from convertToAssets(totalUserShares) using a preview/estimator that doesn’t match the vault’s real accounting expectations, you effectively inflate the collateral.
Runnable simulator (Python)
This script models a vault with virtual shares and shows the mismatch.
from dataclasses import dataclass @dataclass class VaultSim: total_assets: int # Actual assets held by the vault total_supply: int # Total real shares virtual_shares: int = 0 # Virtual shares used in conversion math def convert_to_assets_preview(self, shares: int) -> int: """ Preview-style conversion that includes virtual shares in the denominator (effective total supply). """ effective_total_supply = self.total_supply + self.virtual_shares if effective_total_supply == 0: return 0 # Integer division like Solidity return shares * self.total_assets // effective_total_supply def convert_to_assets_real(self, shares: int) -> int: """ "Real" conversion based on actual supply only. This is the naive computation that ignores virtual shares. """ if self.total_supply == 0: return 0 return shares * self.total_assets // self.total_supply def aave_like_borrow_limit(collateral_value_assets: int, ltv_bps: int) -> int: """ Aave-like limit: borrowable = collateral_value * LTV LTV is in basis points (bps), where 10000 = 100%. """ return collateral_value_assets * ltv_bps // 10_000 def main(): # Example vault state: # - Vault holds 1,000 assets # - Total real shares are only 100 (early stage / small supply) # - Virtual shares are 900 (huge compared to real supply) # # This exaggerates the effect so the difference is obvious. vault = VaultSim(total_assets=1_000, total_supply=100, virtual_shares=900) # User holds 10 real shares user_shares = 10 # Two different conversions: real_assets = vault.convert_to_assets_real(user_shares) preview_assets = vault.convert_to_assets_preview(user_shares) print("=== Vault State ===") print(f"total_assets: {vault.total_assets}") print(f"total_supply (real shares): {vault.total_supply}") print(f"virtual_shares: {vault.virtual_shares}") print() print("=== User Shares ===") print(f"user_shares: {user_shares}") print() print("=== Conversion (assets for the same shares) ===") print(f"Real conversion (ignores virtual shares): {real_assets} assets") print(f"Preview conversion (includes virtual shares): {preview_assets} assets") print() # Suppose we are computing Aave collateral value from these conversions. # Use an LTV of 75% (7500 bps). ltv_bps = 7_500 real_borrowable = aave_like_borrow_limit(real_assets, ltv_bps) preview_borrowable = aave_like_borrow_limit(preview_assets, ltv_bps) print("=== Aave-like Borrowable Limit ===") print(f"LTV: {ltv_bps / 100:.2f}%") print(f"Borrowable using real conversion: {real_borrowable} assets") print(f"Borrowable using preview conversion:{preview_borrowable} assets") print() # Show the optimism delta (how much the dashboard would overestimate) delta = real_borrowable - preview_borrowable print("=== Mispricing ===") print(f"Overestimation (optimistic collateral): {delta} assets") if __name__ == "__main__": main()
What happens when I run it
The output shows:
- Real conversion ignores virtual shares: it computes
10 * 1000 / 100 = 100 assets - Preview conversion includes virtual shares: effective supply is
100 + 900 = 1000, so10 * 1000 / 1000 = 10 assets
With 75% LTV, that becomes:
- Borrowable using real conversion:
100 * 75% = 75 - Borrowable using preview conversion:
10 * 75% = 7
So a dashboard that uses the wrong conversion overestimates collateral by an order of magnitude.
Translating this into an Aave-style integration bug
In a real DeFi integration, the mistake usually looks like one of these:
- Using
previewRedeem()/previewWithdraw()outputs for accounting, while the vault’s internal state evolves and those previews don’t match the exact collateralization math expected by the consumer. - Computing an assumed “assets per share” from
convertToAssets(1e18)using an incorrect supply basis (like forgetting that the vault uses effective supply with virtual shares).
A concrete failure pattern
- I read a vault’s conversion rate to estimate collateral value.
- I used that estimate to compute “how much I can borrow” under a risk model.
- Later, actual withdrawals / redemption returned less than predicted because the conversion rate incorporated virtual shares differently than my assumption.
That mismatch is exactly what the simulator demonstrates.
A minimal Solidity-like conversion snippet
Here’s the conversion logic in a compact Solidity style so the correspondence is clear (not deployable as-is, but it’s the math).
pragma solidity ^0.8.20; contract VirtualShareConversion { uint256 public totalAssets; uint256 public totalSupply; // real shares uint256 public virtualShares; // conceptual shares constructor(uint256 _totalAssets, uint256 _totalSupply, uint256 _virtualShares) { totalAssets = _totalAssets; totalSupply = _totalSupply; virtualShares = _virtualShares; } function convertToAssetsPreview(uint256 shares) public view returns (uint256) { uint256 effectiveTotalSupply = totalSupply + virtualShares; if (effectiveTotalSupply == 0) return 0; return shares * totalAssets / effectiveTotalSupply; } function convertToAssetsReal(uint256 shares) public view returns (uint256) { if (totalSupply == 0) return 0; return shares * totalAssets / totalSupply; } }
The only conceptual difference is whether you divide by totalSupply or (totalSupply + virtualShares).
How to spot it quickly in practice
When I debugged a live vault, I focused on three checks:
- Small-supply regime: If
totalSupplyis small relative to whatever virtual-share parameter the vault uses, mispricing will be dramatic. - Preview outputs vs actual redemption: Compare
previewRedeem(shares)to the effective assets returned for real transactions. - Consistency of math basis: Every consumer of “share price” must use the same conversion basis the vault uses internally.
The most reliable fix was to avoid “share price” shortcuts and instead compute collateral value using the vault’s own conversion functions in the same call context that reflects its effective accounting.
Conclusion
I learned that “virtual shares” in ERC4626-style vaults can make preview-based conversion rates diverge sharply from naive real share pricing—especially early in a vault when totalSupply is small. When an Aave-like risk model uses the wrong conversion basis, it can overestimate collateral value and produce dangerously optimistic borrowing limits.