One Shot: Reloaded

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

Insecure RNG in rap_battle

Description

The rap_battle module derives battle randomness from timestamp::now_seconds() making it insecure because:

  • Anyone can see the timestamp off-chain, making it predictable

  • Aptos validators can manipulate block timestamps within allowed drift.

let seed = timestamp::now_seconds();
let outcome = seed % 2; // determines winner

Risk

  • This creates an unfair battle system where malicious players consistently win, draining honest players’ Coin<CRED> wagers.


Impact

  • Players can locally pre-compute outcomes and only submit challenges they are sure to win.

  • Validators can bias outcomes by adjusting timestamps to favor their own Rapper.


Proof of Concept

Attacker can simulate results locally,

module attacker::rng_poc {
use std::debug;
use aptos_framework::timestamp;
public entry fun predict_battle() {
let seed = timestamp::now_seconds();
let outcome = seed % 2;
if (outcome == 0) {
debug::print(&b"Attacker loses this round");
} else {
debug::print(&b"Attacker wins this round");
};
}
}

Attack Flow

  • Attacker reads timestamp::now_seconds() off-chain

  • Runs same calculation locally to know the winner in advance

  • If outcome is favorable they submit battle transaction

  • If not they skip showing they only play battle they'll win

Recommended Mitigation

Replace timestamp::now_seconds() RNG with a commit–reveal scheme

module tests::commit_reveal_tests {
use std::signer;
use std::vector;
use std::hash;
use rap_battle::commit_reveal_rng;
#[test_only]
public fun test_commit_reveal_resolve() {
// Create two fake signers to represent Player A and Player B
let player_a = @0xA;
let player_b = @0xB;
// === Setup ===
commit_reveal_rng::init_module(&signer::spec_signer(player_a));
commit_reveal_rng::init_module(&signer::spec_signer(player_b));
let battle_id = 1u64;
// === Commit Phase ===
// Each player chooses a secret. In production, this is random & kept off-chain.
let secret_a = b"super_secret_A";
let secret_b = b"super_secret_B";
let commit_a = hash::sha3_256(
vector::concat(secret_a,
vector::concat(commit_reveal_rng::address_to_bytes(player_a),
commit_reveal_rng::u64_to_bytes(battle_id)))
);
let commit_b = hash::sha3_256(
vector::concat(secret_b,
vector::concat(commit_reveal_rng::address_to_bytes(player_b),
commit_reveal_rng::u64_to_bytes(battle_id)))
);
commit_reveal_rng::commit(&signer::spec_signer(player_a), battle_id, commit_a);
commit_reveal_rng::commit(&signer::spec_signer(player_b), battle_id, commit_b);
// === Reveal Phase ===
commit_reveal_rng::reveal(&signer::spec_signer(player_a), battle_id, secret_a);
commit_reveal_rng::reveal(&signer::spec_signer(player_b), battle_id, secret_b);
// === Resolve ===
let winner = commit_reveal_rng::resolve(battle_id, player_a, player_b);
// For test, we just check that winner is either A or B.
assert!(winner == player_a || winner == player_b, 100);
}
}

This ensures that neither player alone can bias randomness, validators cannot unilaterally change outcomes & battle fairness is preserved.

Until commit–reveal is implemented, outcomes should be clearly documented as pseudo-random and insecure, so players understand the risk.

Updates

Lead Judging Commences

bube Lead Judge 16 days ago
Submission Judgement Published
Validated
Assigned finding tags:

Predictable randomness

Appeal created

fredo182 Auditor
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.