GDS LP Mining Rewards Were Reclaimable By Reusing Transferable LP Shares
Exploit Transactions
Victim Addresses
0xc1bb12560468fb255a8e8431bdf883cc4cb3d278BSC0xdd3e3384ae10b295fb353b1bda4fd3776bc4b650BSCLoss Breakdown
Similar Incidents
BTNFT transferFrom reward-claim bypass drains vested BTTToken rewards
33%Marketplace proxy 0x9b3e9b92 bug drains USDT and mints rewards
32%DBW Static-Income LP Drain
31%StakingDYNA Reward Backdating Drain
30%SNKMiner Referral Reward Drain
30%QiQi Reward Quote Override Drain
30%Root Cause Analysis
GDS LP Mining Rewards Were Reclaimable By Reusing Transferable LP Shares
1. Incident Overview TL;DR
GDS LP mining on BNB Smart Chain was exploitable because reward accounting was attached to addresses, while the staked LP position itself remained freely transferable in the Pancake pair contract. In setup tx 0xf9b6cc083f6e0e41ce5e5dd65b294abf577ef47c7056d86315e5e53aa662251e, the attacker primed multiple helper contracts so each helper captured lastEpoch = 29 and remained eligible for future payouts. In harvest tx 0x2bb704e0d158594f7373ec6e53dc9da6c6639f269207da8dab883fc3b5bf6694, after currentEpoch advanced to 30, the attacker rotated one LP position through those stale helpers and triggered reward settlement repeatedly.
Each helper was rewarded as if it had independently carried the LP position across the epoch boundary, even though the LP was only present transiently at claim time. The LP reward pool at 0xdd3e3384ae10b295fb353b1bda4fd3776bc4b650 lost 14374634234643021506248017 GDS in the harvest transaction. The refreshed profit accounting shows the identified adversary cluster increased from 6620446573194870774287 to 195993092744122588055265 raw USDT units across the two-transaction sequence, a net gain of 189372646170927717280978 raw USDT units already net of gas.
The root cause is a reward-debt design failure in GDSToken._settlementLpMining: it reads the helper's live LP balance at settlement time but relies on stale lastEpoch state that is never synchronized when LP tokens move between addresses. checkAccount further lowers the capital requirement by allowing rewards once only 10% of the prior activation burn remains in GDS.
2. Key Background
GDS implements LP mining inside the verified token contract 0xc1bb12560468fb255a8e8431bdf883cc4cb3d278, while the actual LP token is the Pancake pair 0x4526c263571eb57110d161b41df8fd073df3c44a. That split matters because LP transfers happen in the pair contract, not in GDS, so GDS does not automatically observe stake movement.
The LP mining system tracks a per-address epoch marker in lastEpoch[address]. A helper address becomes initialized when _settlementLpMining runs while it has nonzero LP. Once an epoch rolls forward, the same helper can later claim the accumulated LP reward if it again has LP balance at settlement time.
Reward delivery is not guarded by a proper per-position reward debt. Instead, GDS computes a fresh pro-rata reward from the helper's current LP balance and sends tokens out of the shared LP reward pool. Eligibility is enforced by checkAccount, which requires the recipient to remain activated and keep only destroyMiningAccounts[_from] * 1000 / 10000, or 10%, of the activation burn.
That design makes the following combination unsafe:
- Prime many addresses so each has a stale
lastEpoch. - Remove LP from those addresses before the block boundary.
- After the epoch advances, move one larger LP position through those addresses one by one.
- Trigger GDS settlement via a public GDS transfer or burn.
3. Vulnerability Analysis & Root Cause Summary
This is an ACT attack caused by stale, address-scoped LP reward accounting. The decisive victim-side logic is shown below.
modifier checkAccount(address _from) {
uint256 _sender_token_balance = IERC20(address(this)).balanceOf(_from);
if (
!isExcludedReward[_from] &&
isActivated[_from] &&
_sender_token_balance >= destroyMiningAccounts[_from] * 1000 / _denominator
) {
_;
}
}
function _settlementLpMining(address _from) internal {
uint256 _lpTokenBalance = IERC20(gdsUsdtPair).balanceOf(_from);
uint256 _lpTokenTotalSupply = IERC20(gdsUsdtPair).totalSupply();
if (lastEpoch[_from] > 0 && currentEpoch > lastEpoch[_from] && _lpTokenBalance > 0) {
uint256 _totalRewardAmount = 0;
for (uint i = lastEpoch[_from]; i < currentEpoch; i++) {
_totalRewardAmount += everyEpochLpReward[i];
_totalRewardAmount += everyDayLpMiningAmount;
}
uint256 _lpRewardAmount = _totalRewardAmount * _lpTokenBalance / _lpTokenTotalSupply;
_internalTransfer(lpPoolContract, _from, _lpRewardAmount, 4);
lastEpoch[_from] = currentEpoch;
}
if (lastEpoch[_from] == 0 && _lpTokenBalance > 0) {
lastEpoch[_from] = currentEpoch;
}
if (_lpTokenBalance == 0) {
lastEpoch[_from] = 0;
}
}
The invariant that should hold is straightforward: one LP position must not claim the same historical epoch reward more than once, regardless of how many addresses later receive that LP. GDS violates that invariant because lastEpoch is stored per address, while LP ownership is only sampled when settlement runs. There is no synchronization step when LP is transferred in the pair contract, so historical reward debt does not follow the position.
The second issue is capital efficiency. checkAccount does not require the helper to keep the full burn-sized balance that activated it. Retaining about 10% is enough, so the attacker could cheaply keep many helpers eligible and reuse them later.
4. Detailed Root Cause Analysis
The exploit path is fully reconstructible from public code, traces, and balance diffs.
- Before setup, LP mining was already open and
currentEpoch()was 29. The GDS/USDT pair and reward pool were live, and the verified source already exposed the flawed accounting model. - In the setup transaction, the attacker created helper contracts, transferred a dust LP amount into each helper, and triggered a burn-sized GDS transfer so
_settlementLpMiningsetlastEpoch(helper) = 29. - After priming, the helpers no longer held LP at the block boundary, but they stayed activated and retained enough residual GDS to satisfy
checkAccount. - By the block before the harvest tx,
currentEpochhad advanced from 29 to 30, while representative helpers still showedlastEpoch = 29andpair.balanceOf(helper) = 0at the boundary state. - In the harvest transaction, the attacker routed the same larger LP balance through stale helpers one by one, triggered settlement in each helper via a public GDS burn path, returned the LP back out, and swapped harvested GDS to USDT.
The on-chain outcome matches that mechanism exactly:
{
"holder": "0xcf2362b46669e04b16d0780cf9b6e61c82de36a7",
"token": "USDT",
"delta": "39201649548418393336468"
}
{
"holder": "0xdd3e3384ae10b295fb353b1bda4fd3776bc4b650",
"token": "GDS",
"delta": "-14374634234643021506248017"
}
{
"holder": "0x0b995c08abddc0442bee87d3a7c96b227f8e7268",
"token": "GDS",
"delta": "11112305132080436192256555"
}
Those balance changes show three important facts. First, the LP reward pool funded the extraction. Second, the attacker EOA realized a large USDT payout directly. Third, the harvest orchestrator still retained a large GDS balance after the transaction, which is why the refreshed profit accounting correctly measures cluster value rather than only direct EOA USDT receipts.
The profit note removes the last ambiguity from the earlier draft. It values the attacker cluster at the two boundary states using public Pancake quotes for WBNB and GDS plus a pro-rata LP reserve valuation:
value_before = 6174060786170403347613 + 437524092561413734296 + 8861694463053692378
= 6620446573194870774287
value_after = 39201649548418393336468 + 54060786170403347613
+ 156510590418258333543429 + 219013934117602517861
+ 7778057157855309894
= 195993092744122588055265
net_delta = 189372646170927717280978
5. Adversary Flow Analysis
The adversary used a two-phase sequence.
-
0xf9b6cc083f6e0e41ce5e5dd65b294abf577ef47c7056d86315e5e53aa662251eThe sender EOA0xcf2362b46669e04b16d0780cf9b6e61c82de36a7called setup orchestrator0x16059b0b6842b33c088b3246e5b7afddd9dffb4b. The orchestrator created helper contracts, gave each helper temporary LP solastEpochwould initialize at 29, burned enough GDS to activate the helper, and returned the LP so each helper ended the block with zero LP but nonzero residual GDS. -
Epoch rollover between the two transactions Because GDS increments epochs based on block progression,
currentEpochmoved from 29 to 30 while the helpers still carried stale reward state from epoch 29. -
0x2bb704e0d158594f7373ec6e53dc9da6c6639f269207da8dab883fc3b5bf6694The sender EOA called harvest orchestrator0x0b995c08abddc0442bee87d3a7c96b227f8e7268. The orchestrator transferred the same LP position into helper0x0f8d735c0b67f845068bb31684707851f9d2767d, triggered the helper burn path, recovered the LP, and repeated that sequence across additional helpers such as0x02695ed7759d01d1daf76a9a351dbaad8ba07078. -
Realization Each helper independently received an LP reward from
lpPoolContractbecause GDS observed a live LP balance together with stalelastEpoch = 29. The harvested GDS was then sold through Pancake into USDT, while one orchestrator retained additional GDS value after the swap sequence.
This flow satisfies the ACT model. No private keys, privileged protocol roles, or nonpublic data were required. The attacker only needed public on-chain liquidity, public contract interfaces, and the ability to deploy arbitrary helper contracts.
6. Impact & Losses
The immediate protocol loss was depletion of the shared LP reward pool. In the harvest transaction alone, 14374634234643021506248017 GDS left 0xdd3e3384ae10b295fb353b1bda4fd3776bc4b650. Because the reward accounting could be replayed across many stale helpers, the pool was debited multiple times for what was economically the same LP position.
The attacker realized value in two forms. The EOA directly received 39201649548418393336468 raw USDT units, and the harvest orchestrator retained 11112305132080436192256555 GDS, which the profit note values through the public Pancake route. At the cluster level, the boundary-state value increased by 189372646170927717280978 raw USDT units, with gas costs of 84742471815873811570 raw USDT units already reflected in the before/after portfolio values.
The practical effect is broader than the raw token loss. GDS LP mining no longer enforced one-claim-per-position semantics, so any attacker who understood the contract could transform one LP balance into repeated historical claims by cycling it through pre-primed helpers.
7. References
- Setup tx:
0xf9b6cc083f6e0e41ce5e5dd65b294abf577ef47c7056d86315e5e53aa662251e - Harvest tx:
0x2bb704e0d158594f7373ec6e53dc9da6c6639f269207da8dab883fc3b5bf6694 - Victim token:
0xc1bb12560468fb255a8e8431bdf883cc4cb3d278 - GDS/USDT Pancake pair:
0x4526c263571eb57110d161b41df8fd073df3c44a - LP reward pool:
0xdd3e3384ae10b295fb353b1bda4fd3776bc4b650 - Attacker EOA:
0xcf2362b46669e04b16d0780cf9b6e61c82de36a7 - Setup orchestrator:
0x16059b0b6842b33c088b3246e5b7afddd9dffb4b - Harvest orchestrator:
0x0b995c08abddc0442bee87d3a7c96b227f8e7268 - Representative stale helper:
0x0f8d735c0b67f845068bb31684707851f9d2767d - Supporting evidence used for validation: verified
GDS.sol, setup and harvest metadata, setup and harvest cast traces, setup and harvest balance diffs, the auditor evidence note, and the refreshed deterministic profit accounting note.