DEV Community

ohmygod
ohmygod

Posted on

The Makina Finance Flash Loan Exploit: How $280M in Borrowed USDC Drained a Curve Pool in One Transaction

TL;DR

On January 20, 2026, Makina Finance lost ~$4M when an attacker borrowed $280M via flash loans, manipulated external Curve pool balances to inflate Makina's internal AUM calculations, and drained the DUSD/USDC pool—all in a single transaction. The root cause: trusting external pool state as a price oracle without flash-loan resistance.


The Attack Vector: External Pool State as an Implicit Oracle

Most developers know not to use slot0() for price feeds. Fewer realize that calling calc_withdraw_one_coin() on an external Curve pool is functionally the same thing—a spot-price oracle masquerading as a utility function.

Makina's Caliber contract computed its Assets Under Management (AUM) by querying external Curve pools:

// Simplified vulnerable pattern
function computeAUM() internal view returns (uint256) {
    // Reading external pool balances as "truth"
    uint256 curveBalance = ICurvePool(EXTERNAL_POOL).calc_withdraw_one_coin(
        lpTokenBalance, 
        USDC_INDEX
    );
    return curveBalance + internalReserves;
}
Enter fullscreen mode Exit fullscreen mode

This pattern treats the current state of an external liquidity pool as ground truth. In normal conditions, it works. Under flash loan manipulation, it's a loaded gun.

Step-by-Step Reconstruction

Phase 1: Capital Assembly

The attacker borrowed ~$280M USDC across two flash loan sources:

Source Amount
Morpho ~160.59M USDC
Aave V2 ~119.41M USDC

Spreading across multiple sources reduced the chance of hitting individual pool limits.

Phase 2: Position Setup

  • Added 100M USDC to Makina's DUSD/USDC pool → received LP tokens
  • Swapped 10M USDC → ~9.215M DUSD

This established the attacker's position inside Makina's ecosystem before the manipulation began.

Phase 3: External Pool Manipulation (The Kill Shot)

This is where it gets interesting. The attacker didn't manipulate Makina's pools directly for the price distortion—they manipulated the external Curve pools that Makina's accounting read from:

  1. Added 170M USDC to Curve's DAI/USDC/USDT (3pool)
  2. Added 30M liquidity to Curve MIM-3LP3CRV-f pool
  3. Partially withdrew from MIM pool to receive MIM tokens

These operations temporarily bloated the external pool balances. When Makina's accountForPosition() function called calc_withdraw_one_coin() on these pools, it returned inflated values because the pool now had massively skewed reserves.

Phase 4: AUM Inflation → SharePrice Manipulation

With external pool balances artificially inflated:

sharePrice = totalAUM / totalShares

Before manipulation: sharePrice ≈ 1.01
After manipulation:  sharePrice ≈ 1.33  (↑ 31.7%)
Enter fullscreen mode Exit fullscreen mode

A 31.7% increase in share price within a single transaction. No legitimate market movement could produce this.

Phase 5: Profit Extraction

The inflated sharePrice let the attacker withdraw more USDC per LP token than they deposited. The arbitrage loop drained the majority of USDC reserves from the DUSD/USDC pool.

Phase 6: Cleanup

  • Repaid both flash loans (~$280M)
  • Converted profits to ~1,299 ETH (~$4M)
  • Transferred to external addresses

Plot twist: An MEV bot front-ran part of the attack, capturing ~276 ETH. Even exploiters get exploited.

The Root Cause: Trust Boundaries

The vulnerability wasn't a bug in the traditional sense—no integer overflow, no reentrancy. It was an architectural trust assumption: treating the current state of external pools as reliable input for accounting.

┌─────────────────────────────────────────────┐
│              TRUST BOUNDARY                  │
│                                              │
│  Makina's Internal State  ←  TRUSTED         │
│       ↕                                      │
│  External Curve Pool State ←  UNTRUSTED      │
│  (but treated as trusted)                    │
│       ↕                                      │
│  Flash Loan Manipulation  ←  ADVERSARIAL     │
└─────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Any on-chain value that can be atomically changed within the same transaction is, by definition, manipulable by flash loans.

Defense Patterns That Actually Work

1. TWAP Oracles (Time-Weighted Average Prices)

Instead of reading spot values, use time-weighted averages that span multiple blocks:

// Resistant to single-transaction manipulation
function getPrice() external view returns (uint256) {
    (int56[] memory tickCumulatives, ) = pool.observe(
        [uint32(TWAP_WINDOW), 0]  // e.g., 30 minutes
    );
    int56 tickDelta = tickCumulatives[1] - tickCumulatives[0];
    return computePriceFromTick(tickDelta / int56(uint56(TWAP_WINDOW)));
}
Enter fullscreen mode Exit fullscreen mode

Flash loans exist within a single transaction (single block). A TWAP spanning 30+ minutes is immune to single-block manipulation.

2. Multi-Source Oracle Aggregation

Don't rely on a single price source. Cross-reference:

function validatePrice(uint256 price) internal view returns (bool) {
    uint256 chainlinkPrice = getChainlinkPrice();
    uint256 twapPrice = getTWAPPrice();

    uint256 maxDeviation = 200; // 2% in basis points

    require(
        withinDeviation(price, chainlinkPrice, maxDeviation) &&
        withinDeviation(price, twapPrice, maxDeviation),
        "Price deviation too high"
    );
    return true;
}
Enter fullscreen mode Exit fullscreen mode

3. Flash-Loan Detection Guards

Block flash-loan-powered calls to sensitive functions:

modifier noFlashLoan() {
    require(
        block.number > lastInteractionBlock[msg.sender],
        "Same-block interaction prohibited"
    );
    lastInteractionBlock[msg.sender] = block.number;
    _;
}
Enter fullscreen mode Exit fullscreen mode

This forces at least one block to pass between deposit and withdrawal, breaking the atomic execution that flash loans require.

4. Rate-Limited AUM Updates

Instead of computing AUM on-the-fly from external state, use a delayed update pattern:

uint256 public lastAUM;
uint256 public lastAUMUpdate;
uint256 public constant AUM_UPDATE_DELAY = 1 hours;

function updateAUM() external {
    require(
        block.timestamp >= lastAUMUpdate + AUM_UPDATE_DELAY,
        "AUM update cooldown"
    );
    lastAUM = computeAUM();
    lastAUMUpdate = block.timestamp;
}

function getSharePrice() public view returns (uint256) {
    return lastAUM / totalShares; // Uses cached, not live data
}
Enter fullscreen mode Exit fullscreen mode

5. Withdrawal Caps and Circuit Breakers

Limit the damage surface even if manipulation succeeds:

modifier withdrawalCap(uint256 amount) {
    uint256 maxWithdraw = totalReserves * MAX_WITHDRAW_PCT / 10000;
    require(amount <= maxWithdraw, "Exceeds withdrawal cap");
    _;
}
Enter fullscreen mode Exit fullscreen mode

A Broader Pattern: The Composability Tax

DeFi's strength—permissionless composability—is also its attack surface. Every external call is a potential trust boundary violation. The Makina exploit joins a growing list of attacks that weaponize composability:

Protocol Date Loss External Dependency Exploited
Makina Finance Jan 2026 $4M Curve pool balances
Resupply Jun 2025 $9.6M Unverified pool collateral pricing
New Gold Sep 2025 $2M Price oracle + transfer logic
KiloEx Mar 2025 $7M Price feed manipulation

The pattern is consistent: protocol A trusts protocol B's current state, attacker manipulates protocol B's state within the same transaction.

Checklist for Developers

Before shipping any contract that reads external state:

  • [ ] Identify all external reads — map every view call to external contracts
  • [ ] Classify each as spot or delayed — spot values are manipulable
  • [ ] Replace spot reads with TWAPs where price/value data is involved
  • [ ] Add cross-source validation — never trust a single oracle
  • [ ] Implement same-block interaction guards on sensitive functions
  • [ ] Add circuit breakers — pause on anomalous AUM/price changes
  • [ ] Rate-limit AUM recalculations — don't compute from live state on every call
  • [ ] Fuzz test with flash loan scenarios — simulate atomic manipulation in tests
  • [ ] Get audited — specifically ask auditors to test flash loan vectors

Final Thought

The Makina exploit wasn't sophisticated in concept—it's the same "manipulate oracle → extract value" pattern we've seen since 2020. What's changed is the scale ($280M in borrowed capital) and the indirection (manipulating external pools that the target protocol reads from).

Every view function call to an external contract is an implicit oracle. Treat it accordingly.


This analysis is based on publicly available post-mortem reports and on-chain data. It is provided for educational purposes to improve DeFi security practices.

Top comments (0)