Description
Players can place bets with zero CRED amount when entering the battle arena as defenders. This completely breaks the game's economic model by allowing battles with no financial stakes, undermining the risk/reward mechanism that drives the protocol's tokenomics.
Root Cause
In rap_battle::go_on_stage_or_battle()
, there is no validation that the bet_amount
parameter is greater than zero. The function accepts any value including 0:
public entry fun go_on_stage_or_battle(
player: &signer,
rapper_token: Object<Token>,
bet_amount: u64
) acquires BattleArena {
if (arena.defender == @0x0) {
arena.defender_bet = bet_amount;
let first_bet = coin::withdraw<CRED>(player, bet_amount);
coin::merge(&mut arena.prize_pool, first_bet);
} else {
assert!(arena.defender_bet == bet_amount, E_BETS_DO_NOT_MATCH);
}
}
Key issues:
No minimum bet validation allows defenders to set 0 CRED bets
Challengers are forced to match the defender's bet amount (including 0)
coin::withdraw(player, 0)
succeeds, creating empty prize pools
Battles can occur with no economic stakes
Risk
Likelihood: High - Any player can easily call the function with bet_amount = 0
Impact: Medium - Breaks core game mechanics and tokenomics but no direct fund loss
Impact
Medium severity because:
-
Defeats game purpose: Battles become meaningless without financial stakes
-
Breaks tokenomics: CRED utility is undermined if battles don't require it
-
Arena griefing: Defenders can occupy the arena indefinitely with 0 bets, blocking real battles
-
Economic exploitation: Players can gain battle experience/wins without risk
Proof of Concept
This test demonstrate that a defender can griffe the battle with 0 token
#[test]
public fun test_farm_winning() {
let one_day = 86_400_000_000;
let aptos_framework = account::create_account_for_test(@0x1);
timestamp::set_time_has_started_for_testing(&aptos_framework);
timestamp::update_global_time_for_test(one_day);
coin::create_coin_conversion_map(&aptos_framework);
let module_owner = account::create_account_for_test(@battle_addr);
let minter = account::create_account_for_test(@minter_addr);
let attacker = account::create_account_for_test(@attacker_addr);
cred_token::init_module_test(&module_owner);
rap_battle::init_module_test(&module_owner);
one_shot::mint_rapper(&module_owner, signer::address_of(&minter));
let mint_events = event::emitted_events<MintRapperEvent>();
let first_event = vector::borrow(&mint_events, 0);
let token_id = one_shot::get_mint_event_token_id(first_event);
let rapper_token = object::address_to_object<Token>(
token_id
);
print(&rapper_token);
one_shot::mint_rapper(&module_owner, signer::address_of(&attacker));
let second_mint_events = event::emitted_events<MintRapperEvent>();
let second_event = vector::borrow(&second_mint_events, 1);
let token_id2 = one_shot::get_mint_event_token_id(second_event);
let rapper_token2 = object::address_to_object<Token>(
token_id2
);
print(&rapper_token2);
rap_battle::go_on_stage_or_battle(&minter, rapper_token, 0);
rap_battle::go_on_stage_or_battle(&attacker, rapper_token2, 0);
}
Recommended Mitigation
Add a minimum bet amount validation:
+ const E_INVALID_BET_AMOUNT: u64 = 3;
+ const MINIMUM_BET: u64 = 1; // Or higher value as desired
public entry fun go_on_stage_or_battle(
player: &signer,
rapper_token: Object<Token>,
bet_amount: u64
) acquires BattleArena {
+ assert!(bet_amount >= MINIMUM_BET, E_INVALID_BET_AMOUNT);
+
let player_addr = signer::address_of(player);
let arena = borrow_global_mut<BattleArena>(@battle_addr);
// ... rest of function unchanged
}
This ensures all battles have meaningful economic stakes while preserving the existing battle mechanics.