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.
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:
#[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);
}
#[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
}
#[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
) {
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);
cred_token::init(&module_owner);
rap_battle::init(&module_owner);
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);
let balance = one_shot::balance_of(alice_address);
assert!(balance == 2, 3);
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);
};