One Shot: Reloaded

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

Challenger can manipulate outcome by timing transaction

Author Revealed upon completion

Root + Impact

Description

  • The function is intended to determine a fair winner between the defender and the challenger by comparing their skill levels and applying randomness. A higher skill value should give a participant a proportionally higher chance of winning.

  • The function is intended to determine a fair winner between the defender and the challenger by comparing their skill levels and applying randomness. A higher skill value should give a participant a proportionally higher chance of winning.

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:

  • This will occur whenever the blockchain timestamp produces a value where timestamp % total_skill falls into the challenger’s guaranteed win range (e.g., 108 or 109 in the case of defender skill = 8 and challenger skill = 2). At those exact seconds, the challenger deterministically wins and can time their transaction submission to align with these moments.

Impact:

  • The challenger with lower skill can bypass the intended probability distribution and secure guaranteed victories, undermining fairness and potentially draining rewards from higher-skilled defenders.


Proof of Concept

  1. Read on-chain defender and challenger skills (public).

  2. Off-chain compute seconds t where t % total_skill is within challenger winning range.

  3. At such a second, that triggers the vulnerable go_on_stage_or_battle. The attacker’s tx will be mined within that second and deterministically win.

Recommended Mitigation

The original battle function used timestamp::now_seconds() % total_skill to determine the winner, which is predictable and allows challengers to calculate exactly when they will win, breaking fairness even if their skill is lower. The recommended mitigation is to replace this with Aptos Roll’s VRF-based randomness using let r: u64 = randomness::u64_integer(); let rnd: u8 = ((r % 10) as u8) + 1;, which is cryptographically secure and unpredictable. This ensures that outcomes cannot be manipulated or timed by any player or miner, while still allowing defenders with higher skill to retain a proportionally higher probability of winning, preserving fairness and skill weighting in the game.

- remove this code
let rnd = timestamp::now_seconds() % (if (total_skill == 0) { 1 } else { total_skill });
+ add this code
let r: u64 = randomness::u64_integer();
let rnd: u8 = ((r % 10) as u8) + 1;

Support

FAQs

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