All incidents

Gradient Pool Unit Mismatch Drain

Share
Jun 23, 2025 05:42 UTCAttackLoss: 3.01 ETHPending manual check1 exploit txWindow: Atomic
Estimated Impact
3.01 ETH
Label
Attack
Exploit Tx
1
Addresses
2
Attack Window
Atomic
Jun 23, 2025 05:42 UTC → Jun 23, 2025 05:42 UTC

Exploit Transactions

TX 1Ethereum
0xb5cfa3f86ce9506e2364475dc43c44de444b079d4752edbffcdad7d1654b1f67
Jun 23, 2025 05:42 UTCExplorer

Victim Addresses

0x37ea5f691bce8459c66ffceeb9cf34ffa32fdadcEthereum
0x893d41635725d8ea6f528d3f3f3df3e9e8076934Ethereum

Loss Breakdown

3.01ETH

Similar Incidents

Root Cause Analysis

Gradient Pool Unit Mismatch Drain

1. Incident Overview TL;DR

On Ethereum mainnet block 22765114, transaction 0xb5cfa3f86ce9506e2364475dc43c44de444b079d4752edbffcdad7d1654b1f67 exploited GradientMarketMakerPool at 0x37ea5f691bce8459c66ffceeb9cf34ffa32fdadc. The adversary used a permissionless 3 WETH Morpho flash loan, bought 950 GRAY on the public GRAY/WETH Uniswap V2 market, deposited that GRAY plus 0.632090074270700494 ETH into the pool, and immediately withdrew 3.642989205975327587 ETH plus 946.989100868295372906 GRAY before repaying the flash loan. The pool lost 3010899131704627093 wei of ETH in the transaction.

The root cause is a deterministic accounting bug. GradientMarketMakerPool treats raw GRAY token units and raw ETH wei as if they were interchangeable by summing tokenAmount + msg.value when minting LP shares and by summing mm.tokenAmount + mm.ethAmount when accruing rewards. Because GRAY uses 18 decimals, the arithmetic compiles cleanly but represents heterogeneous assets. The attacker used cheap token units to mint outsized LP shares and then redeemed those shares for almost all ETH tracked by the pool.

2. Key Background

GradientMarketMakerPool is an ETH-and-token liquidity pool keyed by token address. For each token market, the contract stores totalEth, totalToken, totalLiquidity, totalLPShares, and reward-accounting state. The market relevant to this incident uses GRAY at 0xa776a95223c500e81cb0937b291140ff550ac3e4 and the public GRAY/WETH Uniswap V2 pair at 0x0846f55387ab118b4e59eee479f1a3e8ea4905ec.

The pool enforces deposit ratio checks against public Uniswap reserves, but it does not normalize token-side and ETH-side value into a common unit before updating its own accounting. At the end of block 22765113, the collected pre-state snapshot shows:

getPoolInfo@22765113
(3022481813096655000, 0, 3022481813096655000, 2249676575000000000000, ..., 5328686345838292, 0x0846F55387ab118B4E59eee479f1a3e8eA4905EC)

getReserves@22765113
172690952204762289631
261545090101530069672838

This state matters because the pool already tracked about 3.022481813096655000 ETH and reward ETH, while totalLPShares was 2249676575000000000000. That large LP-share supply versus small ETH-side tracked liquidity made the mixed-unit share formula highly exploitable once a user deposited a large raw token amount.

3. Vulnerability Analysis & Root Cause Summary

The vulnerability is a unit-consistency failure in core pool accounting. In provideLiquidity, the contract computes totalContribution = tokenAmount + msg.value, then uses that mixed number to mint LP shares against pool.totalLiquidity. It also updates pool.totalLiquidity += tokenAmount + msg.value, again treating raw token units and wei as one homogeneous measure.

The same flaw affects rewards. Before and after liquidity changes, the contract computes user liquidity as mm.tokenAmount + mm.ethAmount and uses that value for pendingReward and rewardDebt. This means reward claims are also based on heterogeneous units instead of normalized economic value.

The invariant that should hold is straightforward: LP shares and reward claims must be proportional to normalized contributed value, not to the raw sum of two different asset unit systems. The concrete breakpoint is inside GradientMarketMakerPool::provideLiquidity and GradientMarketMakerPool::withdrawLiquidity, where the mixed-unit sums directly drive LP-share minting, reward accrual, and withdrawal entitlements. Once the attacker satisfied the public ratio check using Uniswap reserves, the pool’s own accounting deterministically over-minted LP shares and enabled an oversized ETH withdrawal.

4. Detailed Root Cause Analysis

The verified victim code contains the faulty accounting:

uint256 userLiquidity = mm.tokenAmount + mm.ethAmount;
...
uint256 totalContribution = tokenAmount + msg.value;
lpSharesToMint = (totalContribution * pool.totalLPShares) / pool.totalLiquidity;
...
mm.rewardDebt = ((mm.tokenAmount + mm.ethAmount) * pool.accRewardPerShare) / SCALE;
pool.totalLiquidity += tokenAmount + msg.value;

On full withdrawal, the pool then converts those over-minted shares into actual pool balances:

uint256 lpSharesToBurn = (mm.lpShares * shares) / 10000;
uint256 actualTokenWithdraw = (pool.totalToken * lpSharesToBurn) / pool.totalLPShares;
uint256 actualEthWithdraw = (pool.totalEth * lpSharesToBurn) / pool.totalLPShares;

The exploit trace shows the attacker reaching the deposit stage with exactly 950 GRAY, then calling the victim pool with 632090074270700494 wei:

0x37Ea5f691bCe8459C66fFceeb9cf34ffa32fdadC::provideLiquidity{value: 632090074270700494}(
  0xa776A95223C500E81Cb0937B291140fF550ac3E4,
  950000000000000000000,
  0
)
...
0x37Ea5f691bCe8459C66fFceeb9cf34ffa32fdadC::withdrawLiquidity(
  0xa776A95223C500E81Cb0937B291140fF550ac3E4,
  10000
)
emit LiquidityWithdrawn(
  ...,
  946989100868295372906,
  3642989205975327587,
  707569102721011712333070
)

This is the deterministic exploit path. The attacker acquired 950 GRAY for only 0.664800798545598825 WETH on the public Uniswap market, but the victim pool treated 950e18 GRAY units plus 0.632090074270700494e18 wei as one large liquidity number. Against a pool with only about 3.022e18 tracked ETH-side liquidity but 2.249e21 LP shares outstanding, that mixed-unit contribution minted 707569102721011712333070 LP shares, giving the attacker a dominant claim on real ETH held by the contract.

The balance-diff artifact confirms the loss and the residual token behavior:

{
  "address": "0x37ea5f691bce8459c66ffceeb9cf34ffa32fdadc",
  "delta_wei": "-3010899131704627093"
}
{
  "token": "0xa776a95223c500e81cb0937b291140ff550ac3e4",
  "holder": "0x37ea5f691bce8459c66ffceeb9cf34ffa32fdadc",
  "delta": "3010899131704627094"
}

The ETH loss equals the value drained from the pool. The leftover GRAY inside the pool is a side effect of the same distorted accounting: the attacker’s share claim was calculated from mixed units, but redemption consumes actual tracked pool balances.

5. Adversary Flow Analysis

The adversary cluster consists of EOA 0x1234567a98230550894bf93e2346a8bc5c3b36e3 and helper contract 0xcb4059bb021f4cf9d90267b7961125210cedb792, which was created and used inside the exploit transaction. The end-to-end flow is visible in the seed trace:

  1. The EOA deploys the helper contract and starts the exploit transaction.
  2. The helper calls Morpho at 0xbbbbbbbbbb9cc5e90e3b3af64bdaf62c37eeffcb for a permissionless 3 WETH flash loan.
  3. The helper unwraps 1 WETH to ETH and swaps 0.664800798545598825 WETH on the public Uniswap router at 0x7a250d5630b4cf539739df2c5dacb4c659f2488d to receive 950 GRAY.
  4. The helper approves the victim pool, computes the ETH amount needed to satisfy the pool’s reserve-ratio check from public pair reserves, and deposits 950 GRAY plus 0.632090074270700494 ETH through provideLiquidity.
  5. The helper immediately calls withdrawLiquidity(..., 10000) to burn all minted LP shares and withdraw 3.642989205975327587 ETH plus 946.989100868295372906 GRAY.
  6. The helper wraps its ETH back into WETH, repays the 3 WETH flash loan, and transfers the remaining 2.346098333159028268 WETH profit to the originating EOA.

The critical decision point is step 4. The attacker does not need privileged access or hidden state; the only requirement is to choose a token amount and ETH amount that satisfy the public Uniswap ratio check. Once that condition is met, the victim’s mixed-unit accounting guarantees share inflation.

6. Impact & Losses

The measurable loss is 3010899131704627093 wei of native ETH from GradientMarketMakerPool. The pool also paid out accumulated reward ETH through the same flawed mixed-unit reward basis during the full withdrawal path.

The affected victim protocol component is:

  • GradientMarketMakerPool at 0x37ea5f691bce8459c66ffceeb9cf34ffa32fdadc

The relevant loss record is:

[
  {
    "token_symbol": "ETH",
    "amount": "3010899131704627093",
    "decimal": 18
  }
]

The exploit is an ACT opportunity because any unprivileged actor could reproduce the same sequence from the public pre-state using public liquidity, public contract interfaces, and a permissionless flash-loan provider.

7. References

  1. Seed exploit transaction: 0xb5cfa3f86ce9506e2364475dc43c44de444b079d4752edbffcdad7d1654b1f67
  2. Victim contract: GradientMarketMakerPool at 0x37ea5f691bce8459c66ffceeb9cf34ffa32fdadc
  3. Victim registry: GradientRegistry at 0x893d41635725d8ea6f528d3f3f3df3e9e8076934
  4. Public GRAY token: 0xa776a95223c500e81cb0937b291140ff550ac3e4
  5. Public GRAY/WETH Uniswap V2 pair: 0x0846f55387ab118b4e59eee479f1a3e8ea4905ec
  6. Verified victim source showing the bug: provideLiquidity and withdrawLiquidity in the collected GradientMarketMakerPool.sol
  7. Pre-attack pool snapshot at block 22765113
  8. Seed transaction trace and balance diff confirming deposit, withdrawal, repayment, and final ETH loss