All incidents

GROK Tax-Swap Dump

Share
Nov 10, 2023 02:51 UTCAttackLoss: 26.35 ETHPending manual check1 exploit txWindow: Atomic

Root Cause Analysis

GROK Tax-Swap Dump

1. Incident Overview TL;DR

In Ethereum mainnet transaction 0x3e9bcee951cdad84805e0c82d2a1e982e71f2ec301a1cbd344c832e0acaee813 at block 18538679, an unprivileged attacker used nested Uniswap V3 flash loans to force GROK's token contract to sell its own treasury-held tax inventory into the GROK/WETH Uniswap V2 pool at an attacker-controlled price. That forced sale depressed GROK on the V2 pool while the Uniswap V3 pool remained on a different price curve, which let the attacker repay the flash liquidity, arbitrage the V2/V3 price gap, and exit with 26.347451739753993931 ETH net profit.

The root cause is GROK's sell-path tax-swap logic. Any seller targeting the V2 pair can trigger swapTokensForEth once the GROK contract balance is above _taxSwapThreshold, and the internal router swap uses amountOutMin = 0. Because any user can push the contract above the threshold with a direct token transfer, the treasury sale is permissionlessly triggerable under manipulated spot conditions.

2. Key Background

GROK is a fee-on-transfer ERC-20 token at 0x8390a1da07e376ef7add4be859ba74fb83aa02d5. Its tax logic accumulates tokens in the contract itself and, on user sells into the Uniswap V2 pair 0x69c66beafb06674db41b22cfc50c34a93b8d82a2, can liquidate that inventory for ETH and forward the proceeds to the tax wallet 0xf9387ac9f61cc22994a59a6008f827435ce744b6.

Three public thresholds matter:

  • _taxSwapThreshold = 69,000,000 * 10^9
  • _maxTaxSwap = 69,000,000 * 10^9
  • _preventSwapBefore = 20

By the attack block, trading was already open and _buyCount was already above _preventSwapBefore, so the remaining precondition was only that GROK's contract-held balance be pushed strictly above _taxSwapThreshold.

The attacker sourced liquidity entirely from public venues:

  • 0x109830a1aaad605bbf02a9dfa7b0b92ec2fb7daa for the outer 30 WETH flash loan
  • 0x66ba59cbd09e75b209d1d7e8cf97f4ab34da413b for the inner GROK flash loan
  • 0x69c66beafb06674db41b22cfc50c34a93b8d82a2 for the manipulated GROK/WETH V2 trading venue

The updated root-cause artifact also closes the prior verification-status gap: the GROK/WETH V2 pair resolves to verified UniswapV2Pair source on Etherscan, and the GROK/WETH V3 pool resolves to verified UniswapV3Pool source on Etherscan.

3. Vulnerability Analysis & Root Cause Summary

The vulnerability is an attacker-triggerable treasury liquidation path embedded in GROK's ordinary sell flow. GROK _transfer checks whether the recipient is the Uniswap V2 pair and whether the contract's own balance is above _taxSwapThreshold; if so, it calls swapTokensForEth before finishing the user transfer. That call sells the contract's own inventory through the Uniswap V2 router with amountOutMin = 0, so it accepts any manipulated spot price. Because any user can send GROK directly to the token contract, the balance threshold is not a meaningful safety boundary. Once the attacker forces the internal sale, the V2 pool becomes materially cheaper than the untouched V3 pool, creating a same-transaction arbitrage. This is an ACT-style exploit because every step uses public contracts, public liquidity, and publicly reconstructible state.

Collected GROK source shows the precise breakpoint:

if (!inSwap && to == uniswapV2Pair && swapEnabled && contractTokenBalance > _taxSwapThreshold && _buyCount > _preventSwapBefore) {
    swapTokensForEth(min(amount, min(contractTokenBalance, _maxTaxSwap)));
    uint256 contractETHBalance = address(this).balance;
    if (contractETHBalance > 0) {
        sendETHToFee(address(this).balance);
    }
}

function swapTokensForEth(uint256 tokenAmount) private lockTheSwap {
    address[] memory path = new address[](2);
    path[0] = address(this);
    path[1] = uniswapV2Router.WETH();
    _approve(address(this), address(uniswapV2Router), tokenAmount);
    uniswapV2Router.swapExactTokensForETHSupportingFeeOnTransferTokens(
        tokenAmount,
        0,
        path,
        address(this),
        block.timestamp
    );
}

The violated invariant is straightforward: contract-held tax inventory should not be permissionlessly liquidated against an attacker-manipulated spot price, especially with zero slippage protection.

4. Detailed Root Cause Analysis

The pre-state immediately before the seed transaction already satisfied most exploit conditions. Trading was open, GROK's V2 pair and both V3 pools had live liquidity, and _buyCount was already above the sell-path gate. The only missing condition was that the GROK contract itself held 66,262,042.000910581 GROK, which was below the 69,000,000 GROK swap threshold.

The attacker first borrowed GROK from the V3 pool and sold 30,695,631.768482954 GROK into the V2 pair to move the V2 price. The attacker then transferred exactly 2,737,958.999089419 GROK directly into the GROK token contract, which raised the contract balance to 69,000,001.000000000 GROK and armed the sell-path trigger.

Collected seed trace evidence shows that top-up and the immediately following attacker-controlled sell:

GROK::transfer(GROK, 2737958999089419)
  emit Transfer(from: 0x03e7...7eca, to: GROK, value: 2737958999089419)

0x7a250d...F2488D::swapExactTokensForTokensSupportingFeeOnTransferTokens(
  30000000000000000,
  0,
  [GROK, WETH],
  0x03e7...7eca,
  1699584695
)

That second sell entered GROK _transfer with to == uniswapV2Pair, contractTokenBalance > _taxSwapThreshold, and _buyCount > _preventSwapBefore, so GROK sold its own balance through the router. The same trace shows the internal treasury sale and the router withdrawing WETH to ETH:

0x7a250d...F2488D::swapExactTokensForETHSupportingFeeOnTransferTokens(
  30000000000000000,
  0,
  [GROK, WETH],
  GROK,
  1699584695
)
GROK::transferFrom(GROK, 0x69c6...82a2, 30000000000000000)
WETH9::transfer(0x7a250d...F2488D, 30636674232013169665)
WETH9::withdraw(30636674232013169665)
0xf9387a...44b6::fallback{value: 33998101403229299434}()

The balance diff confirms the semantic result of that path:

  • GROK contract balance fell from 66,262,042.000910581 to 39,000,001.000000000, a loss of 27,262,041.000910581 GROK.
  • Tax wallet 0xf9387ac9f61cc22994a59a6008f827435ce744b6 gained 33.998101403229299434 ETH.
  • Attacker EOA 0x864e656c57a5a119f332c47326a35422294db5c9 gained 26.347451739753993931 ETH net.

After the forced treasury dump, the attacker bought back 64,067,926.675248097 GROK from the now-depressed V2 pool to repay the inner GROK flash loan, then used the remaining 30 WETH to buy 17,906,643.278652560 GROK on V2 and sold that inventory into the V3 pool for 38.464577848246153489 WETH. The outer flash loan was repaid with fee, and the residual WETH was withdrawn to ETH and forwarded to the attacker EOA.

The exploit conditions from the validated root cause are therefore all satisfied:

  • GROK trading was open and the sell-path swap gate was live.
  • The attacker could permissionlessly top up the token contract above _taxSwapThreshold.
  • Independent public venues existed for the manipulated V2 price and the arbitrage V3 exit.
  • Public flash liquidity existed for both WETH and GROK sourcing.

5. Adversary Flow Analysis

  1. 0x864e656c57a5a119f332c47326a35422294db5c9 sent the seed transaction to attacker contract 0x03e7b13bcd9b8383f403696c1494845560607eca.
  2. The attacker contract borrowed 30 WETH from the public Uniswap V3 pool 0x109830a1aaad605bbf02a9dfa7b0b92ec2fb7daa, creating the outer flash-loan context.
  3. Inside that callback, the attacker borrowed 63,433,590.767572373 GROK from V3 pool 0x66ba59cbd09e75b209d1d7e8cf97f4ab34da413b, sold part of it into the V2 pair, topped the GROK contract above the threshold, and then sold another 30,000,000 GROK so GROK's own logic dumped treasury inventory into the same pair.
  4. The attacker bought back GROK on the now-depressed V2 pool to repay the GROK flash loan, used the remaining WETH to buy additional discounted GROK on V2, and sold that position into the V3 pool at the higher price.
  5. The attacker repaid 30.003 WETH to the outer flash pool, withdrew the residual WETH to ETH, and forwarded the proceeds to the sender EOA.

Collected seed trace evidence for the repayment and profit realization is explicit:

swapTokensForExactTokens(64067926675248097, 104401603085934625288, [WETH, GROK], 0x03e7...7eca, ...)
GROK::transfer(0x66bA59...413B, 64067926675248097)
WETH9::transfer(0x109830...7dAa, 30003000000000000000)
WETH9::withdraw(26382963431285374891)
0x864E656c57A5A119F332C47326A35422294DB5C9::fallback{value: 26382963431285374891}()

This is a single-transaction ACT path. No privileged address, private key compromise, or local mocking is required to realize it.

6. Impact & Losses

The measurable attacker profit is 26.347451739753993931 ETH, which is 26347451739753993931 wei. That value is directly visible in the collected balance diff for the attacker EOA.

The exploit also forced GROK's contract to liquidate a material portion of its treasury-held tax inventory:

  • GROK contract inventory depletion: 27,262,041.000910581 GROK
  • ETH forwarded to the GROK tax wallet: 33.998101403229299434 ETH
  • Net attacker profit: 26.347451739753993931 ETH

Affected parties were the GROK protocol treasury inventory and the market participants exposed to the abrupt treasury dump and induced V2/V3 price dislocation.

The security principles violated are equally clear:

  • Treasury or tax inventory should not be liquidated at an attacker-controlled live AMM price without slippage bounds or oracle sanity checks.
  • Ordinary user transfers should not trigger large external market side effects that meaningfully reshape AMM reserves.
  • User-controlled direct transfers should not be able to arm an internal safety threshold that gates treasury liquidation.

7. References

  • Seed transaction: 0x3e9bcee951cdad84805e0c82d2a1e982e71f2ec301a1cbd344c832e0acaee813
  • GROK token contract: 0x8390a1da07e376ef7add4be859ba74fb83aa02d5
  • GROK/WETH Uniswap V2 pair: 0x69c66beafb06674db41b22cfc50c34a93b8d82a2
  • GROK/WETH Uniswap V3 pool: 0x66ba59cbd09e75b209d1d7e8cf97f4ab34da413b
  • Outer WETH flash pool: 0x109830a1aaad605bbf02a9dfa7b0b92ec2fb7daa
  • Collected GROK source and collected seed trace/balance-diff artifacts for the transaction above
  • Etherscan verified source page for the V2 pair: https://etherscan.io/address/0x69c66beafb06674db41b22cfc50c34a93b8d82a2#code
  • Etherscan verified source page for the V3 pool: https://etherscan.io/address/0x66ba59cbd09e75b209d1d7e8cf97f4ab34da413b#code