On October 18, 2026, an attacker borrowed about 1.03 million USDC, deposited ALPS into AlpsCon, and collected rewards that had accumulated before the deposit existed. After closing the position and repaying the loan, the attacker received 152,295.36 USDC before gas.
The attack combined two problems. First, AlpsCon trusted a test wallet whose private key was publicly known. Second, its newer reward hubs counted a fresh stake from an older hub without giving it the correct reward starting point.
Attacker (EOA): https://bscscan.com/address/0x3d9DF7D20D8296286383ffe475aA7b9e548946b2
Attack Tx: https://bscscan.com/tx/0x824b83f94bdc2e69b81f1cb2dfdf9d4aeed066114e3de29e3af59bd2d9355fc2
Victim: https://bscscan.com/address/0x9B210Da482217afC24074e57f6F8337fe07F0124
System Analysis
The address 0xf39F…2266 is familiar to developers who use Hardhat or Anvil: it is the first account in their standard test setup. Anyone who knows its published private key can sign for it. Yet it had been on AlpsCon’s whitelist since August 2025.
For this attack, the wallet ran code through EIP-7702, which allows an account to delegate execution to a contract. Calls made through that code still came from the approved address, so they passed AlpsCon’s access checks.
The whitelist therefore offered no protection against someone using this account. It had remained trusted for roughly fourteen months.
AlpsCon had several reward hubs, numbered V6 through V9. They were linked: a newer hub could read a user’s balance from an earlier version. That arrangement allowed older positions to remain visible after an upgrade, but it also meant that a deposit into V6 could affect the reward calculation in V9.
V6 still accepted deposits. The attacker deposited there, then called its migrate() function to convert the pending amount into a locked balance. The ALPS stayed in V6. The later hubs could nevertheless count it through their legacy balance lookups.
The V9 code below shows where this happens. For an account that has not migrated into V9, _getUserLockedAmount() adds the locked amount returned by V8 to the local balance.
Counting the balance was not enough to calculate its rewards correctly. V9 also needed a starting value that excluded earnings from before those funds joined.
That starting value is entryProfitPerShare. The hub subtracts it from profitPerShare, the accumulated rewards per staked token, and multiplies the difference by the user’s balance. In this case, the new account’s starting value was still zero.

Consider a small example. Suppose the hub has accumulated 0.20 USDC per ALPS before a new position of 1,000 ALPS becomes visible. With an entry value of zero, the calculation credits:
1,000 × (0.20 − 0) = 200 USDC
These are illustrative amounts. The point is that all 200 USDC belongs to an earlier reward period. The new position has only just appeared, but the formula treats it as eligible for that history.
V9 does update entryProfitPerShare. The order of operations explains why that update did not save it.
In _updateUserProfitCheckpoint(), the contract calculates earnings using the existing entry value, adds the result to the user’s recorded earnings, and then advances the entry value. By then, the false entitlement is already in the account.
Using the example above, the checkpoint records 200 USDC before changing the entry value to 0.20. A later checkpoint would not credit that same history again. The first 200 USDC, however, remains recorded and can be paid.
The claim function runs this checkpoint before calculating the payment. Its earnings limit operates on a balance that already includes the incorrect reward.
There was a checkpoint; it settled rewards against the wrong starting value for the newly recognized stake.
Attack flow:
The attacker borrowed 1,027,990 USDC from Aave V3 and swapped it for about 9.97 million ALPS. V6’s purchaseServiceCredits() took an 8% deposit fee, leaving about 9.17 million ALPS pending. A call to V6’s migrate() then made that amount a locked balance.
Once the balance was visible to the newer hubs, V9’s claimServiceResults() paid 312,915.56 USDC to the delegated wallet after fees. The transaction also sent 20,553.69 USDC from V8 and 1,966.07 USDC from V7 directly to the attacker.
Those V7/V8 payments are confirmed by the receipt. Their full internal call path has not been reconstructed here, so they should not be assumed to come from a single call to V9.
The attacker then withdrew the V6 position. After the 5% withdrawal fee, approximately 8.71 million ALPS came back to the delegated wallet. Selling those tokens returned 845,364.05 USDC.
The wallet left 1,028,503.995 USDC available for Aave to collect, covering the principal and a 513.995 USDC premium. Before that repayment was pulled, it sent the remaining 129,775.61 USDC to the attacker.
Adding the V8 and V7 transfers gives the final receipts:
129,775.61 + 20,553.69 + 1,966.07 ≈ 152,295.36 USDC before gas
The loan, deposit, reward claims, withdrawal and repayment all fit into one transaction. The staking position did not need time to earn anything; the profit came from rewards that had accumulated earlier.
After the Attack
The test account was removed from the whitelist about five hours later. That closed access through this particular wallet, but the reward accounting needed a separate correction.
Whenever a newer hub starts counting a balance from an older version, its reward baseline must reflect when those funds became eligible. Otherwise, an old hub that still accepts deposits can provide a fresh balance with access to rewards from before it was deposited.




