One Shot: Reloaded

First Flight #48
Beginner FriendlyNFT
100 EXP
View results
Submission Details
Impact: medium
Likelihood: high
Invalid

Arena griefing (no timeout for defenders)

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();
}
Updates

Lead Judging Commences

bube Lead Judge about 1 month ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

The defender can't cancel the battle if there is no challenger

There is no security impact on the protocol from this issue. The defender should wait until the challenger joins, this is intended behavior.

Support

FAQs

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