One Shot: Reloaded

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

L01. Zero-Stake Battle leads to NFT Loss

Author Revealed upon completion

Root + Impact

Description

The normal behavior of go_on_stage_or_battle is that two players stake an equal amount of CRED tokens along with their NFT rappers. The winner should receive the combined coin prize pool, and the losing player forfeits their NFT.

However, the function allows a defender to set bet_amount = 0. In this case, the challenger must also use a 0 bet. This results in an empty prize pool, but the losing player still permanently loses their NFT. This creates “loss-only” battles, with no upside for either participant, and enables griefing.

// Root cause in the codebase with @> marks to highlight the relevant section
if (arena.defender == @0x0) {
assert!(arena.defender_bet == 0, E_BATTLE_ARENA_OCCUPIED);
arena.defender = player_addr;
@> arena.defender_bet = bet_amount; // can be set to 0
...
@> let first_bet = coin::withdraw<cred_token::CRED>(player, bet_amount); // withdraw(0) is valid
coin::merge(&mut arena.prize_pool, first_bet); // pool remains empty
}

Risk

Likelihood:

  • Defenders can always initialize the arena with a 0 bet, since no validation prevents it.

  • Challengers must match this 0 bet, so the exploit path is straightforward.

Impact:

  • Players can be tricked into battles where one of them inevitably loses their NFT but gains no prize.

  • Attackers can grief by forcing zero-stake battles and stripping NFTs from others with no coin risk.

Proof of Concept

// Step 1: Defender calls go_on_stage_or_battle with bet_amount = 0 and a rapper token.
// Step 2: Challenger calls go_on_stage_or_battle with bet_amount = 0 and their rapper token.
// Step 3: A battle runs, but the prize pool is empty.
// Step 4: Loser loses their NFT permanently, winner receives no coins.

Recommended Mitigation

- arena.defender_bet = bet_amount;
+ assert!(bet_amount > 0, E_INVALID_BET_AMOUNT);
+ arena.defender_bet = bet_amount;

This ensures that all battles require a positive stake, preventing loss-only scenarios.

Support

FAQs

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