DEV Community

ohmygod
ohmygod

Posted on

The Curve LlamaLend Donation Attack: How a $240K Oracle Manipulation Exposed Soft-Liquidation's Achilles Heel

On March 2, 2026, an attacker exploited a fundamental design flaw in Curve Lend's LlamaLend protocol, draining approximately $240,000 from the sDOLA market. The attack didn't rely on a classic reentrancy bug or access control flaw — it weaponized Curve's own soft-liquidation mechanism against its users through a carefully orchestrated donation attack that manipulated the sDOLA oracle price.

This incident highlights a class of vulnerability that's becoming increasingly common in DeFi: atomic oracle manipulation in vault-based collateral systems.

What Is LlamaLend's Soft-Liquidation?

Unlike traditional lending protocols (Aave, Compound) that use hard liquidations — selling all collateral once a health factor drops below 1.0 — Curve's LlamaLend uses an innovative LLAMMA (Lending-Liquidating AMM Algorithm) soft-liquidation mechanism.

In this system, as a borrower's collateral value drops, their position is gradually converted between collateral and debt through an internal AMM. Think of it as a continuous rebalancing that's supposed to protect borrowers from sudden, catastrophic liquidations.

The key insight: soft-liquidation behaves like an AMM position, which means it's subject to impermanent loss — especially when prices change atomically.

The Attack: Step by Step

Here's how the attacker (address: 0x33a0...4be2) executed the exploit:

Step 1: Trigger Soft-Liquidations

The attacker first identified all positions in the sDOLA/crvUSD market that were in soft-liquidation range. By pushing market conditions, they triggered soft-liquidations on these positions, forcing the AMM to begin "selling" sDOLA collateral into crvUSD.

Step 2: Donate to Manipulate the Oracle

The critical move: the attacker donated assets to the sDOLA vault, atomically changing the sDOLA exchange rate:

Before: 1.188 sDOLA = 1 DOLA
After:  1.358 sDOLA = 1 DOLA  (~14% increase)
Enter fullscreen mode Exit fullscreen mode

Because sDOLA is a vault token, its price is determined by the ratio of underlying assets to shares. A donation directly inflates this ratio — instantly, in a single transaction.

Step 3: Exploit Impermanent Loss

Here's the devastating part. The soft-liquidation AMM had already begun converting users' sDOLA into crvUSD at the old price. When the oracle price jumped 14% atomically, the AMM positions experienced instant impermanent loss.

Even though the collateral was now worth more, the AMM had already partially converted at the lower price, locking in losses.

// Simplified: The AMM's internal price lags behind the oracle
// When oracle jumps atomically, the gap becomes instant IL

// Before donation: AMM converting at price P
// After donation:  Oracle says price is 1.14 * P
// AMM positions are now "stale" — they sold low, can't buy back
Enter fullscreen mode Exit fullscreen mode

Step 4: Hard-Liquidate and Extract

With positions now underwater due to impermanent loss (not actual collateral value decline), the attacker:

  1. Hard-liquidated all affected positions
  2. Repaid users' crvUSD debt with crvUSD from the liquidation
  3. Kept the remaining sDOLA collateral
  4. Deposited gained collateral back into Curve Lend
  5. Borrowed crvUSD to repay flash loans
  6. Routed profits through Tornado Cash

Transaction: 0xb935...d8a4

The Root Cause: Atomic Oracle Incompatibility

The fundamental issue is an incompatibility between Curve Lend's soft-liquidation mechanism and atomically-changeable oracles.

Traditional price oracles (Chainlink, TWAP-based) change gradually over time. The LLAMMA soft-liquidation mechanism was designed with this assumption — prices drift, and the AMM rebalances smoothly.

But vault tokens like sDOLA have a different price mechanism. Their "price" is the exchange rate between the vault share and the underlying asset. This rate can change atomically — in a single transaction — via deposits (donations) to the vault.

# Vault token price = total_assets / total_shares
# A donation increases total_assets without changing total_shares
# Result: instant, atomic price increase

class VaultOracle:
    def price(self):
        return self.vault.totalAssets() / self.vault.totalShares()

    # Attacker calls: vault.donate(large_amount)
    # price() instantly jumps — no TWAP protection
Enter fullscreen mode Exit fullscreen mode

Why This Matters Beyond Curve

This vulnerability pattern affects any lending protocol that:

  1. Uses vault/receipt tokens as collateral (sDOLA, stETH wrappers, yield-bearing tokens)
  2. Has liquidation mechanisms that assume gradual price changes
  3. Doesn't sanitize oracle inputs for atomic manipulation

As DeFi moves toward yield-bearing collateral everywhere, this attack surface is expanding rapidly.

Defense Patterns

1. Oracle Rate-of-Change Guards

Reject oracle updates that exceed a maximum rate of change within a single block:

contract SafeVaultOracle {
    uint256 public lastPrice;
    uint256 public lastBlock;
    uint256 public constant MAX_CHANGE_BPS = 500; // 5% max per block

    function getPrice() external returns (uint256 price) {
        price = _rawVaultPrice();

        if (lastBlock == block.number) {
            // Same block — enforce rate limit
            uint256 change = price > lastPrice 
                ? ((price - lastPrice) * 10000) / lastPrice
                : ((lastPrice - price) * 10000) / lastPrice;

            require(change <= MAX_CHANGE_BPS, "Oracle: atomic change too large");
        }

        lastPrice = price;
        lastBlock = block.number;
        return price;
    }

    function _rawVaultPrice() internal view returns (uint256) {
        return IVault(vault).totalAssets() * 1e18 / IVault(vault).totalShares();
    }
}
Enter fullscreen mode Exit fullscreen mode

2. TWAP Wrappers for Vault Tokens

Never use spot vault exchange rates directly. Wrap them in a time-weighted average:

contract VaultTWAPOracle {
    struct Observation {
        uint256 timestamp;
        uint256 priceCumulative;
    }

    Observation[] public observations;
    uint256 public constant WINDOW = 30 minutes;

    function consult() external view returns (uint256) {
        uint256 currentCumulative = _currentCumulative();

        // Find observation at least WINDOW seconds ago
        for (uint i = observations.length; i > 0; i--) {
            Observation memory obs = observations[i - 1];
            if (block.timestamp - obs.timestamp >= WINDOW) {
                uint256 elapsed = block.timestamp - obs.timestamp;
                return (currentCumulative - obs.priceCumulative) / elapsed;
            }
        }
        revert("TWAP: insufficient history");
    }
}
Enter fullscreen mode Exit fullscreen mode

3. Donation-Resistant Vault Design

Implement virtual reserves that dilute the impact of donations:

// ERC-4626 with virtual offset (OpenZeppelin pattern)
function _convertToAssets(uint256 shares, Math.Rounding rounding) 
    internal view override returns (uint256) 
{
    return shares.mulDiv(
        totalAssets() + 1,      // +1 virtual asset
        totalSupply() + 1e3,    // +1000 virtual shares (offset)
        rounding
    );
}
// The offset makes donation attacks prohibitively expensive
// To move price by 14%, attacker needs to donate 14% of (totalAssets + offset)
Enter fullscreen mode Exit fullscreen mode

4. Soft-Liquidation Circuit Breakers

Pause soft-liquidations when oracle volatility exceeds thresholds:

# Monitoring script for vault oracle manipulation
from web3 import Web3

def monitor_vault_oracle(vault_address, threshold_pct=5):
    w3 = Web3(Web3.HTTPProvider(RPC_URL))
    vault = w3.eth.contract(address=vault_address, abi=VAULT_ABI)

    last_rate = vault.functions.convertToAssets(10**18).call()

    while True:
        current_rate = vault.functions.convertToAssets(10**18).call()
        change_pct = abs(current_rate - last_rate) / last_rate * 100

        if change_pct > threshold_pct:
            alert(f"⚠️ Vault rate changed {change_pct:.1f}% — possible donation attack")
            # Trigger emergency pause or alert

        last_rate = current_rate
        time.sleep(12)  # Every block
Enter fullscreen mode Exit fullscreen mode

Audit Checklist: Vault Collateral Oracle Safety

When auditing any protocol that accepts vault/receipt tokens as collateral:

  • [ ] Can the vault exchange rate change atomically? (donation, direct transfer, flash mint)
  • [ ] Is there a TWAP or rate-of-change guard on the oracle?
  • [ ] Does the liquidation mechanism assume gradual price changes?
  • [ ] Are virtual offsets used in share-to-asset conversion? (ERC-4626 inflation attack mitigation)
  • [ ] Can flash loans be used to temporarily inflate the vault rate?
  • [ ] Is there a minimum time between oracle updates?
  • [ ] Are circuit breakers in place for abnormal price movements?

The Bigger Picture

The Curve LlamaLend donation attack represents a growing class of DeFi vulnerabilities at the intersection of composability and oracle design. As protocols increasingly use yield-bearing vault tokens (stETH, sDAI, sDOLA, PT tokens) as collateral, the assumption that "prices change gradually" breaks down.

Every lending protocol needs to ask: What happens when my oracle price changes by 14% in a single transaction? If the answer involves impermanent loss, bad debt, or cascading liquidations — you have a bug.

The fix isn't complicated, but it requires acknowledging that vault tokens are fundamentally different from spot-priced assets. Treat them accordingly.


DreamWork Security researches DeFi vulnerabilities and publishes technical analysis to help protocol teams build safer systems. Follow for weekly deep dives into the latest exploits and defense patterns.

Tags: #security #defi #web3 #solidity

Top comments (0)