One Shot: Reloaded

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

Battle Outcomes are Predictable Due to Weak Randomness

Author Revealed upon completion

Root + Impact

Description

  • The battle system is supposed to use a random number to fairly decide the winner based on each Rapper's skill level.

  • The contract uses the block's timestamp, which is a public and predictable number, to determine the winner. This allows any user to calculate the result of a battle before it happens, removing any element of chance.

// filepath: sources/rap_battle.move
// ...existing code...
let total_skill = defender_skill + challenger_skill;
// The timestamp is predictable. An attacker knows what this value will be.
@> let rnd = timestamp::now_seconds() % (if (total_skill == 0) { 1 } else { total_skill });
// The outcome is determined by a simple comparison against the predictable number.
@> let winner = if (rnd < defender_skill) { defender_addr } else { chall_addr };

Risk

Likelihood:

  • This can be exploited in every battle by anyone who can read the current time of the blockchain.

Impact:

  • Guaranteed Wins/Losses: Attackers can see the outcome of a battle in advance and will only choose to participate in battles they are guaranteed to win. This breaks the core game mechanic.

  • Theft of Funds: Malicious players can consistently drain CRED tokens from honest players by only entering favorable battles, leading to direct financial loss for others.

  • Loss of Trust: Once players realize the game is rigged, they will no longer trust or use the protocol.

Proof of Concept

An attacker can easily predict the winner.

  1. An attacker sees a defender waiting in the arena.

  2. They calculate both Rappers' skill scores (defender_skill, challenger_skill).

  3. They read the current block timestamp and perform the same calculation the contract does: rnd = timestamp % total_skill.

  4. If the calculation shows they will win (rnd >= defender_skill), they join the battle. If not, they wait for the next block and try again.

This can be demonstrated with a simple function:

// An attacker can use a function like this off-chain to predict the winner
// before sending their transaction.
public fun predict_if_challenger_wins(defender_skill: u64, challenger_skill: u64): bool {
let total_skill = defender_skill + challenger_skill;
if (total_skill == 0) { return false };
// The attacker gets the current timestamp from a node.
let predictable_timestamp = aptos_framework::timestamp::now_seconds();
let rnd = predictable_timestamp % total_skill;
// They know the outcome before risking any funds.
let challenger_wins = (rnd >= defender_skill);
challenger_wins
}

Recommended Mitigation

Replace the insecure timestamp with the secure randomness provided by the Aptos framework. This ensures the outcome is unpredictable.

// filepath: sources/rap_battle.move
- let rnd = timestamp::now_seconds() % (if (total_skill == 0) { 1 } else { total_skill });
+ // Use the secure on-chain randomness API provided by the Aptos framework.
+ let rnd = aptos_framework::randomness::u64_range(0, total_skill);

Support

FAQs

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