One Shot: Reloaded

First Flight #48
Beginner FriendlyNFT
100 EXP
View results
Submission Details
Severity: high
Valid

Predictable RNG using timestamp::now_seconds() enables outcome manipulation

Root + Impact

Description

  • Normal behavior: The battle module (rap_battle.move) should determine the winner between two players in an unpredictable and fair way.

Issue: The code uses timestamp::now_seconds() as a randomness source, which is deterministic and predictable. Attackers can time transactions or repeatedly attempt until the result favors them.

// rap_battle.move
// RNG derived directly from timestamp
let rnd = timestamp::now_seconds()

Risk

Likelihood:

  • Every battle resolution relies on a predictable system value (timestamp::now_seconds()).

Validators and attackers can easily anticipate or influence timestamps, so manipulation can occur consistently during battles.

Impact:

  • Attackers can systematically bias outcomes and drain the CRED prize pools.

Honest players suffer repeated unfair losses, damaging trust in the protocol.

Proof of Concept

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;
contract VulnerableBattle {
// Winner selection using block.timestamp (similar to timestamp::now_seconds())
function getWinner(address player1, address player2) external view returns (address) {
uint rand = uint(block.timestamp) % 2; // predictable RNG
if (rand == 0) {
return player1;
} else {
return player2;
}
}
}
// ------------------------------
// Attacker Simulation
// ------------------------------
contract Attacker {
VulnerableBattle battle;
constructor(address _battle) {
battle = VulnerableBattle(_battle);
}
function manipulate(address defender, address challenger) external view returns (address) {
// Attacker checks the outcome *before* submitting a real transaction
// by simulating what block.timestamp % 2 would return.
uint rand = uint(block.timestamp) % 2;
if (rand == 1) {
// Only proceed if attacker is guaranteed to win
return challenger; // attacker wins
} else {
// Otherwise wait / resubmit in another block
return defender; // attacker loses, so they skip this block
}
}
}
  • VulnerableBattle mimics the rap_battle.move logic, but in Solidity style: randomness is block.timestamp.

  • Attacker repeatedly simulates the outcome by observing block.timestamp % 2.

  • The attacker waits or re-submits until they get a winning condition → biased outcome.


Recommended Mitigation

- let rnd = timestamp::now_seconds() % (if (total_skill == 0) { 1 } else { total_skill });
+ // Use commit–reveal seeds or Aptos randomness instead of timestamp
+ // Example: derive randomness from combined player seeds and block height
+ let combined = hash(player1_seed, player2_seed, block_height);
+ let rnd = combined % (if (total_skill == 0) { 1 } else { total_skill });
Updates

Lead Judging Commences

bube Lead Judge about 1 month ago
Submission Judgement Published
Validated
Assigned finding tags:

Predictable randomness

Appeal created

berkep1914 Submitter
about 1 month ago
bube Lead Judge
about 1 month ago
berkep1914 Submitter
about 1 month ago
bube Lead Judge
about 1 month ago
bube Lead Judge about 1 month ago
Submission Judgement Published
Validated
Assigned finding tags:

Predictable randomness

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.