We do not have a reliable USD price for the recorded assets yet.
0x8d5fb97b35b830f8addcf31c8e0c6135f15bbc2163d891a3701ada0ad654d4270x71e3056aa4985de9f5441f079e6c74454a3c95f0BSCOn BNB Chain transaction 0x8d5fb97b35b830f8addcf31c8e0c6135f15bbc2163d891a3701ada0ad654d427, attacker path 0x36a6135672035507b772279d99a9f7445f2d1601 -> 0x471038827c05c87c23e9dba5331c753337fd918b executed 50 consecutive winning bets against RedKeysGame at 0x71e3056aa4985de9f5441f079e6c74454a3c95f0. The game transferred out 150000000000 raw REDKEYS, received 50000000000 raw REDKEYS in stakes, and lost a net 100000000000 raw REDKEYS.
The root cause is deterministic pseudo-randomness. RedKeysGame derived each bet result from public block fields plus msg.sender, so the bettor could compute the next winning choice before calling playGame.
RedKeysGame is a token betting contract that accepts a choice, ratio, and amount. For a correct guess it pays amount * benefit, where benefit = ratios[ratio]. At the exploitable pre-state, block 39079951, was and was , so a correct ratio-2 bet turned a raw-token stake into a raw-token payout.
counter()122ratios(2)310000000003000000000The profit asset was REDKEYS at 0x00e62b6ccf1fe3e5e01ce07f6232d7f378518b6b, which uses 4 decimals. The maximum stake used in the exploit loop was 100000.0000 REDKEYS (1000000000 raw units), and each win produced 200000.0000 REDKEYS of net profit.
The vulnerability class is attacker-solvable randomness in an adversarial game. playGame increments counter, computes _betResult = uint16(randomNumber()) % ratio, and pays the caller when choice == _betResult. The randomness source does not include any entropy hidden from the caller. Instead it depends on public block metadata, the current counter state, and msg.sender.
That breaks the core safety invariant for betting systems: the winning outcome must remain unpredictable to the bettor until the stake is committed. Here, the attacker could read the current counter, fix the caller address, evaluate the same expression locally, and submit a bet with the exact winning choice. Because ratios[2] == 3, each correctly predicted ratio-2 bet had positive expected value and, in practice, guaranteed profit.
// Verified RedKeysGame logic, summarized from the referenced source
function playGame(uint16 choice, uint16 ratio, uint256 amount) external {
uint16 benefit = ratios[ratio];
redkeys.transferFrom(msg.sender, address(this), amount);
counter += 1;
uint16 result = uint16(randomNumber()) % ratio;
if (choice == result) {
redkeys.transfer(msg.sender, amount * benefit);
}
}
The exploitable pre-state was fixed and public. Before the seed transaction, counter() returned 122 and the profitable ratio schedule remained active with ratios(2) == 3. The attacker only needed enough REDKEYS for the first stake and approval for the game contract.
The helper contract then executed a deterministic loop. On each iteration it read the current counter, predicted the next modulo-2 result from the same block context the contract would use, and called playGame with the matching choice. Because the transaction executed within one block, all relevant block fields were stable across the loop and msg.sender was the helper address on every call.
The recorded seed trace shows the exact exploit mechanics: a stake transfer from the helper into the game, a larger payout back to the helper, and the Game event for each created bet.
Trace excerpt from the seed transaction
[5667868] 0x471038827C05C87C23E9dba5331c753337FD918B::beginWork(50)
├─ RedKeysGame::counter() -> 122
├─ RedKeysGame::playGame(0, 2, 1000000000)
│ ├─ RedKeys::transferFrom(helper, RedKeysGame, 1000000000)
│ ├─ RedKeys::transfer(RedKeysGame, helper, 3000000000)
│ ├─ emit Game(_id: 123)
│ └─ storage: counter 122 -> 123
├─ RedKeysGame::counter() -> 123
└─ ...
The final balance diff confirms the incident-level consequence. The attacker helper gained 100000000000 raw REDKEYS and the game lost the same amount.
{
"helper_delta": "100000000000",
"game_delta": "-100000000000",
"token": "REDKEYS"
}
The adversary flow is a single-transaction ACT execution:
0x36a6135672035507b772279d99a9f7445f2d1601 called helper contract 0x471038827c05c87c23e9dba5331c753337fd918b with beginWork(50).playGame result for ratio=2, staked 1000000000 raw REDKEYS, and matched the winning branch.1000000000 raw REDKEYS into the game and 3000000000 raw REDKEYS back out.123 through 172, and the helper realized 100000000000 raw REDKEYS of net profit.The helper contract was attacker-controlled, but it was not a privileged dependency. The exploit remained ACT because any unprivileged actor could deploy their own helper or call the vulnerable game directly after computing the same deterministic outcome.
RedKeysGame lost 100000000000 raw REDKEYS, equal to 10,000,000.0000 REDKEYS, in the seed transaction alone. The loss was direct treasury depletion from the game's token inventory. The exploit required no privileged access, no stolen keys, and no hidden off-chain capability beyond computing the public pseudo-random output.
0x8d5fb97b35b830f8addcf31c8e0c6135f15bbc2163d891a3701ada0ad654d427https://bscscan.com/address/0x71e3056aa4985de9f5441f079e6c74454a3c95f0#codehttps://bscscan.com/address/0x00e62b6ccf1fe3e5e01ce07f6232d7f378518b6b#code