All incidents

GSS Pair Skim Misclassification

Share
Aug 23, 2023 16:27 UTCAttackLoss: 25,964.72 USDTPending manual check1 exploit txWindow: Atomic
Estimated Impact
25,964.72 USDT
Label
Attack
Exploit Tx
1
Addresses
3
Attack Window
Atomic
Aug 23, 2023 16:27 UTC → Aug 23, 2023 16:27 UTC

Exploit Transactions

TX 1BSC
0x4f8cb9efb3cc9930bd38af5f5d34d15ce683111599a80df7ae50b003e746e336
Aug 23, 2023 16:27 UTCExplorer

Victim Addresses

0x37e42b961ae37883bac2fc29207a5f88efa5db66BSC
0x1ad2cb3c2606e6d5e45c339d10f81600bdbf75c0BSC
0xb4f4cd1cc2dff1a14c4aaa9e9434a92082855c64BSC

Loss Breakdown

25,964.72USDT

Similar Incidents

Root Cause Analysis

GSS Pair Skim Misclassification

1. Incident Overview TL;DR

On BNB Chain block 31108559, transaction 0x4f8cb9efb3cc9930bd38af5f5d34d15ce683111599a80df7ae50b003e746e336 used a public DODO flash loan to buy GSS, distort pair balances, chain two permissionless skim() calls across the GSS/USDT and GSS/GSSDAO pairs, and then dump the extracted GSS back into USDT. The attacker contract 0x69ed5b59d977695650ec4b29e61c0faa8cc0ed5c finished with 24883439810865059102747 raw USDT units of profit after repaying the 30000000000000000000000 raw-unit flash loan principal, while the GSS/USDT pair lost 25964719534010397527333 raw USDT units.

The root cause is that GSS applies pair-specific fee logic whenever either transfer endpoint equals a tracked pair address. That logic does not distinguish real buys and sells from permissionless pair-maintenance transfers such as skim(). As a result, skim() transfers on GSS-bearing pairs trigger fee redistribution, create fresh excess balances, and let any public caller route and extract unreserved GSS.

2. Key Background

skim(address) on a UniswapV2-style pair is permissionless. It transfers balance(token) - reserve(token) to an arbitrary recipient without updating stored reserves. Under normal token semantics, skim() is a reserve-maintenance helper, not a taxable trade.

GSS tracks two AMM pairs in token state: uniswapV2Pair0 for GSS/USDT and uniswapV2Pair1 for GSS/GSSDAO. The verified GSS source shows that _transfer dispatches any transfer touching either pair into processPair0 or processPair1, and those routines always call fee-taking helpers when the sender and recipient are not fee-exempt.

if (from == uniswapV2Pair0 || to == uniswapV2Pair0) {
    processPair0(from, to, amount);
}

if (from == uniswapV2Pair1 || to == uniswapV2Pair1) {
    processPair1(from, to, amount);
}

Inside those paths, _tokenTransfer0 and _tokenTransfer1 deduct holder, burn, LP, and buyback fees before forwarding the remainder. That behavior is appropriate only for genuine pair trades. Once it is applied to pair maintenance, the token and pair accounting diverge.

3. Vulnerability Analysis & Root Cause Summary

The vulnerability is an application-level accounting bug in GSS, not a PancakePair flaw. GSS classifies transfers entirely by whether from or to equals a tracked pair address, so a permissionless skim() transfer is treated exactly like a buy or sell. In processPair0 and processPair1, the token then routes the transfer into _tokenTransfer0 or _tokenTransfer1, which remove fees and send portions of the transferred amount to the token contract, the dividend tracker, burn address, LP receiver, and buyback receiver. That breaks the expected invariant for pair maintenance: a skim() call should reduce balanceOf(pair) - reserve(pair) by the amount skimmed and should not create new excess balances elsewhere. Here, skimming from the GSS/USDT pair into the GSS/GSSDAO pair produced fee side effects and a downstream excess balance, and skimming from the second pair to the attacker repeated the same mistake. Because both skim() and sync() are public, any unprivileged actor with enough temporary capital could reproduce the sequence.

4. Detailed Root Cause Analysis

The critical breakpoint is the pair-address routing in the verified GSS token:

function _transfer(address from, address to, uint256 amount) internal override {
    ...
    if (from == uniswapV2Pair0 || to == uniswapV2Pair0) {
        processPair0(from, to, amount);
    }
    if (from == uniswapV2Pair1 || to == uniswapV2Pair1) {
        processPair1(from, to, amount);
    }
    ...
}

processPair0 and processPair1 do not inspect call context. They only inspect the transfer endpoints and then execute fee-taking paths:

// transfer amount, it will take tax, burn, liquidity fee
_tokenTransfer0(from, to, amount, takeFee);
...
// transfer amount, it will take tax, burn, liquidity fee
_tokenTransfer1(from, to, amount, takeFee);

In _tokenTransfer0, a transfer with recipient == uniswapV2Pair0 is treated as a sell and charged sellLpFee0 + sellBuyBackFee0, sellHoldersFee0, and sellBurnFee0. In _tokenTransfer1, a transfer with sender == uniswapV2Pair1 is treated as a buy and charged buyLpFee1, buyBuyBackFee1, and buyBurnFee1. Those branches are exactly what a skim()-initiated pair transfer hits.

The seed trace shows the complete exploit realization:

0x9ad32e3054268B849b84a8dBcC7c8f7c52E4e69A::flashLoan(..., 30000000000000000000000, ...)
PancakeRouter::swapExactTokensForTokensSupportingFeeOnTransferTokens(30000000000000000000000, ...)
GSS::transfer(PancakePair: [0x1ad2...75C0], 707162351662098288993328)
PancakePair::skim(0xB4F4cD1cc2DfF1A14c4Aaa9E9434A92082855C64)
0xB4F4cD1cc2DfF1A14c4Aaa9E9434A92082855C64::skim(0x69ED5B59D977695650eC4B29E61c0Faa8cc0ed5C)
PancakeRouter::swapExactTokensForTokensSupportingFeeOnTransferTokens(1232737198706559843688819, ...)
BEP20USDT::transfer(0x69ED5B59D977695650eC4B29E61c0Faa8cc0ed5C, 54883439810865059102747)
BEP20USDT::transfer(0x9ad32e3054268B849b84a8dBcC7c8f7c52E4e69A, 30000000000000000000000)

Step by step, the attacker borrowed 30,000 USDT from the public DODO pool, bought GSS through PancakeRouter, then transferred almost the entire GSS/USDT pair balance worth of GSS directly into pair0 to manufacture a large excess balance. When pair0 skimmed to pair1, GSS treated the pair-originated transfer as a taxed pair transfer instead of neutral maintenance. After pair0.sync(), the attacker skimmed pair1 back to the attacker contract, again triggering pair-originated fees. The attacker then sold the extracted GSS back into pair0 for 54883439810865059102747 raw USDT units, repaid the flash loan principal, and retained the remainder.

The balance diff confirms the economic result. The attacker contract gained 24883439810865059102747 raw USDT units, the GSS/USDT pair lost 25964719534010397527333 raw USDT units, and smaller portions were redistributed to fee destinations and token-side accounting buckets. No USDT-denominated fee remained unpaid at transaction end; gas was paid separately in BNB by the sender EOA, so the USDT profit predicate is deterministic.

5. Adversary Flow Analysis

The adversary cluster contains EOA 0x84f37f6cc75ccde5fe9ba99093824a11cfdc329d, which submitted the transaction and paid gas, and helper contract 0x69ed5b59d977695650ec4b29e61c0faa8cc0ed5c, which executed the exploit logic.

The on-chain sequence was:

  1. Borrow 30000000000000000000000 raw USDT units from the public DODO flash-loan pool 0x9ad32e3054268B849b84a8dBcC7c8f7c52E4e69A.
  2. Swap the borrowed USDT into GSS through PancakeRouter 0x10ED43C718714eb63d5aA57B78B54704E256024E.
  3. Transfer GSS directly into the GSS/USDT pair 0x1ad2cb3c2606e6d5e45c339d10f81600bdbf75c0 to create skimmable excess over reserves.
  4. Call pair0.skim(pair1) to move excess GSS into the GSS/GSSDAO pair 0xb4f4cd1cc2dff1a14c4aaa9e9434a92082855c64, while GSS fee logic mutates the amount in transit.
  5. Call pair0.sync() so the first pair records its new balances and the manipulated state persists.
  6. Call pair1.skim(attacker) to extract the enlarged excess from the second pair into the attacker contract, again through fee-taking token logic.
  7. Sell the recovered GSS back into the GSS/USDT pair for USDT.
  8. Repay the DODO principal and keep the residual USDT.

This is an ACT path because every primitive used in the sequence was permissionless and public: flash liquidity, router swaps, pair skim(), and pair sync(). No privileged role, private key compromise, or attacker-only artifact was required to realize the exploit.

6. Impact & Losses

The immediate victimized asset was the USDT liquidity in the GSS/USDT pair. The pair balance diff shows a loss of 25964719534010397527333 raw USDT units. The attacker contract realized 24883439810865059102747 raw USDT units of profit. The gap between gross pool loss and attacker profit was redistributed into GSS-side fee destinations such as the token contract, dividend tracker, burn address, and related fee receivers during the misclassified pair transfers.

Affected public components were:

  • GSS token 0x37e42b961ae37883bac2fc29207a5f88efa5db66
  • GSS/USDT PancakePair 0x1ad2cb3c2606e6d5e45c339d10f81600bdbf75c0
  • GSS/GSSDAO PancakePair 0xb4f4cd1cc2dff1a14c4aaa9e9434a92082855c64

7. References

  • Seed transaction: 0x4f8cb9efb3cc9930bd38af5f5d34d15ce683111599a80df7ae50b003e746e336
  • Attacker sender EOA: 0x84f37f6cc75ccde5fe9ba99093824a11cfdc329d
  • Attacker helper contract: 0x69ed5b59d977695650ec4b29e61c0faa8cc0ed5c
  • Verified victim token source: GSS at 0x37e42b961ae37883bac2fc29207a5f88efa5db66
  • Victim pairs: GSS/USDT at 0x1ad2cb3c2606e6d5e45c339d10f81600bdbf75c0, GSS/GSSDAO at 0xb4f4cd1cc2dff1a14c4aaa9e9434a92082855c64
  • Flash-loan pool: 0x9ad32e3054268B849b84a8dBcC7c8f7c52E4e69A
  • Public router used in the exploit: 0x10ED43C718714eb63d5aA57B78B54704E256024E
  • Supporting evidence artifacts: seed metadata, opcode-level seed trace, balance diff, verified GSS source, and verified GSSDAO source under /workspace/session/artifacts/collector/seed/56/