One Shot: Reloaded

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

Predictable Timestamp-Based Battle Outcomes Enable Complete Protocol Drainage

Author Revealed upon completion

Root + Impact

Description

  • The RapBattle protocol is designed to facilitate fair head-to-head battles between Rapper NFTs using on-chain randomness to determine winners. Each battle should have genuinely unpredictable outcomes based on participant skill levels, ensuring that no player can systematically game the battle system. The randomness mechanism is intended to prevent exploitation while maintaining competitive integrity based on NFT training and skill development.

  • The go_on_stage_or_battle() function uses timestamp::now_seconds() as its sole source of randomness, making battle outcomes completely predictable. The outcome formula rnd = timestamp % total_skill followed by winner = if (rnd < defender_skill) { defender } else { challenger } allows sophisticated attackers to calculate exact battle results before committing funds and manipulate transaction timing to guarantee victory. This predictable behavior violates the protocol's core fairness assumption and enables systematic drainage of the entire battle prize pool system.


The vulnerability stems from the randomness implementation in the battle resolution logic:

// rap_battle.move lines 91-93
let total_skill = defender_skill + challenger_skill;
let rnd = timestamp::now_seconds() % (if (total_skill == 0) { 1 } else { total_skill }); @> // Predictable timestamp source
let winner = if (rnd < defender_skill) { defender_addr } else { chall_addr }; @> // Deterministic outcome calculation

Risk

Likelihood:

  • Technical users routinely analyze smart contract code before participating in DeFi protocols, making discovery of the predictable timestamp formula highly probable during normal due diligence processes.

  • Battle timing is entirely user-controlled through the go_on_stage_or_battle() function, enabling attackers to monitor the mempool, calculate exact timestamps when their transactions will execute, and only submit transactions for battles they're mathematically guaranteed to win.

Impact:

  • Systematic battle manipulation enables sophisticated attackers to achieve 100% win rates while honest players experience normal randomness, creating complete economic extraction from all battle prize pools.

  • Protocol integrity violation destroys the core competitive gaming experience, leading to honest player exodus, protocol abandonment, and total system collapse when the predictable outcome pattern becomes publicly known.

Proof of Concept


The following test demonstrates the predictable nature of battle outcomes by successfully predicting exact winners based on timestamp manipulation:

#[test]
public fun test_timestamp_rng_exploitation() {
let defender_skill = 65;
let challenger_skill = 60;
let total_skill = defender_skill + challenger_skill; // 125
// Test Case 1: Timestamp guaranteeing defender victory
let timestamp_1000 = 1000;
let rnd_1000 = timestamp_1000 % total_skill; // 1000 % 125 = 0
let defender_wins_1000 = rnd_1000 < defender_skill; // 0 < 65 = true
assert!(defender_wins_1000 == true, 1);
// Test Case 2: Timestamp guaranteeing challenger victory
let timestamp_1065 = 1065;
let rnd_1065 = timestamp_1065 % total_skill; // 1065 % 125 = 65
let defender_wins_1065 = rnd_1065 < defender_skill; // 65 < 65 = false
assert!(defender_wins_1065 == false, 2);
// Attacker can predict ANY battle outcome before committing funds
// Only participates in guaranteed victories, draining all honest players
}

Recommended Mitigation

Replace the predictable timestamp-based randomness with proper cryptographic randomness using Aptos Framework's secure randomness API:

#[randomness]
public entry fun go_on_stage_or_battle(
player: &signer,
rapper_token: Object<Token>,
bet_amount: u64
) acquires BattleArena {
// ... battle setup logic ...
- let rnd = timestamp::now_seconds() % (if (total_skill == 0) { 1 } else { total_skill });
+ let secure_random = randomness::u64_range(0, total_skill);
+ let rnd = secure_random;
let winner = if (rnd < defender_skill) { defender_addr } else { chall_addr };
// ... rest of battle logic ...
}

Support

FAQs

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