One Shot: Reloaded

First Flight #47
Beginner FriendlyNFT
100 EXP
Submission Details
Impact: medium
Likelihood: medium

M02. Insecure Randomness / RNG Manipulation in go_on_stage_or_battle

Author Revealed upon completion

Root + Impact

Description

The intended behavior of go_on_stage_or_battle is that after both defender and challenger stake their NFT and CRED tokens, the contract determines the winner based on each rapper’s skill plus a random draw. This randomness should make the outcome unpredictable and fair.

However, the contract currently uses timestamp::now_seconds() % total_skill as the random number source. On mainnet, block timestamps are predictable and partially influenced by validators, making them an insecure source of randomness. An attacker can exploit this by submitting or re-submitting their transaction at moments when the timestamp modulo falls in their favor, tilting outcomes unfairly.

let defender_skill = one_shot::skill_of(arena.defender_token_id);
let challenger_skill = one_shot::skill_of(chall_token_id);
let total_skill = defender_skill + challenger_skill;
@> let rnd = timestamp::now_seconds() % (if (total_skill == 0) { 1 } else { total_skill });
@> let winner = if (rnd < defender_skill) { defender_addr } else { chall_addr };

Risk

Likelihood:

  • Timestamps are predictable at the second level, so attackers can time transactions to land in favorable slots.

  • Validators have discretion over acceptable block timestamps (within protocol bounds), allowing them to bias results.

Impact:

  • Attackers can systematically bias outcomes in their favor by timing or coordinating transactions.

  • This breaks fairness: battle outcomes are no longer random but subject to manipulation.


Proof of Concept

// Scenario:
// defender_skill = 70, challenger_skill = 30
// total_skill = 100
// If timestamp % 100 < 70 => defender wins
// If timestamp % 100 >= 70 => challenger wins
// Challenger observes current block timestamp = 170
// 170 % 100 = 70 -> challenger wins
rap_battle::go_on_stage_or_battle(&challenger, challenger_token, bet_amount);
// Challenger can retry until their transaction lands on a favorable timestamp.

On mainnet, this attack is feasible because block timestamps are visible in mempool and transaction inclusion can be timed or influenced.


Recommended Mitigation

Use the Aptos randomness API (aptos_framework::randomness) instead of timestamps for winner selection.

- let rnd = timestamp::now_seconds() % (if (total_skill == 0) { 1 } else { total_skill });
+ let rnd = if (total_skill == 0) {
+ 0
+ } else {
+ aptos_framework::randomness::u64_range(0, total_skill)
+ };

This ensures unbiased, unpredictable randomness that cannot be manipulated by players or validators.

Support

FAQs

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