All incidents

MahaLend Liquidity Index Inflation

Share
Nov 10, 2023 22:43 UTCAttackLoss: 10,001.47 ARTHPending manual check1 exploit txWindow: Atomic
Estimated Impact
10,001.47 ARTH
Label
Attack
Exploit Tx
1
Addresses
2
Attack Window
Atomic
Nov 10, 2023 22:43 UTC → Nov 10, 2023 22:43 UTC

Exploit Transactions

TX 1Ethereum
0x2881e839d4d562fad5356183e4f6a9d427ba6f475614ce8ef64dbfe557a4a2cc
Nov 10, 2023 22:43 UTCExplorer

Victim Addresses

0x76f0c94ced5b48020bf0d7f3d0ceabc877744cb5Ethereum
0xe6b683868d1c168da88cfe5081e34d9d80e4d1a6Ethereum

Loss Breakdown

10,001.47ARTH

Similar Incidents

Root Cause Analysis

MahaLend Liquidity Index Inflation

1. Incident Overview TL;DR

At Ethereum mainnet block 18544605, an unprivileged attacker used Balancer flash liquidity and public MahaLend entrypoints to drain the full ARTH reserve in transaction 0x2881e839d4d562fad5356183e4f6a9d427ba6f475614ce8ef64dbfe557a4a2cc. The attacker borrowed 13,923,271.097322 USDC from the Balancer Vault, manipulated the MahaLend USDC reserve accounting inside the same transaction, borrowed all 10,001.466755107579871763 ARTH from MahaLend, repaid Balancer, and kept the ARTH as profit.

The root cause is an accounting flaw in how MahaLend combines flash-loan premium accrual, scaled aToken supply, and collateral valuation. Flash-loan premiums were added to the USDC liquidity index using the current aToken total supply as the denominator, while the attacker could shrink that supply to one unit through public supply, transfer, and withdraw operations. Once the USDC normalized income was inflated from 1e27 to 31894733256000000000000000010721889520, the protocol valued one scaled USDC unit as enough collateral to borrow the entire ARTH reserve.

2. Key Background

MahaLend uses Aave V3-style scaled accounting. A user does not hold a raw underlying balance directly; instead, the reserve stores scaled balances, and a user's visible balance is scaledBalance * normalizedIncome. The same pattern applies at reserve level: real aToken supply is scaledTotalSupply * normalizedIncome.

That accounting is safe only if normalized income tracks genuine economic yield. In MahaLend, flash-loan repayment updates the reserve liquidity index by dividing the lender premium by the current aToken total supply. If that denominator becomes attacker-reducible and near zero, the liquidity index can explode without any corresponding economic gain for honest depositors.

ARTH was a live, borrowable reserve during the incident, and MahaLend's borrow validator reused the same normalized-income path when valuing USDC collateral. That made the inflated USDC index immediately monetizable: once the attacker inflated USDC normalized income, the borrow path treated one scaled USDC unit as highly valuable collateral and allowed a full ARTH reserve borrow.

The relevant public components were:

  • Balancer Vault 0xBA12222222228d8Ba445958a75a0704d566BF2C8 for external USDC flash liquidity.
  • MahaLend Pool 0x76F0C94Ced5B48020bf0D7f3D0CEabC877744cB5 for supply, withdraw, flash-loan, and borrow actions.
  • MahaLend USDC aToken 0x658b0f629B9e3753AA555C189D0cB19C1eD59632.
  • MahaLend ARTH reserve holder 0xE6B683868D1C168Da88cfe5081E34d9D80E4D1a6.

3. Vulnerability Analysis & Root Cause Summary

This is an ATTACK-class ACT incident, not a privileged-access event. The violated invariant is that USDC normalized income must remain bounded by real economic yield and must not let one scaled USDC unit represent materially more collateral than the reserve truly owns. The code-level breakpoint is the flash-loan repayment path, which uses the current aToken total supply as the liquidity-index denominator, and the aToken mint and burn paths, which allow attacker-controlled rounding-sensitive supply reduction.

The MahaLend Pool implementation shows the critical liquidity-index update:

function _handleFlashLoanRepayment(
    DataTypes.ReserveData storage reserve,
    DataTypes.FlashLoanRepaymentParams memory params
) internal {
    uint256 premiumToProtocol = params.totalPremium.percentMul(params.flashLoanPremiumToProtocol);
    uint256 premiumToLP = params.totalPremium - premiumToProtocol;
    DataTypes.ReserveCache memory reserveCache = reserve.cache();
    reserve.updateState(reserveCache);
    reserveCache.nextLiquidityIndex = reserve.cumulateToLiquidityIndex(
        IERC20(reserveCache.aTokenAddress).totalSupply(),
        premiumToLP
    );
}
function cumulateToLiquidityIndex(
    DataTypes.ReserveData storage reserve,
    uint256 totalLiquidity,
    uint256 amount
) internal returns (uint256) {
    uint256 result =
        (amount.wadToRay().rayDiv(totalLiquidity.wadToRay()) + WadRayMath.RAY).rayMul(
            reserve.liquidityIndex
        );
    reserve.liquidityIndex = result.toUint128();
    return result;
}

The attacker paired that with the USDC aToken and scaled-balance rounding behavior:

function totalSupply() public view override returns (uint256) {
    uint256 currentSupplyScaled = super.totalSupply();
    if (currentSupplyScaled == 0) return 0;
    return currentSupplyScaled.rayMul(POOL.getReserveNormalizedIncome(_underlyingAsset));
}

function _mintScaled(address caller, address onBehalfOf, uint256 amount, uint256 index)
    internal
    returns (bool)
{
    uint256 amountScaled = amount.rayDiv(index);
    require(amountScaled != 0, Errors.INVALID_MINT_AMOUNT);
    _mint(onBehalfOf, amountScaled.toUint128());
    return (super.balanceOf(onBehalfOf) == 0);
}

Once the liquidity index was inflated, the borrow-side valuation path trusted it directly:

function _getUserBalanceInBaseCurrency(
    address user,
    DataTypes.ReserveData storage reserve,
    uint256 assetPrice,
    uint256 assetUnit
) private view returns (uint256) {
    uint256 normalizedIncome = reserve.getNormalizedIncome();
    uint256 balance =
        IScaledBalanceToken(reserve.aTokenAddress).scaledBalanceOf(user).rayMul(normalizedIncome)
            * assetPrice;
    return balance / assetUnit;
}

In short, MahaLend assumed the liquidity-index denominator was economically meaningful when it was actually attacker-reducible, trusted the resulting normalized income for collateral valuation, and preserved the exploitability of the inflated state through rounding-sensitive scaled mint and burn operations.

4. Detailed Root Cause Analysis

The ACT opportunity existed in the Ethereum state immediately before transaction 0x2881e839d4d562fad5356183e4f6a9d427ba6f475614ce8ef64dbfe557a4a2cc in block 18544605. The Balancer Vault held enough USDC to fund the attack, MahaLend's USDC reserve was active and flash-loan enabled, and the ARTH reserve still held 10001466755107579871763 raw units.

Stage 1 was external flash funding and reserve-supply collapse. The attacker sender EOA 0x0ec330df28ae6106a774d0add3e540ea8d226e3b invoked orchestration contract 0xf5836e292f716a7979f9bc5c2d3ed59913e07962, which received 13,923,271.097322 USDC from Balancer. The trace then shows a MahaLend USDC supply of 1160272591443, followed by one internal MahaLend USDC flash loan for the same amount, a direct USDC transfer back to the USDC aToken, and a withdraw of 1160272591442. That sequence leaves exactly one USDC aToken unit in circulation.

Representative trace evidence:

emit Supply(... amount: 1160272591443)
emit Withdraw(... amount: 1160272591442)
emit FlashLoan(... amount: 1160272591443, premium: 580136296)

Stage 2 was internal premium compounding against that one-unit supply. After the first internal flash loan collapsed supply, the attacker executed 54 additional internal USDC flash loans. Each one added 580136296 USDC-denominated premium to the reserve while the denominator passed to cumulateToLiquidityIndex remained effectively one unit. The trace shows the USDC liquidity index moving from 1000000000000000000000000000 to 31894733256000000000000000010721889520.

Representative reserve updates:

emit ReserveDataUpdated(... liquidityIndex: 579904242000000000000000000000000000)
...
emit ReserveDataUpdated(... liquidityIndex: 31894733256000000000000000010721889520)

Stage 3 was collateral overvaluation and reserve theft. After index inflation, MahaLend's borrow path valued the attacker's single scaled USDC unit using the inflated normalized income. Under the protocol's own pricing path, that one scaled USDC unit became worth 31,894.733256 USDC-equivalent collateral, enough to satisfy borrow checks for the entire ARTH reserve. The trace then shows:

emit Borrow(
  reserve: 0x8CC0F052fff7eaD7f2EdCCcaC895502E884a8a71,
  amount: 10001466755107579871763,
  interestRateMode: 2
)

Immediately after the borrow, ARTH moved from reserve holder 0xE6B683868D1C168Da88cfe5081E34d9D80E4D1a6 to the orchestration contract and then to profit recipient 0xf60a033d246ae5ee1772989a577adaaebce2366d. The balance diff confirms that the reserve holder's ARTH balance fell from 10001466755107579871763 to 0, while the profit recipient's ARTH balance increased by the same amount.

Stage 4 was Balancer repayment using the same inflated accounting. Helper contract 0x3a393162888dbc7ff850a1b3a56b27daab81267c received the remaining USDC, performed 75 small USDC supplies of 15950556101, then withdrew 2388471795299 USDC. Because _mintScaled only rejects zero rounded scaled amounts, each helper deposit could still mint one full scaled unit at the inflated index. The helper then repaid Balancer in the same transaction, leaving the ARTH theft as the attacker's retained profit.

The exploitable conditions were therefore all public and permissionless:

  • Public external flash liquidity existed on Balancer.
  • MahaLend exposed public supply, withdraw, flash-loan, and borrow entrypoints.
  • The attacker could reduce USDC aToken supply to one unit through public interactions.
  • MahaLend continued accepting flash-loan premium accrual and borrow-side collateral valuation after supply collapse.
  • A monetizable borrowable reserve, ARTH, still held meaningful value.

The security principles violated were:

  • Flash-loan premium distribution assumed a protocol-controlled denominator that was actually attacker-controllable.
  • Collateral valuation trusted normalized-income accounting without bounding it against underlying reserve reality.
  • Rounded scaled mint and burn semantics let tiny nominal deposits become full scaled units after index inflation.

5. Adversary Flow Analysis

The adversary strategy was a single-transaction, Balancer-funded, multi-stage attack using one orchestration contract and one helper contract.

The adversary cluster identified by the evidence is:

  • Sender EOA 0x0ec330df28ae6106a774d0add3e540ea8d226e3b, which submitted the transaction and paid native ETH gas.
  • Orchestration contract 0xf5836e292f716a7979f9bc5c2d3ed59913e07962, which received the Balancer flash loan, interacted with MahaLend, borrowed ARTH, and funded the helper.
  • Helper contract 0x3a393162888dbc7ff850a1b3a56b27daab81267c, which executed the inflated-index USDC supply loop and repaid Balancer.
  • Profit-recipient EOA 0xf60a033d246ae5ee1772989a577adaaebce2366d, which received the stolen ARTH.

The attacker lifecycle is:

  1. 0xf583... receives the Balancer flash loan.
  2. 0xf583... supplies 1160272591443 USDC into MahaLend.
  3. 0xf583... takes one internal MahaLend flash loan, transfers borrowed USDC directly to the USDC aToken, and withdraws amount - 1 to force one-unit aToken supply.
  4. 0xf583... executes 54 more internal USDC flash loans to inflate the liquidity index.
  5. 0xf583... borrows the full ARTH reserve and transfers ARTH to 0xf60a....
  6. 0xf583... transfers remaining USDC to 0x3a39....
  7. 0x3a39... performs 75 small inflated-index USDC supplies, withdraws 2388471795299 USDC, and repays Balancer.

Every step was adversary-crafted. There was no victim-authenticated action, no privileged role, and no dependency on private keys or nonpublic calldata. That satisfies the ACT model.

6. Impact & Losses

The measurable loss was the complete drain of MahaLend's ARTH reserve:

  • Token: ARTH
  • Raw loss amount: 10001466755107579871763
  • Decimals: 18
  • Human amount: 10,001.466755107579871763 ARTH

The profit predicate is directly evidenced. The balance diff shows the profit recipient 0xf60a033d246ae5ee1772989a577adaaebce2366d gaining exactly 10001466755107579871763 ARTH, while the reserve holder lost the same amount. The only observed fee payment by the attacker cluster is native ETH gas paid by the sender EOA, so ARTH-denominated fees were 0.

The affected parties were MahaLend and its ARTH reserve depositors. Balancer was not left with a loss because the external flash loan was repaid in the same transaction.

7. References

  • Seed transaction: 0x2881e839d4d562fad5356183e4f6a9d427ba6f475614ce8ef64dbfe557a4a2cc
  • Seed trace: /workspace/session/artifacts/collector/seed/1/0x2881e839d4d562fad5356183e4f6a9d427ba6f475614ce8ef64dbfe557a4a2cc/trace.cast.log
  • Seed balance diff: /workspace/session/artifacts/collector/seed/1/0x2881e839d4d562fad5356183e4f6a9d427ba6f475614ce8ef64dbfe557a4a2cc/balance_diff.json
  • Seed metadata: /workspace/session/artifacts/collector/seed/1/0x2881e839d4d562fad5356183e4f6a9d427ba6f475614ce8ef64dbfe557a4a2cc/metadata.json
  • MahaLend Pool implementation: https://etherscan.io/address/0xfd11aba71c06061f446ade4eec057179f19c23c4#code
  • MahaLend borrow-logic implementation: https://etherscan.io/address/0x8fad782a749dd851b9579d3000968a81f44d6496#code
  • USDC aToken artifact: /workspace/session/artifacts/collector/seed/1/0xac3418ce48dbd35fe213dcfdfb70f044a47f9b0f/lib/@mahalend/core-v3/contracts/protocol/tokenization/AToken.sol
  • Scaled balance artifact: /workspace/session/artifacts/collector/seed/1/0xac3418ce48dbd35fe213dcfdfb70f044a47f9b0f/lib/@mahalend/core-v3/contracts/protocol/tokenization/base/ScaledBalanceTokenBase.sol