It's March 2026, and we're still watching protocols lose money to ERC-4626 inflation attacks.
The sDOLA Llamalend exploit on March 2nd drained ~$240K through a classic donation-style attack that manipulated sDOLA's exchange rate. The attacker used flash loans to inflate totalAssets(), triggering cascading liquidations of innocent borrowers.
This wasn't a novel zero-day. It was a well-documented vulnerability class that has been public knowledge since 2022. Yet here we are.
Let's dissect exactly how this attack works, why it keeps happening, and what you need to do to stop it.
The Anatomy of an ERC-4626 Inflation Attack
ERC-4626 standardizes tokenized vaults — you deposit assets, receive shares. The share price is determined by a simple ratio:
shares = (depositAmount × totalSupply) / totalAssets
The attack exploits Solidity's integer division (which rounds down) in three steps:
Step 1: Seed the Vault
The attacker deposits a tiny amount (1 wei) into a new or low-liquidity vault, receiving 1 share.
// Attacker gets 1 share for 1 wei
vault.deposit(1, attacker);
// totalSupply = 1, totalAssets = 1
Step 2: Inflate the Exchange Rate
Instead of depositing through deposit(), the attacker directly transfers a large amount of the underlying token to the vault contract:
// Direct transfer — no shares minted
underlying.transfer(address(vault), 1_000_000e18);
// totalSupply = 1, totalAssets = 1_000_000e18 + 1
Now each share is "worth" 1M+ tokens.
Step 3: Victim Gets Zero
When a legitimate user deposits, say, 500K tokens:
shares = (500_000e18 × 1) / 1_000_000e18 = 0 (rounds down)
Zero shares. The victim's 500K tokens are now trapped in the vault, and the attacker — holding the only share — can withdraw everything.
The sDOLA Variant
The Llamalend exploit was more sophisticated. Rather than targeting first depositors, the attacker:
- Flash-loaned a large position
- Used the
donate()function to inflate sDOLA's internal exchange rate from ~1.189 to ~1.353 DOLA - This price distortion triggered the oracle to report inflated collateral values
- Existing borrowers' positions became "underwater" based on the manipulated rate
- The attacker liquidated these positions for profit
Same root cause — external balance manipulation — but weaponized through the lending protocol's liquidation engine.
Why Protocols Keep Getting Hit
Three recurring patterns:
1. "We Use OpenZeppelin, We're Safe"
OpenZeppelin's ERC-4626 implementation (v5.x) includes virtual offset protection. But you have to enable it by overriding _decimalsOffset(). The default returns 0, which provides no protection.
// ❌ Default — still vulnerable
contract MyVault is ERC4626 { }
// ✅ Protected — override the offset
contract MyVault is ERC4626 {
function _decimalsOffset() internal pure override returns (uint8) {
return 6; // or at least 3
}
}
2. "totalAssets() Uses balanceOf()"
If your vault calculates total assets by reading the token balance:
// ❌ Vulnerable to donation
function totalAssets() public view returns (uint256) {
return asset.balanceOf(address(this));
}
Anyone can inflate this by transferring tokens directly. Internal accounting is mandatory:
// ✅ Track deposits internally
uint256 private _totalManagedAssets;
function totalAssets() public view returns (uint256) {
return _totalManagedAssets;
}
function _deposit(...) internal override {
_totalManagedAssets += assets;
// ...
}
3. "Oracles Will Catch It"
The sDOLA exploit proved that if your oracle reads from a manipulable source (like an ERC-4626 vault's exchange rate), the oracle becomes the attack vector. TWAPs help but aren't bulletproof against flash-loan-scale manipulation within a single block.
The 8 Defense Patterns
Here's a comprehensive checklist. Implement ALL of these, not just one:
1. Virtual Offsets (Minimum Viable Defense)
Add virtual shares and assets to the conversion math. This makes inflation economically unfeasible.
function _decimalsOffset() internal pure override returns (uint8) {
return 6; // Makes attack cost ~1M× the potential profit
}
2. Dead Shares on Deployment
Mint a small initial supply to the zero address during deployment:
constructor(...) {
_mint(address(0xdead), 1000);
asset.transferFrom(msg.sender, address(this), 1000);
}
3. Internal Asset Tracking
Never rely on balanceOf(address(this)). Track all deposits and withdrawals through internal state variables.
4. Zero-Share Revert Guard
function deposit(uint256 assets, address receiver) public override returns (uint256 shares) {
shares = previewDeposit(assets);
require(shares > 0, "ZERO_SHARES");
// ...
}
5. Minimum Deposit Threshold
require(assets >= MIN_DEPOSIT, "BELOW_MINIMUM");
6. Slippage Protection on Deposits
function depositWithSlippage(
uint256 assets,
address receiver,
uint256 minSharesOut
) external returns (uint256 shares) {
shares = deposit(assets, receiver);
require(shares >= minSharesOut, "SLIPPAGE");
}
7. Oracle Hardening for Vault Collateral
If your lending protocol accepts ERC-4626 tokens as collateral:
- Use time-weighted exchange rates, not spot rates
- Cap the maximum rate change per block
- Cross-reference against external price feeds
8. Flash Loan Guards
modifier noFlashLoan() {
require(block.number > _lastDepositBlock[msg.sender], "SAME_BLOCK");
_;
}
Testing Checklist
Before deploying any ERC-4626 vault, your test suite should include:
- Fuzz test: random deposit/withdraw sequences with donation interspersed
- Invariant test: totalAssets ≈ sum of all depositor balances (within rounding)
- Edge case: first depositor with 1 wei followed by large donation
- Edge case: empty vault after full withdrawal, then re-deposit
- Flash loan simulation: deposit + donate + withdraw in same tx
- Oracle manipulation: verify price feed behavior under 10× exchange rate change
Tools like Echidna, Medusa, and Foundry's invariant testing are your best friends here.
The Bigger Picture
The sDOLA Llamalend exploit wasn't just about one vault. It exposed a systemic risk: ERC-4626 vaults as DeFi composability primitives create attack surfaces that compound across protocols.
When you use a vault token as collateral in a lending protocol, you're implicitly trusting:
- The vault's share price calculation
- The vault's resistance to donation attacks
- The oracle's ability to distinguish manipulated prices from real ones
- The lending protocol's liquidation engine under extreme price movements
Any weakness in this chain is exploitable. The sDOLA attacker didn't need to break the vault itself — they just needed to wiggle the exchange rate enough to trigger liquidations.
As DeFi composability deepens in 2026, expect more of these cross-protocol exploits. The defense isn't just securing your own contracts — it's understanding the full dependency graph of every token you integrate.
Minimum bar for 2026: Virtual offsets + internal tracking + zero-share revert. If you're not doing at least these three, your vault is a ticking time bomb.
Found this useful? Follow for weekly DeFi security deep dives covering vulnerabilities, audit tooling, and defensive patterns.
Top comments (0)