One Shot: Reloaded

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

Self-challenge exploit (stat inflation / free wins)

Author Revealed upon completion

Root + Impact

Description

  • Normal behavior:
    A defender should place a CRED bet, and a different player should act as the challenger. The battle outcome is determined by Rapper stats and RNG, and the winner receives the prize pool.

    Issue:
    The contract never checks that the challenger’s address is different from the defender’s. Because of this, the same user can play both roles in a battle.
    This creates two problems:

    1. Stat Inflation: Players can farm unlimited “wins” for their own Rappers without risk. Since wins are supposed to signal competitive success, this breaks the reputation system.

    2. Economic Abuse: Users can stage fake battles with themselves to unlock staking rewards or trigger unintended side-effects, without actually putting capital at risk.

// Root cause in the codebase with @> marks to highlight the relevant section
// Root cause: missing check on challenger vs defender
public entry fun go_on_stage_or_battle(
defender: &signer,
rapper_token: object::Object<Rapper>,
bet: coin::Coin<CRED>,
) acquires RapBattleArena, RapperStats {
...
@> // No check that challenger != defender
let challenger_addr = signer::address_of(defender);
...
}

Risk

Likelihood:

  • Any player can do this immediately, no special conditions.

Impact:

  • Fake wins, stat inflation, possible manipulation of prize pools.

Proof of Concept

Alice stakes her Rapper.
Alice also challenges her own Rapper using the same signer.
Regardless of the outcome, Alice still controls both sides and can repeatedly generate “wins.”
This breaks the competitive fairness.

Recommended Mitigation

- remove this code
+ add this code
public entry fun go_on_stage_or_battle(
defender: &signer,
rapper_token: object::Object<Rapper>,
bet: coin::Coin<CRED>,
) acquires RapBattleArena, RapperStats {
let defender_addr = signer::address_of(defender);
...
- // missing check
+ let challenger_addr = signer::address_of(defender);
+ assert!(challenger_addr != defender_addr, E_SELF_BATTLE);

Support

FAQs

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