+ #[test_only]
+ public fun mint_rapper_and_get_id(module_owner: &signer, to: address): address
+ acquires Collection, RapperStats {
+ let owner_addr = signer::address_of(module_owner);
+ assert!(owner_addr == @battle_addr, 1 /* E_NOT_AUTHORIZED */);
+
+ // 🔧 Lazy-init if needed (unit tests don’t auto-run init_module)
+ if (!exists<Collection>(@battle_addr)) {
+ init_module(module_owner);
+ };
+ if (!exists<RapperStats>(@battle_addr)) {
+ // This case shouldn’t happen if init_module just ran, but keep it safe.
+ let stats_table = table::new<address, StatsData>();
+ let owner_table = table::new<address, u64>();
+ move_to(module_owner, RapperStats { stats: stats_table, owner_counts: owner_table });
+ };
+
+ // Safe to assume collection/stats exist now
+ let _ = borrow_global<Collection>(@battle_addr);
+
+ let tok_ref = token::create(
+ module_owner,
+ string::utf8(COLLECTION_NAME),
+ string::utf8(b"A new rapper enters the scene."),
+ string::utf8(b"Rapper"),
+ option::none(),
+ string::utf8(b""),
+ );
+ let token_obj = object::object_from_constructor_ref<token::Token>(&tok_ref);
+ let token_id = object::address_from_constructor_ref(&tok_ref);
+
+ let stats_res = borrow_global_mut<RapperStats>(@battle_addr);
+ table::add(&mut stats_res.stats, token_id, StatsData {
+ owner: to,
+ weak_knees: true,
+ heavy_arms: true,
+ spaghetti_sweater: true,
+ calm_and_ready: false,
+ battles_won: 0,
+ });
+ // increment owner count
+ if (table::contains(&stats_res.owner_counts, to)) {
+ let cnt = table::borrow_mut(&mut stats_res.owner_counts, to);
+ *cnt = *cnt + 1;
+ } else {
+ table::add(&mut stats_res.owner_counts, to, 1);
+ };
+
+ event::emit(MintRapperEvent { minter: to, token_id });
+
+ object::transfer(module_owner, token_obj, to);
+ token_id
+ }
+
+ #[test_only]
+ public fun read_stats_for_test(token_id: address): (bool, bool, bool, bool, u64)
+ acquires RapperStats {
+ read_stats(token_id)
+ }
module battle_addr::arena_lock_simple_test {
use std::signer;
use std::debug;
use aptos_framework::account;
use battle_addr::one_shot;
use battle_addr::rap_battle;
use aptos_framework::object::{Self, Object};
use aptos_token_v2::token::Token;
#[test]
fun test_nft_locked_in_arena_permanently() {
debug::print(&std::string::utf8(b"=== TEST: NFT Locked in Arena Permanently ==="));
let module_owner = account::create_account_for_test(@battle_addr);
let player = account::create_account_for_test(@0x123);
debug::print(&std::string::utf8(b"[SUCCESS] Created test accounts"));
debug::print(&signer::address_of(&player));
rap_battle::init_for_test(&module_owner);
debug::print(&std::string::utf8(b"[SUCCESS] Initialized rap_battle module"));
let token_id = one_shot::mint_rapper_and_get_id(&module_owner, signer::address_of(&player));
let token_obj = object::address_to_object<Token>(token_id);
debug::print(&std::string::utf8(b"[SUCCESS] Minted NFT for player"));
debug::print(&token_id);
let initial_balance = one_shot::balance_of(signer::address_of(&player));
debug::print(&std::string::utf8(b"Player initial NFT balance:"));
debug::print(&initial_balance);
assert!(initial_balance == 1, 1001);
debug::print(&std::string::utf8(b"Player entering arena..."));
rap_battle::go_on_stage_or_battle(&player, token_obj, 0);
debug::print(&std::string::utf8(b"[SUCCESS] Player entered arena successfully"));
let player_balance_after = one_shot::balance_of(signer::address_of(&player));
debug::print(&std::string::utf8(b"Player NFT balance after entering arena:"));
debug::print(&player_balance_after);
assert!(player_balance_after == 0, 1002);
let contract_balance = one_shot::balance_of(@battle_addr);
debug::print(&std::string::utf8(b"Contract NFT balance:"));
debug::print(&contract_balance);
assert!(contract_balance == 1, 1003);
debug::print(&std::string::utf8(b"[WARNING] VULNERABILITY CONFIRMED:"));
debug::print(&std::string::utf8(b" - NFT is now locked in arena"));
debug::print(&std::string::utf8(b" - No cancel_challenge() function exists"));
debug::print(&std::string::utf8(b" - No timeout mechanism available"));
debug::print(&std::string::utf8(b" - Player has permanently lost NFT unless opponent appears"));
debug::print(&std::string::utf8(b"=== TEST COMPLETED ==="));
}
#[test]
fun test_arena_blocked_for_other_players() {
debug::print(&std::string::utf8(b"=== TEST: Arena Blocked for Other Players ==="));
let module_owner = account::create_account_for_test(@battle_addr);
let player1 = account::create_account_for_test(@0x123);
let player2 = account::create_account_for_test(@0x456);
debug::print(&std::string::utf8(b"[SUCCESS] Created test accounts for 2 players"));
debug::print(&signer::address_of(&player1));
debug::print(&signer::address_of(&player2));
rap_battle::init_for_test(&module_owner);
debug::print(&std::string::utf8(b"[SUCCESS] Initialized rap_battle module"));
let token_id1 = one_shot::mint_rapper_and_get_id(&module_owner, signer::address_of(&player1));
let token_obj1 = object::address_to_object<Token>(token_id1);
debug::print(&std::string::utf8(b"[SUCCESS] Minted NFT for player1"));
debug::print(&token_id1);
let token_id2 = one_shot::mint_rapper_and_get_id(&module_owner, signer::address_of(&player2));
let token_obj2 = object::address_to_object<Token>(token_id2);
debug::print(&std::string::utf8(b"[SUCCESS] Minted NFT for player2"));
debug::print(&token_id2);
let player1_initial = one_shot::balance_of(signer::address_of(&player1));
let player2_initial = one_shot::balance_of(signer::address_of(&player2));
debug::print(&std::string::utf8(b"Player1 initial balance:"));
debug::print(&player1_initial);
debug::print(&std::string::utf8(b"Player2 initial balance:"));
debug::print(&player2_initial);
assert!(player1_initial == 1, 2001);
assert!(player2_initial == 1, 2002);
debug::print(&std::string::utf8(b"Player1 entering arena..."));
rap_battle::go_on_stage_or_battle(&player1, token_obj1, 0);
debug::print(&std::string::utf8(b"[SUCCESS] Player1 entered arena successfully"));
let player1_after = one_shot::balance_of(signer::address_of(&player1));
let contract_balance = one_shot::balance_of(@battle_addr);
debug::print(&std::string::utf8(b"Player1 balance after entering arena:"));
debug::print(&player1_after);
debug::print(&std::string::utf8(b"Contract balance:"));
debug::print(&contract_balance);
assert!(player1_after == 0, 2003);
assert!(contract_balance == 1, 2004);
let player2_after = one_shot::balance_of(signer::address_of(&player2));
debug::print(&std::string::utf8(b"Player2 balance (should still have NFT):"));
debug::print(&player2_after);
assert!(player2_after == 1, 2005);
debug::print(&std::string::utf8(b"[WARNING] VULNERABILITY CONFIRMED:"));
debug::print(&std::string::utf8(b" - Arena is now blocked indefinitely"));
debug::print(&std::string::utf8(b" - Player2 cannot enter arena alone"));
debug::print(&std::string::utf8(b" - No timeout mechanism to free the arena"));
debug::print(&std::string::utf8(b" - No cancel mechanism for player1"));
debug::print(&std::string::utf8(b" - Arena remains locked until opponent appears"));
debug::print(&std::string::utf8(b"=== TEST COMPLETED ==="));
}
#[test]
fun test_no_cancellation_mechanism() {
debug::print(&std::string::utf8(b"=== TEST: No Cancellation Mechanism ==="));
let module_owner = account::create_account_for_test(@battle_addr);
let player = account::create_account_for_test(@0x123);
debug::print(&std::string::utf8(b"[SUCCESS] Created test accounts"));
debug::print(&signer::address_of(&player));
rap_battle::init_for_test(&module_owner);
debug::print(&std::string::utf8(b"[SUCCESS] Initialized rap_battle module"));
let token_id = one_shot::mint_rapper_and_get_id(&module_owner, signer::address_of(&player));
let token_obj = object::address_to_object<Token>(token_id);
debug::print(&std::string::utf8(b"[SUCCESS] Minted NFT for player"));
debug::print(&token_id);
// Verify initial state
let initial_balance = one_shot::balance_of(signer::address_of(&player));
let initial_contract_balance = one_shot::balance_of(@battle_addr);
debug::print(&std::string::utf8(b"Player initial NFT balance:"));
debug::print(&initial_balance);
debug::print(&std::string::utf8(b"Contract initial NFT balance:"));
debug::print(&initial_contract_balance);
assert!(initial_balance == 1, 3001);
assert!(initial_contract_balance == 0, 3002);
debug::print(&std::string::utf8(b"Player entering arena..."));
rap_battle::go_on_stage_or_battle(&player, token_obj, 0);
debug::print(&std::string::utf8(b"[SUCCESS] Player entered arena successfully"));
let final_player_balance = one_shot::balance_of(signer::address_of(&player));
let final_contract_balance = one_shot::balance_of(@battle_addr);
debug::print(&std::string::utf8(b"Player final NFT balance:"));
debug::print(&final_player_balance);
debug::print(&std::string::utf8(b"Contract final NFT balance:"));
debug::print(&final_contract_balance);
assert!(final_player_balance == 0, 3003);
assert!(final_contract_balance == 1, 3004);
debug::print(&std::string::utf8(b"[CRITICAL] VULNERABILITY CONFIRMED:"));
debug::print(&std::string::utf8(b" - NFT is permanently locked in arena"));
debug::print(&std::string::utf8(b" - No cancel_challenge() function exists in contract"));
debug::print(&std::string::utf8(b" - No automatic timeout mechanism implemented"));
debug::print(&std::string::utf8(b" - Player is stuck and cannot retrieve their NFT"));
debug::print(&std::string::utf8(b" - This constitutes an Arena Lock DoS vulnerability"));
debug::print(&std::string::utf8(b" - Only solution: wait indefinitely for an opponent"));
debug::print(&std::string::utf8(b"=== TEST COMPLETED ==="));
}
}
The protocol must implement a mechanism for defenders to withdraw their assets if a battle does not occur. A combination of the following solutions is recommended.