All incidents

GGGTOKEN Treasury Drain via receive()

Share
May 12, 2023 02:10 UTCAttackLoss: 172,248.76 USDTPending manual check2 exploit txWindow: 1m 24s
Estimated Impact
172,248.76 USDT
Label
Attack
Exploit Tx
2
Addresses
2
Attack Window
1m 24s
May 12, 2023 02:10 UTC → May 12, 2023 02:11 UTC

Exploit Transactions

TX 1BSC
0xb846f3aeb9b3027fe138b23bbf41901c155bd6d4b24f08d6b83bd37a975e4e4a
May 12, 2023 02:10 UTCExplorer
TX 2BSC
0x96b34dc3a98cd4055a984132d7f3f4cc5a16b2525113b8ef83c55ac0ba2b3713
May 12, 2023 02:11 UTCExplorer

Victim Addresses

0x7b8c378df8650373d82ceb1085a18fe34031784fBSC
0xae2f168900d5bb38171b01c2323069e5fd6b57b9BSC

Loss Breakdown

172,248.76USDT

Similar Incidents

Root Cause Analysis

GGGTOKEN Treasury Drain via receive()

1. Incident Overview TL;DR

GGGTOKEN on BNB Smart Chain was exploited because its token contract exposed a public receive() entrypoint that any external caller could use to spend protocol-controlled USDT from _marketAddr. In the two confirmed exploit transactions, 0xb846f3aeb9b3027fe138b23bbf41901c155bd6d4b24f08d6b83bd37a975e4e4a and 0x96b34dc3a98cd4055a984132d7f3f4cc5a16b2525113b8ef83c55ac0ba2b3713, the attacker first armed the thanPrice gate through direct transfers into the GGGTOKEN/USDT pair and immediate skim() recovery, then repeatedly called receive() to force 3000 USDT buy-and-burns funded by the protocol treasury helper 0xae2f168900d5bb38171b01c2323069e5fd6b57b9.

The root cause is twofold: _marketAddr had granted the token contract unlimited USDT allowance during deployment, and the supposed guard for treasury spending, thanPrice > 0, could be increased permissionlessly by manipulating unsynchronized pair balances. The attacker therefore needed no privileged role, no private key compromise, and no attacker-only infrastructure beyond a standard flash-swap helper contract. Balance-diff artifacts show _marketAddr lost 172248759773508961303541 raw USDT units across the two seed exploit transactions, while the attacker helper contract ended with 48415.966620732203452796 USDT before gas accounting.

2. Key Background

GGGTOKEN is a fee-on-transfer token paired against USDT on PancakeSwap V2 at 0x6d2d124acfe01c2d2adb438e37561a0269c6eabb. The contract routes accumulated fees through internal swaps and stores protocol-controlled USDT in helper addresses _router and _marketAddr. On BSC, the verified constructor sets _token = 0x55d398326f99059ff775485246999027b3197955 and creates _marketAddr as a fresh URoter contract.

PancakeSwap V2 pairs track reserves separately from actual token balances. If someone transfers tokens directly into the pair without calling the pair contract, the pair balance increases but reserves do not. Anyone can then call skim(address) to withdraw the excess balance. That accounting behavior matters because GGGTOKEN increments thanPrice in its sell path before the pair reserves are synchronized, so a direct transfer can satisfy the price-based gate while leaving the tokens immediately recoverable through skim().

The attacker used a helper contract at 0xa4fbc2c95ac4240277313bf3f810c54309dfcd6c, deployed by EOA 0x4404de29913e0fd055190e680771a016777973e5. Independent validation confirms that cast compute-address --nonce 0 0x4404de29913e0fd055190e680771a016777973e5 resolves to that helper address. The helper flash-borrowed USDT from pair 0x16b9a82891338f9ba80e2d6970fdda79d1eb0dae, bought GGG, ran the drain loop, sold the appreciated GGG back to USDT, repaid the flash swap, and kept the remainder.

3. Vulnerability Analysis & Root Cause Summary

The incident is an ATTACK-category ACT exploit against GGGTOKEN. The violated invariant is simple: USDT parked in the protocol-controlled treasury helper _marketAddr should only be spendable through privileged tokenomics flows, and any state gate intended to protect that treasury should not be armable by arbitrary traders at negligible cost. GGGTOKEN breaks that invariant because deployment of _marketAddr through URoter pre-approves the token contract for unlimited USDT spending, while receive() lets any caller exercise that spending power whenever thanPrice > 0.

The second half of the bug is that thanPrice is not a real authorization control. In _transfer(), any sell to the pair with on-chain value greater than 2500e18 after _startTimeForSwap + 72 hours increments thanPrice. Because a direct transfer into the pair counts as a sell path for this logic, an attacker can raise thanPrice without performing a normal AMM trade. Since the direct transfer leaves an unsynchronized excess balance in the pair, the attacker can immediately reclaim those tokens with skim() and repeat.

The core victim-side code is below.

_router = address(new URoter(_token, address(this)));
_marketAddr = address(new URoter(_token, address(this)));

if (2500e18 < price && _startTimeForSwap + 72 * 60 * 60 < block.timestamp) {
    thanPrice += 1;
    pr[from] = price;
}

receive() external payable {
    if (thanPrice == 0) return;
    if (IERC20(_token).balanceOf(_marketAddr) >= 3000e18) {
        IERC20(_token).transferFrom(_marketAddr, address(this), 3000e18);
        swapTokensForDead(3000e18);
        thanPrice -= 1;
    }
}

contract URoter {
    constructor(address tokens, address to) {
        tokens.call(abi.encodeWithSelector(0x095ea7b3, to, ~uint256(0)));
    }
}

That snippet shows the complete code-level breakpoint: _marketAddr grants approval at construction time, _transfer() increments thanPrice on attacker-controlled pair transfers, and receive() drains _marketAddr in fixed 3000 USDT tranches with no caller restriction.

4. Detailed Root Cause Analysis

At the validated pre-state before tx 0xb846f3aeb9b3027fe138b23bbf41901c155bd6d4b24f08d6b83bd37a975e4e4a in block 28002214, GGGTOKEN trading was enabled, the 72-hour timing gate had already elapsed, and _marketAddr held enough USDT to fund repeated drain calls. The attacker helper contract started with no USDT of its own and used only public protocol functionality.

The first critical condition is the approval path. GGGTOKEN deploys _marketAddr as new URoter(_token, address(this)), and URoter immediately calls approve(tokenContract, type(uint256).max) on USDT. That means GGGTOKEN itself permanently has allowance to move USDT out of _marketAddr. The second critical condition is the weak gate in _transfer(): when tokens are sent to the pair and the notional value exceeds 2500 USDT-equivalent, thanPrice increments before reserve synchronization.

The exploit loop seen in the seed trace is deterministic:

PancakePair::skim(0xA4Fbc2c95aC4240277313BF3f810c54309dFcd6C)
GGGTOKEN::receive{value: 1}()
BEP20USDT::transferFrom(
  0xae2f168900D5bb38171B01c2323069E5FD6b57B9,
  GGGTOKEN,
  3000000000000000000000
)
PancakeRouter::swapExactTokensForTokensSupportingFeeOnTransferTokens(
  3000000000000000000000,
  0,
  [USDT, GGG],
  0x000000000000000000000000000000000000dEaD,
  1683857415
)

That trace evidence shows exactly what the code predicts. After the attacker transfers GGG into the pair and immediately skims the excess back, the helper calls receive() with 1 wei of BNB. GGGTOKEN then spends 3000 USDT from _marketAddr, swaps it through PancakeRouter into GGG, and sends the purchased GGG to the dead address. Burning GGG this way supports the attacker’s remaining GGG inventory, so each drain iteration improves the attacker’s exit price while depleting the treasury helper.

The attack begins with public flash liquidity. In the same validated trace, the top-level helper borrows 1000000e18 USDT from flash pair 0x16b9a82891338f9ba80e2d6970fdda79d1eb0dae and immediately swaps it into GGG. After arming and consuming thanPrice in a loop, it sells its full GGG balance back into USDT and repays 1002506265664160401002507 raw USDT units to the flash pair. No privileged step is required anywhere in that sequence.

The balance diffs quantify the loss. In tx 0xb846f3..., _marketAddr loses 94062679733733139470673 raw USDT units and the helper gains 34549430524604983319973. In tx 0x96b34d..., _marketAddr loses another 78186080039775821832868 raw USDT units and the helper gains another 13866536096127220132823. The total treasury depletion is therefore 172248759773508961303541 raw USDT units, and the helper’s total gross gain across the two seed transactions is 48415966620732203452796 raw USDT units.

5. Adversary Flow Analysis

The adversary execution flow is:

  1. EOA 0x4404de29913e0fd055190e680771a016777973e5 calls its helper contract 0xa4fbc2c95ac4240277313bf3f810c54309dfcd6c.
  2. The helper flash-borrows 1000000e18 USDT from Pancake pair 0x16b9a82891338f9ba80e2d6970fdda79d1eb0dae.
  3. The helper swaps the borrowed USDT into GGGTOKEN through PancakeRouter 0x10ed43c718714eb63d5aa57b78b54704e256024e.
  4. For each drain iteration, the helper transfers a computed amount of GGG into the GGG/USDT pair 0x6d2d124acfe01c2d2adb438e37561a0269c6eabb, causing _transfer() to increment thanPrice.
  5. The helper immediately calls skim(address(this)) on the pair to recover the unsynchronized GGG excess.
  6. The helper calls GGGTOKEN.receive() to force a 3000 USDT treasury spend and a dead-address buy-and-burn.
  7. After enough iterations, the helper sells its appreciated GGG inventory back into USDT, repays the flash swap, and transfers the residual USDT out as profit.

This flow is ACT because every step is open to any unprivileged actor: deploying a helper contract, taking a Pancake flash swap, transferring tokens to the pair, calling skim(), and sending native value to a public receive() function are all permissionless. The exploit does not depend on the original attacker identity, custom attacker bytecode from the incident, or any protocol-admin capability.

6. Impact & Losses

The measurable victim loss is the depletion of GGGTOKEN's protocol-controlled treasury helper _marketAddr at 0xae2f168900d5bb38171b01c2323069e5fd6b57b9. Across the two validated exploit transactions, that helper lost 172248759773508961303541 raw USDT units, with decimal = 18. Those funds were converted into dead-address GGG purchases that improved the attacker's market exit while irreversibly draining protocol-owned USDT.

The attacker helper contract realized 34549.430524604983319973 USDT in the first seed transaction and 13866.536096127220132823 USDT in the second, for a gross total of 48415.966620732203452796 USDT before gas. The sender EOA paid gas in BNB for both transactions, but the attack remained strongly profitable after that cost.

Affected public components are:

  • GGGTOKEN token contract: 0x7b8c378df8650373d82ceb1085a18fe34031784f
  • GGGTOKEN treasury helper _marketAddr: 0xae2f168900d5bb38171b01c2323069e5fd6b57b9
  • GGGTOKEN/USDT Pancake pair: 0x6d2d124acfe01c2d2adb438e37561a0269c6eabb
  • Flash-swap funding pair: 0x16b9a82891338f9ba80e2d6970fdda79d1eb0dae

7. References

  • Exploit tx 0xb846f3aeb9b3027fe138b23bbf41901c155bd6d4b24f08d6b83bd37a975e4e4a, block 28002214
  • Exploit tx 0x96b34dc3a98cd4055a984132d7f3f4cc5a16b2525113b8ef83c55ac0ba2b3713, block 28002242
  • Verified GGGTOKEN source at 0x7b8c378df8650373d82ceb1085a18fe34031784f, especially constructor deployment of _marketAddr, _transfer(), and receive()
  • Seed traces for both exploit transactions showing the repeated skim() plus receive() loop and final unwind
  • Seed balance-diff artifacts for both exploit transactions showing _marketAddr depletion and helper-contract profit