LlamaRisk
LlamaLend sDOLA-long2 Post-mortem

LlamaLend sDOLA-long2 Post-mortem

This Post-mortem was produced in collaboration with Swiss Stake, CurveDocs and Wavey

#1. Executive Summary

On March 2, 2026, an exploit occurred on the sDOLA/crvUSD LlamaLend market using a two-pronged flash loan attack, hard-liquidating 27 active borrowers (~$10.9M total debt). The attack combined a large exchange on the LLAMMA AMM to force all positions into soft-liquidation, followed by a DolaSavings.stake() call to inflate the sDOLA oracle price, making the soft-liquidated positions instantly unhealthy.

#Root Cause

Four factors combined to enable this attack:

  1. Permissionless exchange() on LLAMMA

    Anyone can dump arbitrary amounts of crvUSD into the AMM, force soft-liquidating all positions. Under normal conditions, this is economically irrational (bad swap rate), but when combined with oracle manipulation, the liquidation profits exceed the swap losses.

  2. Extreme supply concentration in LLAMMA

    87.9% of all sDOLA supply was deposited as collateral in the LLAMMA. This meant the attacker's single exchange() call simultaneously achieved two objectives: forcing all positions into soft-liquidation and acquiring nearly the entire sDOLA supply (9.83M of 11.77M). The attacker then redeemed this sDOLA, collapsing totalSupply from 11.77M to just 1.16M, which made the following 190,777 DOLA stake() donation inflate PPS by 13.79% instead of ~1.6%. If the bulk of sDOLA supply had been held elsewhere, the attacker would have needed separate transactions to accumulate enough shares- significantly increasing cost and complexity.

  3. Manipulable convertToAssets()

    DolaSavings.stake(amount, recipient) is permissionless. Anyone can stake DOLA on behalf of the sDOLA contract, inflating the share price without minting new shares. The sDOLA contract itself warns: "Any protocol in which sudden, large, and atomic increases in the value of an asset may be a security risk should not integrate this vault."

  4. No oracle smoothing on vault PPS

    The CryptoFromPoolVaultWAgg Oracle applies an EMA to the pool price_oracle() but reads convertToAssets() instantaneously. A single-block manipulation passes through directly.

No single factor alone is sufficient. The attack requires all four.

#Takeaways

The sDOLA-long2 market was deployed before oracle proxies were introduced to LlamaLend, meaning the oracle can not be updated. Therefore, the market is being deprecated. It has been removed from the UI, and the borrow rates are being increased to deter any users from borrowing in the market.

A key takeaway is that LlamaLend price oracles should not have the potential for instantaneous price jumps for any reason, including the path involved here by donating to an ERC-4626 vault. Smoothing is required and should be standardized across all LlamaLend oracles, regardless of the collateral's technical design. Swiss Stake is developing an oracle to be used across all LlamaLend v2 markets, which will prevent incidents like this in the future.

#2. Attack Overview

#Exploit Information

TX 0xb93506af8f1a39f6a31e2d34f5f6a262c2799fef6e338640f42ab8737ed3d8a4
Block 24,566,937 (March 2, 2026 ~04:00 UTC)
Attacker 0x33A0aAb2642c78729873786e5903CC30F9a94be2 via contract 0xd8E8544E0c808641b9b89dfB285b5655BD5B6982
Flash Loan 10M USDC + 15,986 WETH (~$46M from Morpho)
Market sDOLA/crvUSD (Controller: 0xaD444663c6C92B497225c6cE65feE2E7F78BFb86, AMM: 0x0079885E248B572CdC4559A8B156745e2d8EA1f7)
Exploiter Profit ~$240K
Liquidator Profit* 208,250K DAI

*The exploiter left a borrow position open at the end of the sequence, which was liquidated several hours later. This is the profit from liquidating the exploiter's loan.

#Exploit Flow

  1. Flash borrow $46M from Morpho
  2. Convert to crvUSD via multiple routes (FRAX, alUSD, WETH collateral)
  3. Exchange 13.25M crvUSD into LLAMMA and buy 9.83M sDOLA, traversing all bands
  4. Redeem 10.6M sDOLA → 12.6M DOLA from vault
  5. **DolaSavings.stake(190K DOLA, sDOLA_address)** to inflate the oracle
  6. Hard-liquidate 27 positions with 10.9M crvUSD
  7. Open a new leveraged position using seized collateral, borrow 10.9M crvUSD
  8. Settlement processes, repay flash loan

#Impact

Category Detail
Affected Addresses 27 borrowers, all hard-liquidated (see appendix for full list)
sDOLA holders +13.79% (sDOLA donation increased vault rate from 1.189 to 1.353)
Attacker profit ~$245K net (6.74 WETH + 227K DOLA)
Liquidated collateral value 11,729,240 crvUSD
Debt repaid 10,906,770 crvUSD
Borrower losses ≈ 822,475 crvUSD

#3. Attack Mechanism

#Step 1: Exchange on LLAMMA (Force Soft-Liquidation)

The attacker calls AMM.exchange(0, 1, 13.25M, 0) — selling 13.25M crvUSD into the AMM for 9.83M sDOLA. This traverses all occupied bands, converting borrowers' sDOLA collateral into crvUSD within the bands.

exchange() is permissionless with no oracle guard (AMM.vy L993). The only check is slippage (min_amount), which the attacker sets to 0. Under normal conditions, no rational actor would push the price far from the oracle because the swap is unprofitable. However, the exploiter incurs the initial expense to profit from subsequent liquidations.

Exchange alone does not make any position liquidatable. Health actually improves for most positions because the AMM now holds more crvUSD. The purpose of this step is to set up the precondition: all positions must be in soft-liquidation for Step 2 to be effective.

#Step 2: Oracle Inflation via stake()

The exploiter redeemed 10.6M sDOLA, which burned those shares and collapsed totalSupply from 11.77M to just 1.16M**.** Pre-attack, 87.9% of all sDOLA supply was deposited as collateral in the LLAMMA- the attacker acquired and redeemed nearly all of it.

The sDOLA vault (0xb45ad160634c528Cc3D2926d9807104FA3157305) computes totalAssets() as:


function totalAssets() public view override returns (uint) {
    uint actualAssets = savings.balanceOf(address(this)) - remainingLastRevenue - weeklyRevenue[week];
    return actualAssets;
}

Where savings.balanceOf() is an internal accounting variable in DolaSavings (0xE5f24791E273Cb96A1f8E5B67Bc2397F0AD9B8B4). It only changes via stake() and unstake().

The attacker calls DolaSavings.stake(190_777e18, sDOLA_address). This:

  • Increases savings.balanceOf(sDOLA) by 190K
  • Does not mint any sDOLA shares
  • Therefore inflates convertToAssets() = totalAssets / totalSupply
  • This inflated the sDOLA rate from 1.189 to 1.353, a 13.79% increase

With only 1.16M shares remaining, the 190,777 DOLA donation inflates convertToAssets() by ~13.79%, rather than the ~1.6% it would have caused against the original 11.77M supply.

Note: a raw DOLA transfer to DolaSavings does nothing (it wouldn't update the internal *balanceOf* mapping). The attacker must call *stake()* with sDOLA as the recipient.

#Combined Effect

Oracle inflation alone does not make any position liquidatable. When positions are not soft-liquidated, higher oracle = higher collateral value = better health. The oracle manipulation only becomes lethal after Step 1 has placed all positions into soft-liquidation.

When both steps execute in sequence, the liquidation sweep becomes possible. After the exchange, all bands hold crvUSD (soft-liquidated). When the oracle then jumps up, the health formula's round-trip valuation collapses due to the cubic oracle sensitivity (see Section 4). Just 190K DOLA (~1.3% of vault TVL) is sufficient to push the majority of positions below zero health.

#Post-Exploit Sequence

The exploiter finished the transaction with a 10.9M crvUSD loan left open in the sDOLA market (opened in step 7 above). This loan was closed through a three-phase sequence over the following ~4 hours, strongly suggesting a pre-planned multi-phase operation by the same actor.

Phase 2 — Liquidation of exploiter's loan (~3.4 hours later)

Field Value
TX 0xb484224883464bdb1a998508009b95059ca22608962755bd9ebaf3f70286c7a9
Block 24,567,962
From 0x7036CBA312802C8Ae5A4fA262c0dC375CFEf4d3f via contract 0x2e006F999Ea578Dd6ed969a3E9a5Ca92278525c3
Flash Loans 10M USDC + 15,750.19 WETH + 25M crvUSD (Morpho Blue)

The liquidator used the exact same bespoke oracle manipulation technique as the original exploit: dumped crvUSD into LLAMMA to acquire sDOLA, redeemed sDOLA to drain supply, donated DOLA via stake() to inflate PPS, then hard-liquidated the exploiter's 10.9M crvUSD loan. This is not a standard MEV liquidation — it requires deep understanding of the exploit mechanics.

After liquidating the exploiter's position, the liquidator opened a new loan: 834,242 sDOLA collateral / 913,688 crvUSD debt. All flash loans were repaid within the same transaction.

Phase 3 — Loan repayment (~25 min after Phase 2)

The liquidator repaid the 913,688 crvUSD loan in 9 transactions over ~25 minutes (blocks 24,567,989 → 24,568,112), withdrawing the remaining sDOLA collateral:

Phase 4 — Cashout

Through subsequent transactions from the liquidator EOA (61 total txs), the profit was converted to 208,250 DAI and sent to 0xCfb279DD00B3b684Ce3Ab536b37622e465A4f9dc, where they remain as of this publication.

Same actor assessment: The liquidator EOA 0x7036CBA312802C8Ae5A4fA262c0dC375CFEf4d3f was a fresh wallet (first tx ~3 hours after exploit), deployed a single-use contract with hardcoded sDOLA addresses, and used the identical bespoke oracle manipulation technique. While the wallet was funded from an active MEV wallet (not directly from the exploiter), this is consistent with operational security. The evidence strongly suggests the same actor or someone with direct access to the exploit code.

#4. Oracle Architecture

The sDOLA-long2 lend market uses a CryptoFromPoolVaultWAgg oracle (0x88822eE517Bfe9A1b97bf200b0b6D3F356488fF2):

price=VAULT.convertToAssets(1018)×AGG.price()POOL.price_oracle()\text{price} = \frac{\text{VAULT.convertToAssets}(10^{18}) \times \text{AGG.price()}}{\text{POOL.price\_oracle()}}

Where:

  • VAULT = sDOLA (0xb45ad160634c528Cc3D2926d9807104FA3157305) — ERC4626 wrapper around DolaSavings
  • POOL = DOLA/crvUSD StableSwap (0xff17dAb22F1E61078aBa2623c89cE6110E878B3c) — provides DOLA/crvUSD EMA price
  • AGG = crvUSD aggregated price oracle (0x18672b1b0c623a30089A280Ed9256379fb0E4E62)

The pool price_oracle() uses an internal EMA (smoothed). But convertToAssets() is read instantaneously with no smoothing applied. This is the vulnerability surface.

The oracle contract's own source code warns:

"Only suitable for vaults which cannot be affected by donation attack (like sFRAX)"

#Why Oracle UP = Health DOWN

This is counterintuitive: the oracle says collateral is worth more, yet positions become unhealthy. The explanation lies in the LLAMMA health formula.

Health is computed as:

health = get_x_down(user) / debt - 1 + liquidation_discount

get_x_down() answers: "How much crvUSD would this user's bands be worth if we adiabatically converted everything back?" It calls get_xy_up(user, False) in AMM.vy L1286.

For bands where p_o > p_o_up (oracle is above the band, i.e., the band is fully soft-liquidated to crvUSD), The conversion is:

p_current_mid = p_o**2 // p_o_down * p_o // p_o_up   # = p_o³ / (p_o_up * p_o_down)
y_equiv = x * 10**18 // p_current_mid

XY += y_equiv * p_o_up // SQRT_BAND_RATIO

Which simplifies to:

xdown=xpo,up2po,downpo3band_ratiox_{\text{down}} = x \cdot \frac{p_{o,\text{up}}^2 \cdot p_{o,\text{down}}}{p_o^3 \cdot \sqrt{\text{band\_ratio}}}

p_o is in the denominator, cubed. A 1.3% oracle increase translates to a ~4% decrease in x_down, which reduces health by ~4 percentage points. For positions with only 0.3–0.8% health margin, this is instant liquidation.

The intuition: soft-liquidated bands hold crvUSD. To value them, the AMM asks, "What would this crvUSD buy if we converted back to sDOLA at the current oracle?" A higher oracle means sDOLA is more expensive, so the round trip would yield less sDOLA back, resulting in a lower valuation.

#5. Borrower Losses & Funds Flow

#5.1 Borrower Losses

How hard liquidation works in LlamaLend: When a position's health drops below zero, any third party can call Controller.liquidate(). The liquidator repays the borrower's entire debt and, in return, seizes all collateral held in the borrower's LLAMMA bands — both the remaining sDOLA and any crvUSD from soft-liquidation. There is no separate "liquidation bonus" parameter; the liquidator's profit is simply the difference between the value of seized collateral and the debt repaid. This means the borrower's equity (collateral value − debt) is what the liquidator captures.

Although the market held ~$11.7M in notional collateral, that amount was not at risk. The market's low loan discount (1.3%) allowed positions to operate at up to ~77x effective leverage, though actual leverage varied widely. Most borrowers had borrowed close to their maximum, leaving only a thin equity buffer.

Methodology: For each of the 27 liquidated positions, we snapshot the pre-attack state (block 24,566,936) by reading each borrower's collateral (sDOLA + crvUSD in bands) and debt from the Controller. We convert sDOLA collateral to crvUSD-equivalent using the pre-attack convertToAssets() rate (1.189), which represents the actual redemption value of sDOLA at that block. The borrower's loss is defined as:

loss=collateral value (crvUSD)−debt (crvUSD)

This represents the equity each borrower forfeited upon liquidation. The aggregate loss rate (total loss ÷ total collateral value) is 7.0%, reflecting the generally thin equity margins across positions.

Metric Value
Positions liquidated 27 of 30
Total collateral value ~$11.73M (crvUSD equivalent)
Total debt repaid ~$10.9M crvUSD
Total borrower loss (equity seized) ≈ 822,475 crvUSD
Aggregate loss rate 7.0% of collateral value
Top 3 positions' share of losses ~80%

#5.2 Funds Flow

The ~$822K in borrower equity seized through liquidations was the attacker's gross revenue. However, executing the attack required significant upfront costs, reducing net profit to ~$245K. Additional value was subsequently extracted from the liquidation of the exploiter's loan, amounting to 208K DAI.

Attacker balance verification (pre/post attack):

Pre-attack Post-attack Delta
Attack contract WETH 0 6.7368 +6.7368
Attack contract DOLA 0 227,325.57 +227,325.57

No other addresses received funds from the attack contract. The entire profit remained in the attack contract (0xd8E8...6982).

Flow Amount Direction
Borrower equity seized via liquidations ~$822K → Attacker (gross revenue)
Attack execution costs (LLAMMA swap losses, 191K DOLA donated via stake(), flash loan fees, gas, routing) ~$369K ← Attack cost
Attacker net profit ~$245K 6.74 WETH + 227,326 DOLA − 0.024 ETH gas
Liquidator profit (exploiter loan liquidation) ~$208K 208,250 DAI

See the appendix below for a list of all affected addresses.

#6. Recommendations

Oracle:

  • Apply EMA/TWAP smoothing to convertToAssets() reads in LlamaLend oracles, matching the smoothing already applied to pool.price_oracle()
  • Add a per-block rate-of-change cap on vault PPS readings

We are conducting further verification across all vault-collateral markets and working with the Curve team on oracle-level mitigations to ensure this class of attack cannot be repeated against any future market.

#Appendix: Liquidated Addresses

The values in the table below are the user position data in the block just before the liquidation event and the estimated per-user loss following liquidation. Loss is calculated as the collateral value minus the debt, using the sDOLA exchange rate of 1.189042662706965487.

Borrower Collateral (sDOLA) Debt (crvUSD) Health % Collateral value (crvUSD) Estimated loss (crvUSD)
0x2b083a0aa6b808a31e9ac749772a285f5cd34fbe 1,671 1,639 0.539 1,987 348
0xcbcc2b2ecd195ebef03fcb7c7564e4e906485a14 748,854 853,124 0.318 890,420 37,296
0xba5aa2a3dbbbb4c7c3a8950fc6251bb8020cf844 117,078 132,280 1.160 139,211 6,931
0x145e305a6e8979cbefcb75993f7ae5270856c1d2 192 206 0.605 228 22
0xf60de76791c2f09995df52aa1c6e2e7dcf1e75d7 222 252 0.981 264 13
0xe9c0df9bd4607850d410c957fec11ec209de5ef6 2,128 2,409 0.993 2,531 122
0x8db98764ada29b55a23f7a8cb07be6f74f0d0e75 0.008 0.009 0.865 0.0096 0.0004
0xc6c77b16a85c3946e0bdfc71fdb7efd3d89359d0 222,232 250,689 0.958 264,243 13,554
0x5b860e2d38f723d5370cf21f82d6adad31ef0b7d 3,543 4,054 0.592 4,213 159
0xb152fc7e9ddf01a942685e390a74009cd2b9ca52 3,172,023 3,572,521 0.769 3,771,671 199,150
0xe170ed9d77792397271d564c7161351d69fe9300 157 171 0.544 187 16
0x8fc5777d607171b42a61fed4c74242e54677903f 371,141 420,605 0.492 441,303 20,697
0x80c67fe70d7d6cc488782439fad381d8646640c4 1,809,065 1,862,484 0.602 2,151,056 288,572
0x9bf8af305152faddd81c70f8599148e9fc6efa20 84.7 80.3 0.575 101 20
0x21ab0875611da0235bc5b6405b8a08268d859700 1,762 1,910 0.368 2,095 185
0x6ce50491faa9fac1dc883a2769ab129e75eb0a75 36,508 40,042 0.603 43,409 3,367
0xc69f65d2720df32c244163e0f608284415aaef4b 3,877 4,409 1.551 4,609 200
0x6db248100cf4908429ab671f33d105311ed7fef8 2,269,436 2,528,047 0.453 2,698,456 170,409
0xc8233a46f57add754f32cf9e25a85aae8a7d5f29 38,408 35,046 0.497 45,669 10,623
0xc8801ffaaa9dfcce7299e7b4eb616741ea01f5de 523,742 587,312 0.852 622,752 35,440
0x57f845829140d9d9d8e357fa0d9f943483a12fc5 352,469 400,103 0.325 419,100 18,997
0xd4ffcd8b6b7ec90f4eac001125f4a7b21dc0f781 90.0 101.0 0.765 107 6
0x3e258aae11d7ea394b2eb1176ccd54d9eb83861b 29,369 32,230 0.548 34,921 2,691
0x6ef36f7130d00addf40ce9b040da0bc02491d2e1 4,563 5,099 0.492 5,426 327
0x2d57740ee18594bcbfa845703fad49882e1567d9 31,242 35,910 0.306 37,148 1,238
0xadbafae28c3041ecb74456cd7fb9097bd1287308 76,345 80,543 0.395 90,778 10,235
0x8467241838bc761d9ef4f8ae6790ede292fba2f9 48,240 55,503 0.383 57,359 1,857
LlamaLend sDOLA-long2 Post-mortem | LlamaRisk Research | LlamaRisk