One Shot: Reloaded

First Flight #47
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 16 days ago
Submission Judgement Published
Validated
Assigned finding tags:

Predictable randomness

Appeal created

berkep1914 Submitter
15 days ago
bube Lead Judge
15 days ago
berkep1914 Submitter
15 days ago
bube Lead Judge
15 days ago
bube Lead Judge 14 days 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.