DEV Community

ohmygod
ohmygod

Posted on

ERC-4626 Vault Inflation Attacks Still Aren't Solved: Lessons From the sDOLA Llamalend Exploit

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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:

  1. Flash-loaned a large position
  2. Used the donate() function to inflate sDOLA's internal exchange rate from ~1.189 to ~1.353 DOLA
  3. This price distortion triggered the oracle to report inflated collateral values
  4. Existing borrowers' positions became "underwater" based on the manipulated rate
  5. 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
    }
}
Enter fullscreen mode Exit fullscreen mode

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));
}
Enter fullscreen mode Exit fullscreen mode

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;
    // ...
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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");
    // ...
}
Enter fullscreen mode Exit fullscreen mode

5. Minimum Deposit Threshold

require(assets >= MIN_DEPOSIT, "BELOW_MINIMUM");
Enter fullscreen mode Exit fullscreen mode

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");
}
Enter fullscreen mode Exit fullscreen mode

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");
    _;
}
Enter fullscreen mode Exit fullscreen mode

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)