Gradient Pool Unit Mismatch Drain
Exploit Transactions
0xb5cfa3f86ce9506e2364475dc43c44de444b079d4752edbffcdad7d1654b1f67Victim Addresses
0x37ea5f691bce8459c66ffceeb9cf34ffa32fdadcEthereum0x893d41635725d8ea6f528d3f3f3df3e9e8076934EthereumLoss Breakdown
Similar Incidents
Uwerx Pool Drain
37%NOON Pool Drain via Public transfer
37%Pawnfi ApeStaking Debt Mismatch
35%UERII Public Mint Drain
33%Minimal-Proxy Pool Reinitializer Drain
33%Wise Lending WBTC Donation Drain
32%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:
- The EOA deploys the helper contract and starts the exploit transaction.
- The helper calls Morpho at
0xbbbbbbbbbb9cc5e90e3b3af64bdaf62c37eeffcbfor a permissionless 3 WETH flash loan. - The helper unwraps 1 WETH to ETH and swaps
0.664800798545598825WETH on the public Uniswap router at0x7a250d5630b4cf539739df2c5dacb4c659f2488dto receive 950 GRAY. - 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.632090074270700494ETH throughprovideLiquidity. - The helper immediately calls
withdrawLiquidity(..., 10000)to burn all minted LP shares and withdraw3.642989205975327587ETH plus946.989100868295372906GRAY. - The helper wraps its ETH back into WETH, repays the 3 WETH flash loan, and transfers the remaining
2.346098333159028268WETH 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:
GradientMarketMakerPoolat0x37ea5f691bce8459c66ffceeb9cf34ffa32fdadc
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
- Seed exploit transaction:
0xb5cfa3f86ce9506e2364475dc43c44de444b079d4752edbffcdad7d1654b1f67 - Victim contract:
GradientMarketMakerPoolat0x37ea5f691bce8459c66ffceeb9cf34ffa32fdadc - Victim registry:
GradientRegistryat0x893d41635725d8ea6f528d3f3f3df3e9e8076934 - Public GRAY token:
0xa776a95223c500e81cb0937b291140ff550ac3e4 - Public GRAY/WETH Uniswap V2 pair:
0x0846f55387ab118b4e59eee479f1a3e8ea4905ec - Verified victim source showing the bug:
provideLiquidityandwithdrawLiquidityin the collectedGradientMarketMakerPool.sol - Pre-attack pool snapshot at block
22765113 - Seed transaction trace and balance diff confirming deposit, withdrawal, repayment, and final ETH loss