All incidents

GymRouter Arbitrary Approved Token Spend

Share
Jul 31, 2023 16:51 UTCAttackLoss: 151,953.85 GYMNETPending manual check1 exploit txWindow: Atomic
Estimated Impact
151,953.85 GYMNET
Label
Attack
Exploit Tx
1
Addresses
2
Attack Window
Atomic
Jul 31, 2023 16:51 UTC → Jul 31, 2023 16:51 UTC

Exploit Transactions

TX 1BSC
0x7fe96c00880b329aa0fcb00f0ef3a0766c54e13965becf9cc5e0df6fbd0deca6
Jul 31, 2023 16:51 UTCExplorer

Victim Addresses

0x6b869795937dd2b6f4e03d5a0ffd07a8ad8c095bBSC
0x0012365f0a1e5f30a5046c680dcb21d07b15fcf7BSC

Loss Breakdown

151,953.85GYMNET

Similar Incidents

Root Cause Analysis

GymRouter Arbitrary Approved Token Spend

1. Incident Overview TL;DR

On BNB Smart Chain block 30448987, attacker EOA 0x97eace4702217c1fea71cf6b79647a8ad5ddb0eb used attacker contract 0xb8f83f38e262f28f4e7d80aa5a0216378e92baf2 to flash-borrow 1010000000000000000000000 GYM, add GYM/USDT liquidity, and then call GymRouter 18 times against third-party holders who had already approved GymRouter as a GYM spender. Those calls forcibly sold 151953845189344012453965 GYM from victim holders without any victim transaction or signature. After unwinding the temporary liquidity position and repaying 1043936000000000000000000 GYM to the flash-swap pair, the attacker transferred 117193506314277503007996 GYM to the attacker EOA.

The root cause is an authorization failure in GymRouter. The contract treats caller-supplied calldata parameter to as the account to debit via transferFrom, instead of binding the debited owner to msg.sender or to explicit signed intent from the token holder. Any unprivileged caller can therefore consume any holder's pre-existing GymRouter approval and force that holder's GYM through public PancakeSwap routes.

2. Key Background

GymRouter proxy 0x6b869795937dd2b6f4e03d5a0ffd07a8ad8c095b delegates to verified implementation 0x177dd7202eb9ae5154fdf3006f8ae93dcb3b45e9. Users had already granted GymRouter standing ERC20 approvals on GYM token 0x0012365f0a1e5f30a5046c680dcb21d07b15fcf7, so GymRouter could call transferFrom against those users whenever its own function logic selected them as the source account.

The collected GYM token source confirms the relevant surrounding token behavior: allowance(address account, address spender) exposes spender approvals, approve(address spender, uint256 rawAmount) writes those approvals for msg.sender, taxOnSell is 5, gasPriceLimit is enforced at token level, and the token tracks additional trading and hold restrictions. None of those token rules authenticate the router caller's intent on behalf of the holder; they only constrain whether a given transfer path is allowed to execute.

Public PancakeSwap liquidity already existed on both the GYM/USDT pair 0x8e1b75e6c43aeaf5055de07ab4b76e356d7bb2db and the GYM/WBNB pair 0xf5d3cba24783586db9e7f35188ec0747ffb55f9b before the exploit transaction. That pre-state made the opportunity permissionless and immediately realizable from public data.

3. Vulnerability Analysis & Root Cause Summary

The vulnerability class is an authorization bug in a router helper contract. GymRouter exposes public swap and liquidity functions that accept an address parameter named to. Instead of using to only as an output recipient, the implementation also treats to as the source owner for safeTransferFrom. That design collapses the concepts of payer and recipient into one attacker-controlled argument.

The safety invariant is straightforward: only the actual token owner, or logic backed by that owner's explicit signed authorization, should be able to decide which account GymRouter debits with transferFrom. GymRouter breaks that invariant because an arbitrary caller can name any approved holder in calldata and cause GymRouter to pull tokens from that holder. The verified implementation repeats this pattern across swapExactTokensForTokens, swapExactTokensForETH, swapExactTokensForETHSupportingFeeOnTransferTokens, swapExactTokensForTokensSupportingFeeOnTransferTokens, addLiquidity, and addLiquidityETH.

The breakpoint that matters most is inside swapExactTokensForTokensSupportingFeeOnTransferTokens(uint256,uint256,address[],address), where GymRouter executes IERC20Upgradeable(path[0]).safeTransferFrom(to, address(this), amountIn);. At that line, the debited token owner is not msg.sender; it is the attacker-chosen to address. Once that transfer succeeds, GymRouter can approve PancakeRouter and complete the swap using the victim's tokens exactly as if the victim had initiated it.

4. Detailed Root Cause Analysis

The verified GymRouter implementation on BscScan shows the source-of-funds bug directly:

function addLiquidity(
    address tokenA,
    address tokenB,
    uint256 amountADesired,
    uint256 amountBDesired,
    uint256 amountAMin,
    uint256 amountBMin,
    address to,
    uint256 deadline
) external nonReentrant returns (uint256 amountA, uint256 amountB, uint256 liquidity) {
    IERC20Upgradeable(tokenA).safeTransferFrom(to, address(this), amountADesired);
    IERC20Upgradeable(tokenB).safeTransferFrom(to, address(this), amountBDesired);
    ...
}

function swapExactTokensForTokensSupportingFeeOnTransferTokens(
    uint256 amountIn,
    uint256 amountOutMin,
    address[] calldata path,
    address to
) external nonReentrant {
    IERC20Upgradeable(path[0]).safeTransferFrom(to, address(this), amountIn);
    ...
}

The critical fact is that to is fully attacker-controlled calldata. No check binds that address to msg.sender, and no permit or signature is required from the holder. If a holder has a positive GYM balance and a standing GymRouter allowance that covers the requested amountIn, any external caller can spend it.

The reconstructed pre-state at block 30448986 already satisfied those conditions. The attacker contract held 9990000000000000000000000 USDT, public liquidity existed on the PancakeSwap routes, and multiple holders had nonzero GYM balances plus large GymRouter allowances. For example, holder 0x0c8bbd0629050b78c91f1aafdcf04e90238b3568 held 2882503155792021935209 GYM and had GymRouter allowance 992716325819906752100000 before the exploit block.

The seed transaction trace shows the exploit primitive in action on that holder:

0x177DD7202eb9AE5154fDf3006F8aE93DcB3b45e9::swapExactTokensForTokensSupportingFeeOnTransferTokens(
  2882503155792021935209,
  544755592471722209386,
  [GYM, USDT],
  0x0C8bbd0629050b78C91F1AAfDCF04e90238B3568
)
GymNetwork::transferFrom(
  0x0C8bbd0629050b78C91F1AAfDCF04e90238B3568,
  0x6b869795937DD2B6F4E03d5A0Ffd07A8AD8c095B,
  2882503155792021935209
)
emit Approval(... amount: 989833822664114730164791)
emit Transfer(... amount: 2882503155792021935209)
PancakePair::swap(... to: 0x0C8bbd0629050b78C91F1AAfDCF04e90238B3568, amount: 570567903196866472076)

That trace proves the full unauthorized path:

  1. The caller is the attacker-controlled transaction, not the holder.
  2. GymRouter uses the victim address as the transferFrom source.
  3. The victim's allowance decreases from 992716325819906752100000 to 989833822664114730164791.
  4. The victim's GYM balance drops from 2882503155792021935209 to 0.
  5. The swap completes on PancakeSwap and returns 570567903196866472076 USDT to the victim address.

The same pattern repeats across 18 forced swaps in the same transaction, totaling 151953845189344012453965 GYM sold from approved holders. This is why the ACT success predicate is primarily non-monetary: the deterministic failure is unauthorized liquidation of approved third-party balances by an unprivileged caller. The attacker EOA profit fields are now also fully deterministic: the attacker's GYM balance increased from 467547355275343364 to 117193973861632778351360, for a delta of 117193506314277503007996 GYM. Gas was paid in BNB rather than GYM, so fees measured in the chosen GYM reference asset are 0.

5. Adversary Flow Analysis

The adversary lifecycle begins with deployment transaction 0x5561ed8d9ee01a487a246fcba0e6323f66cb1eda49b0f8db12b853b3eab5dc58, where EOA 0x97eace4702217c1fea71cf6b79647a8ad5ddb0eb deployed exploit contract 0xb8f83f38e262f28f4e7d80aa5a0216378e92baf2. That deployment is relevant context, but the exploit realization itself is a single public transaction: 0x7fe96c00880b329aa0fcb00f0ef3a0766c54e13965becf9cc5e0df6fbd0deca6.

Inside the exploit transaction, the attacker contract first flash-borrows 1010000000000000000000000 GYM from pair 0xf5d3cba24783586db9e7f35188ec0747ffb55f9b. It then adds GYM/USDT liquidity through PancakeRouter using 1010000000000000000000000 GYM and 9990000000000000000000000 USDT. This LP priming stage positions the attacker on the other side of the forced sells.

The contract then calls GymRouter 18 times, each time selecting a different approved holder address in the to slot. Those calls force GymRouter to debit the holders and sell their GYM through the public GYM/USDT route. Representative victims include:

  • 0x0c8bbd0629050b78c91f1aafdcf04e90238b3568: 2882503155792021935209 GYM sold, 570567903196866472076 USDT received.
  • 0xbdfca747646975f3bb9da26bd55daf2168c40fe7: 30753817081643089949816 GYM sold, 5892650226075599475762 USDT received.
  • 0x4ad478039be7d1ad17c2ecbeb1029c29366c2789: 12507643322789373085733 GYM sold, 2298924246513379271362 USDT received.

After the forced liquidations, the attacker removes 451685731454957518592000 LP units, repays 1043936000000000000000000 GYM to the flash-loan pair, and transfers 117193506314277503007996 GYM to the attacker EOA. The seed trace contains the exact final transfer sequence:

GymNetwork::transfer(0xf5D3cba24783586Db9e7F35188EC0747FfB55F9B, 1043936000000000000000000)
GymNetwork::transfer(0x97Eace4702217c1fea71Cf6b79647A8aD5dDB0EB, 117193506314277503007996)
emit Transfer(from: 0xB8F83f38E262f28f4E7D80aa5a0216378E92Baf2,
              to:   0x97Eace4702217c1fea71Cf6b79647A8aD5dDB0EB,
              amount: 117193506314277503007996)

This flow is permissionless. It depends only on public liquidity, public approvals that already existed on-chain, and an unprivileged caller capable of submitting a normal transaction.

6. Impact & Losses

The measurable on-chain impact is the forced liquidation of 151953845189344012453965 GYM from 18 approved holders in a single transaction. That figure is the unauthorized GYM volume pulled and sold through GymRouter. It is not a mark-to-market net-loss estimate, because the victims did receive swap output tokens back to their own addresses during the forced trades.

The affected public protocol components are:

  • GymRouter proxy 0x6b869795937dd2b6f4e03d5a0ffd07a8ad8c095b
  • GymRouter implementation 0x177dd7202eb9ae5154fdf3006f8ae93dcb3b45e9
  • GYM NETWORK V2 token 0x0012365f0a1e5f30a5046c680dcb21d07b15fcf7
  • PancakeRouter 0x10ed43c718714eb63d5aa57b78b54704e256024e
  • GYM/USDT pair 0x8e1b75e6c43aeaf5055de07ab4b76e356d7bb2db
  • GYM/WBNB pair 0xf5d3cba24783586db9e7f35188ec0747ffb55f9b

The attacker-side measurable gain is a direct EOA GYM increase of 117193506314277503007996. The victims were harmed because their approved balances were liquidated by a third party without authorization, not because the output tokens failed to arrive. That distinction is why the incident is best characterized as unauthorized third-party liquidation enabled by broken router authorization.

7. References

  1. Seed exploit transaction: 0x7fe96c00880b329aa0fcb00f0ef3a0766c54e13965becf9cc5e0df6fbd0deca6
  2. Attacker contract deployment transaction: 0x5561ed8d9ee01a487a246fcba0e6323f66cb1eda49b0f8db12b853b3eab5dc58
  3. Verified GymRouter implementation: 0x177dd7202eb9ae5154fdf3006f8ae93dcb3b45e9
  4. GymRouter proxy: 0x6b869795937dd2b6f4e03d5a0ffd07a8ad8c095b
  5. GYM token source collected under /workspace/session/artifacts/collector/seed/56/0xdc1b68f73a749cbb5ba94f46d48fbf1a9ce65fd1/src/GymNetwork.sol
  6. Seed transaction metadata: /workspace/session/artifacts/collector/seed/56/0x7fe96c00880b329aa0fcb00f0ef3a0766c54e13965becf9cc5e0df6fbd0deca6/metadata.json
  7. Seed transaction trace: /workspace/session/artifacts/collector/seed/56/0x7fe96c00880b329aa0fcb00f0ef3a0766c54e13965becf9cc5e0df6fbd0deca6/trace.cast.log
  8. Decoded evidence summary: /workspace/session/artifacts/auditor/iter_0/evidence_summary.json