One Shot: Reloaded

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

The winner of a battle steal the looser's rapper

Author Revealed upon completion

Root + Impact

Description

Users can place their rapper NFTs in the arena for battle. After the battle, both NFTs should be send back to their original owner, but the code sends both of them to the winner of the battle.

The looser of the battle will have their NFTs stolen by the winner.

// in rap_battle::go_on_stage_or_battle
if (winner == defender_addr) {
coin::deposit(defender_addr, pool);
one_shot::increment_wins(arena.defender_token_id);
one_shot::transfer_record_only(arena.defender_token_id, @battle_addr, defender_addr);
@> one_shot::transfer_record_only(chall_token_id, @battle_addr, defender_addr);
} else {
coin::deposit(chall_addr, pool);
one_shot::increment_wins(chall_token_id);
@> one_shot::transfer_record_only(arena.defender_token_id, @battle_addr, chall_addr);
one_shot::transfer_record_only(chall_token_id, @battle_addr, chall_addr);
};

In the previous code snippet, we clearly see the loser's token being send to the winner.

Risk

Likelihood:

Happens after each and every battle, regardless of the winner.

Impact:

The loser looses their NFTs. This is not supposed to happen.

Proof of Concept

You will first need to add test_only functions in the code:

// In cred_token:
#[test_only]
public fun mint_cred(to: address,amount: u64) acquires CredCapabilities {
let caps = borrow_global<CredCapabilities>(@battle_addr);
let coins = coin::mint(amount, &caps.mint_cap);
coin::deposit(to, coins);
}
#[test_only]
public entry fun init(account: &signer) {
init_module(account);
}
// In one_shot:
#[test_only]
public fun get_mint_rapper_event_minter(event: &MintRapperEvent): address {
event.minter
}
#[test_only]
public fun get_mint_rapper_event_token_id(event: &MintRapperEvent): address {
event.token_id
}
// In rap_battle:
#[test_only]
public entry fun init(sender: &signer) {
init_module(sender);
}

Add this code in the test file:

#[test(
module_owner = @battle_addr, alice = @0x999, bob = @998, aptos_framework = @aptos_framework
)]
public fun test_steal_rapper(
module_owner: signer, alice: signer, bob: signer, aptos_framework: signer
) {
// Initialize the test framework
timestamp::set_time_has_started_for_testing(&aptos_framework);
aptos_coin::ensure_initialized_with_apt_fa_metadata_for_test();
let alice_address = signer::address_of(&alice);
let bob_address = signer::address_of(&bob);
// Initialize modules
cred_token::init(&module_owner);
rap_battle::init(&module_owner);
// Both users get one rapper
one_shot::mint_rapper(&module_owner, alice_address);
let events = event::emitted_events<one_shot::MintRapperEvent>();
let event = events.borrow(0);
let alice_rapper = one_shot::get_mint_rapper_event_token_id(event);
one_shot::mint_rapper(&module_owner, bob_address);
let events = event::emitted_events<one_shot::MintRapperEvent>();
let event = events.borrow(1);
let bob_rapper = one_shot::get_mint_rapper_event_token_id(event);
let balance = one_shot::balance_of(alice_address);
assert!(balance == 1, 1);
let balance = one_shot::balance_of(bob_address);
assert!(balance == 1, 2);
std::debug::print(&utf8(b"=== RAPPERS ==="));
std::debug::print(&alice_rapper);
std::debug::print(&bob_rapper);
rap_battle::go_on_stage_or_battle(&alice, object::address_to_object<token::Token>(alice_rapper), 0);
rap_battle::go_on_stage_or_battle(&bob, object::address_to_object<token::Token>(bob_rapper), 0);
// Alice won the battle, she now has 2 rappers
let balance = one_shot::balance_of(alice_address);
assert!(balance == 2, 3);
// and bob 0
let balance = one_shot::balance_of(bob_address);
assert!(balance == 0, 4);
}

Recommended Mitigation

Transfer the rappers to their respective owners.

if (winner == defender_addr) {
coin::deposit(defender_addr, pool);
one_shot::increment_wins(arena.defender_token_id);
one_shot::transfer_record_only(arena.defender_token_id, @battle_addr, defender_addr);
- one_shot::transfer_record_only(chall_token_id, @battle_addr, defender_addr);
+ one_shot::transfer_record_only(chall_token_id, @battle_addr, chall_addr);
} else {
coin::deposit(chall_addr, pool);
one_shot::increment_wins(chall_token_id);
- one_shot::transfer_record_only(arena.defender_token_id, @battle_addr, chall_addr);
+ one_shot::transfer_record_only(arena.defender_token_id, @battle_addr, defender_addr);
};

Support

FAQs

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