GymRouter Arbitrary Approved Token Spend
Exploit Transactions
0x7fe96c00880b329aa0fcb00f0ef3a0766c54e13965becf9cc5e0df6fbd0deca6Victim Addresses
0x6b869795937dd2b6f4e03d5a0ffd07a8ad8c095bBSC0x0012365f0a1e5f30a5046c680dcb21d07b15fcf7BSCLoss Breakdown
Similar Incidents
MultiSender Arbitrary From Theft
40%ARA Swap Helper Approved-Holder Exploit
38%Public Treasury Spend on BRAND Helper
38%SwapX Arbitrary transferFrom Approval Drain on BNB Chain
36%SellToken Arbitrary-Pair LP Drain
35%FarmZAP Fee-Free Arbitrary-Farm Abuse
34%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:
- The caller is the attacker-controlled transaction, not the holder.
- GymRouter uses the victim address as the
transferFromsource. - The victim's allowance decreases from
992716325819906752100000to989833822664114730164791. - The victim's GYM balance drops from
2882503155792021935209to0. - The swap completes on PancakeSwap and returns
570567903196866472076USDT 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:2882503155792021935209GYM sold,570567903196866472076USDT received.0xbdfca747646975f3bb9da26bd55daf2168c40fe7:30753817081643089949816GYM sold,5892650226075599475762USDT received.0x4ad478039be7d1ad17c2ecbeb1029c29366c2789:12507643322789373085733GYM sold,2298924246513379271362USDT 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
- Seed exploit transaction:
0x7fe96c00880b329aa0fcb00f0ef3a0766c54e13965becf9cc5e0df6fbd0deca6 - Attacker contract deployment transaction:
0x5561ed8d9ee01a487a246fcba0e6323f66cb1eda49b0f8db12b853b3eab5dc58 - Verified GymRouter implementation:
0x177dd7202eb9ae5154fdf3006f8ae93dcb3b45e9 - GymRouter proxy:
0x6b869795937dd2b6f4e03d5a0ffd07a8ad8c095b - GYM token source collected under
/workspace/session/artifacts/collector/seed/56/0xdc1b68f73a749cbb5ba94f46d48fbf1a9ce65fd1/src/GymNetwork.sol - Seed transaction metadata:
/workspace/session/artifacts/collector/seed/56/0x7fe96c00880b329aa0fcb00f0ef3a0766c54e13965becf9cc5e0df6fbd0deca6/metadata.json - Seed transaction trace:
/workspace/session/artifacts/collector/seed/56/0x7fe96c00880b329aa0fcb00f0ef3a0766c54e13965becf9cc5e0df6fbd0deca6/trace.cast.log - Decoded evidence summary:
/workspace/session/artifacts/auditor/iter_0/evidence_summary.json