One Shot: Reloaded

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

Deterministic Skill System Enables Strategic Battle Outcome Manipulation

Author Revealed upon completion

Root + Impact

Description

  • The RapBattle protocol is designed to create engaging strategic gameplay where Rapper NFT battles depend on skill levels that players develop through staking and training over time. The skill system should provide meaningful variety and unpredictability in battle matchups, preventing players from easily calculating optimal strategies or guaranteeing favorable battle outcomes. The training mechanism is intended to reward time investment while maintaining competitive balance across different skill levels

  • The skill_of() function implements a completely deterministic calculation system with only 6 possible skill values (50, 55, 60, 65, 70, 75) generated from 4 boolean flags. The predictable progression formula allows sophisticated players to calculate exact skill values for any NFT, pre-compute battle probabilities for all 36 possible matchup combinations, and systematically target vulnerable opponents. This deterministic behavior enables strategic exploitation of the limited skill variance and destroys competitive unpredictability.

// one_shot.move lines 159-166
public(friend) fun skill_of(token_id: address): u64 acquires RapperStats {
let s = table::borrow(&stats_res.stats, token_id);
let after1 = if (s.weak_knees) { 65 - 5 } else { 65 }; @> // Predictable -5 penalty
let after2 = if (s.heavy_arms) { after1 - 5 } else { after1 }; @> // Predictable -5 penalty
let after3 = if (s.spaghetti_sweater) { after2 - 5 } else { after2 }; @> // Predictable -5 penalty
let final_skill = if (s.calm_and_ready) { after3 + 10 } else { after3 }; @> // Predictable +10 bonus
final_skill
}

Risk

Likelihood:

  • The skill calculation logic is publicly visible on-chain and easily reverse-engineered by any player examining the protocol.

  • Staking duration directly correlates to skill improvements in a completely predictable manner, enabling players to identify optimal training periods. Battle matchmaking relies on user-initiated challenges, allowing sophisticated players to analyze opponent NFT skill levels before committing to battles.

Impact:

  • Strategic skill manipulation enables experienced players to achieve systematic 60% win rates against fresh NFTs (75 vs 50 skill) while avoiding unfavorable matchups entirely.

  • Protocol gameplay becomes deterministic rather than engaging, leading to player frustration, reduced participation from casual users, and economic extraction by sophisticated players who can calculate optimal battle strategies. The limited skill variance destroys long-term protocol sustainability and competitive integrity.

Proof of Concept

The following analysis demonstrates the complete predictability of the skill system:

#[test]
public fun test_skill_calculation_predictability() {
// All possible skill combinations (2^4 = 16 combinations, only 6 unique values)
// Fresh mint (all vices, no virtue): 65 - 5 - 5 - 5 + 0 = 50
let fresh_skill = 50;
// Maximum trained (no vices, virtue): 65 - 0 - 0 - 0 + 10 = 75
let max_skill = 75;
// All possible skill values: [50, 55, 60, 65, 70, 75]
let skill_range = max_skill - fresh_skill; // Only 25 point spread
// Battle probability calculation for worst-case matchup:
let total_skill = max_skill + fresh_skill; // 125
let trained_advantage = max_skill; // 75
let win_probability = trained_advantage * 100 / total_skill; // 60%
assert!(skill_range == 25, 1);
assert!(win_probability == 60, 2);
// Sophisticated players can pre-calculate all 36 possible matchup outcomes
// and selectively engage only in favorable battles
}

Recommended Mitigation

Implement a more complex skill system with wider variance and randomness elements:

public(friend) fun skill_of(token_id: address): u64 acquires RapperStats {
let s = table::borrow(&stats_res.stats, token_id);
- let after1 = if (s.weak_knees) { 65 - 5 } else { 65 };
- let after2 = if (s.heavy_arms) { after1 - 5 } else { after1 };
- let after3 = if (s.spaghetti_sweater) { after2 - 5 } else { after2 };
- let final_skill = if (s.calm_and_ready) { after3 + 10 } else { after3 };
+ let base_skill = 500; // Wider base range
+ let vice_penalty = randomness::u64_range(20, 80); // Variable penalties
+ let virtue_bonus = randomness::u64_range(50, 150); // Variable bonuses
+ let training_multiplier = calculate_training_bonus(s.days_trained);
+ let final_skill = (base_skill - vice_penalties + virtue_bonuses) * training_multiplier / 100;
final_skill
}

Support

FAQs

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