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:
-
Permissionless
exchange()on LLAMMAAnyone 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.
-
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, collapsingtotalSupplyfrom 11.77M to just 1.16M, which made the following 190,777 DOLAstake()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. -
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." -
No oracle smoothing on vault PPS
The
CryptoFromPoolVaultWAggOracle applies an EMA to the poolprice_oracle()but readsconvertToAssets()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
- Flash borrow $46M from Morpho
- Convert to crvUSD via multiple routes (FRAX, alUSD, WETH collateral)
- Exchange 13.25M crvUSD into LLAMMA and buy 9.83M sDOLA, traversing all bands
- Redeem 10.6M sDOLA → 12.6M DOLA from vault
**DolaSavings.stake(190K DOLA, sDOLA_address)**to inflate the oracle- Hard-liquidate 27 positions with 10.9M crvUSD
- Open a new leveraged position using seized collateral, borrow 10.9M crvUSD
- 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:
| Block | Loan Repaid (crvUSD) | TX Hash |
|---|---|---|
| 24,567,989 | 133,161.54 | 0x78accc56e19219e0fd1ecac4e70933484248a851af9b8ae249d82d93539c24b0 |
| 24,568,008 | 150,000.00 | 0x2d8a2b4f77b13cf5c5a4fc911b3392f8f9b2a4ba031d28ed1a045df698d66aa3 |
| 24,568,023 | 40,000.00 | 0x5be6b952ea2d1102b686b4f5fafe914991dddda229c19292a374a02a3a58ac8f |
| 24,568,037 | 60,000.00 | 0xa503aef9a9c11a55dc8a55a0b491bf889258073133487f3f855ecbbb708c4e0e |
| 24,568,048 | 100,000.00 | 0x358330ec663d22d1c9051b0b0db21b37a82f852aa710f7a883adb4c0d82ae828 |
| 24,568,073 | 100,000.00 | 0xf55343b7d1df10fb9d6beda1dbc7c52d95d49973f6463bd51f5dd13e6a4fe113 |
| 24,568,085 | 100,000.00 | 0x33b2292a9a4d141890afa0f8c1d286217b1ca6cd8debe7e89f793207ebb65442 |
| 24,568,096 | 80,000.00 | 0x0af77ac167796f902d3fe1accd7d89145287456c399758424c9476a0043e240d |
| 24,568,112 | 150,530.60 | 0xa72c2aa5203e2b492b866e4cbc31d33d0ba8d2649a47861b5eeef264f8e8e830 |
| TOTAL | 913,692.15 |
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):
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:
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 topool.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 |
