One Shot: Reloaded

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

Arena griefing (no timeout for defenders)

Author Revealed upon completion

Root + Impact

Description

  • Normal behavior: Battle arena should be accessible to all players with reasonable turnover times.

    The specific issue: Once a defender stages a battle, the arena remains occupied indefinitely until a challenger arrives. No timeout mechanism exists to clear abandoned battles, allowing single griefers to DoS the entire battle system.

// Root cause in the codebase with @> marks to highlight the relevant section
// Root cause - no timeout mechanism
if (arena.defender == @0x0) {
arena.defender = player_addr;
arena.defender_bet = bet_amount;
arena.defender_token_id = token_id;
// @> Arena now occupied indefinitely with no expiration
}

Risk


Likelihood:

  • Griefers can easily stage battles with minimal cost and abandon them

  • Players naturally stage battles and may forget or lose interest

  • Only one battle arena exists for the entire protocol

Impact:

  • Complete denial of service for battle functionality affecting all users

  • Economic griefing through minimal-cost arena occupation

  • Protocol becomes unusable if arena permanently occupied

  • No recovery mechanism for stuck or abandoned battles

Proof of Concept

Proof of Concept
Alice places her Rapper on stage with a 50 CRED bet.
No one challenges her for days.
She has no function to reclaim the Rapper or her bet.
Her assets are effectively lost in limbo unless protocol developers intervene

Recommended Mitigation

- remove this code
+ add this code
+ const BATTLE_TIMEOUT_SECONDS: u64 = 3600; // 1 hour timeout
+ struct BattleArena has key {
+ defender: address,
+ defender_bet: u64,
+ defender_token_id: address,
+ stage_timestamp: u64, // Track when battle was staged
+ prize_pool: Coin<cred_token::CRED>,
+ }
+ // Add timeout clearing mechanism
+ if (arena.defender != @0x0) {
+ let elapsed = timestamp::now_seconds() - arena.stage_timestamp;
+ if (elapsed > BATTLE_TIMEOUT_SECONDS) {
+ clear_expired_battle_and_refund(arena);
+ }
+ }
0r this
+ const TIMEOUT_SECS: u64 = 86400; // 1 day, example
public entry fun reclaim_defender(defender: &signer) acquires RapBattleArena {
let arena = borrow_global_mut<RapBattleArena>(@battle_addr);
let def = &mut arena.defender;
assert!(def.owner == signer::address_of(defender), E_NOT_OWNER);
assert!(timestamp::now_seconds() - def.start_time > TIMEOUT_SECS, E_TOO_EARLY);
// Return Rapper + bet
object::transfer(@battle_addr, def.rapper_token, def.owner);
coin::transfer(&mut arena.pool, def.owner, def.bet_amount);
arena.defender = empty_defender();
}

Support

FAQs

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