Exploiter (EOA with EIP-7702 delegation): https://etherscan.io/address/0x935bfb495E33f74d2E9735DF1DA66acE442ede48

Execution proxy (delegatecall executor) https://etherscan.io/address/0x454d03b2a1D52F5F7AabA8E352225335a1b724E8#code

Exploit Contract

Contract 0x454d03b2… does not contain any protocol business logic. It is a general-purpose bytecode interpreter/proxy that:

  • executes scripts encoded either in msg.data or stored in storage[keccak256(msg.data)];
  • supports: call / staticcall / delegatecall, ETH transfer, balance reading;
  • has privileged mode for one address — 0x935bfb495E33f74d2E9735DF1DA66acE442ede48;
  • implements coinbase bribe (MEV payment);
  • does not contain named Solidity functions — one entrypoint.

Role Machine (Makina)

Machine — is the central accounting/AUM aggregator of the Makina protocol.

  • accounting core;
  • Total AUM repository;
  • share token issuer/burner; AUM aggregator from other chains (Spoke Caliber);

It is the source of:

  • convertToAssets
  • convertToShares
  • getSharePrice
  • an indirect oracle source for external protocols (including Curve);
  • has permissionless updateTotalAum.

Flashloan — Start of the exploit

Morpho Flashloan

"FlashLoan": {
  "token": "USDC",
  "assets": "160590920349812"
}

USDC has 6 decimals => ≈ 160.6 million USDC

Aave Flashloan (inside of the callback)

Exploiter immediatly execute second flashloan:

Сallback (Aave executeOperation)

None

This is where the entire attack occurs.

You can immediately see the cyclical nature of the function calls, indicating that the exploit is designed to configure parameters that will allow the tokens to be withdrawn.

None

First step— Curve DUSD / USDC

Add liquidity

None
DUSDUSDC.add_liquidity(
  amounts = [100_000_000_000_000, 0],
  min_mint_amount = 0
) => reulted in ≈ 9.9206722150127812419545815e22 LP

Curve recalculates the price

Call swap

DUSDUSDC.exchange(
  i = 0,
  j = 1,
  dx = 10_000_000_000_000,
  min_dy = 0
)

inside Curve:

@internal
@view
def _stored_rates() -> DynArray[uint256, MAX_COINS]:
    for i in range(N_COINS):
        if asset_types[i] == 1 and not rate_oracles[i] == 0:
            oracle_response = raw_call(rate_oracles[i], method_id)
            fetched_rate = convert(oracle_response, uint256)
            rates[i] = rates[i] * fetched_rate / PRECISION

We have:

  • asset_types[i] == 1 → Oracle token
  • In our case oracle = MachineShareOracle

MachineShareOracle → getSharePrice

function getSharePrice(
    uint256 aum,
    uint256 supply,
    uint256 shareTokenDecimalsOffset
) public pure returns (uint256) {
    return SHARE_TOKEN_UNIT.mulDiv(
        aum + 1,
        supply + 10 ** shareTokenDecimalsOffset
    );
}

Price depends on:

  • lastTotalAum
  • shareSupply

Next deformation in the Curve 3Pool

Curve.fi DAI/USDC/USDT.add_liquidity(
  [0, 170000000000000, 0]
)

After that several calls to Frax MIM-3LP3CRV-f

None

Add liquidity

MIM-3LP3CRV-f.add_liquidity(
  [0, 30_000_000e18]
)

Remove liquidity one coin

remove_liquidity_one_coin(
  amount = ~15_000_000e18,
  i = 0 or 1
)

Exchange

exchange(
  i = 1,
  j = 0,
  dx = 120_000_000e18
)

CRITICAL POINT— accountForPosition

None
accountForPosition(
  positionId = ...,
  isDebt = false,
  instructionType = 1
)

getSharePrice is used

fixed:

  • new position price
  • new AUM
  • temporary becomes permanent

updateTotalAum — commit фаза

   /// @inheritdoc IMachine
    function updateTotalAum() external override nonReentrant notRecoveryMode returns (uint256) {
        MachineStorage storage $ = _getMachineStorage();

        uint256 _lastTotalAum = MachineUtils.updateTotalAum($, IHubCoreRegistry(registry).oracleRegistry());
        emit TotalAumUpdated(_lastTotalAum);

        uint256 _mintedFees = MachineUtils.manageFees($);
        if (_mintedFees != 0) {
            emit FeesMinted(_mintedFees);
        }

        return _lastTotalAum;
   }

This is a commit function:

  • it commits the effect of the manipulation;

it is used for:

  • the next convertToShares;
  • the next arbitration cycle.

Why does the cycle repeat?

Each cycle:

  • Moves the Curve price slightly
  • Increases the sharePrice slightly
  • Increases the AUM slightly

Next cycle:

  • Starts from an already distorted base
  • Provides an even more favorable ratio

Final

  • All flashloans are closed
  • USDC → WETH via Uniswap V3
  • WETH → ETH
  • ≈ 1,299.04 ETH → MEV builder (coinbase bribe)
  • ≈ 0.13 ETH → exploiter's net profit

Root Cause

  • Doesn't protect updateTotalAum;
  • Doesn't introduce delays/TWAP;
  • Doesn't separate accounting and pricing.