One Shot: Reloaded

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

Missing Rapper Token Transfers After Battle Causes both NFT Assets Locked In @battle_addr.

Author Revealed upon completion

Root + Impact

Description

  • During a battle, both the defender's and challenger's rapper NFTs are transferred to the module address (@battle_addr) using object::transfer. However, after determining the winner, only the stats are updated via one_shot::transfer_record_only—there are no corresponding object::transfer calls to send the NFTs back to the winner. The tokens remain owned by the module address indefinitely.


//missing object::transfer to send Rapper NFT back to 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);
};

Risk

Likelihood:

  • This bug triggers on every battle completion, making it inevitable in normal usage. No special conditions or attacker intervention are required beyond participating in gameplay.

Impact:

  • Players lose permanent control of their NFTs after any battle or staging action. The winner receives the CRED coin prize pool but not the rapper tokens, rendering the NFTs unusable and "bricked."


Recommended Mitigation

  • In the go_on_stage_or_battle function, add transfer_raw(owner: &signer, object: address, to: address) calls post-battle to send both NFTs to the winner. This allows transfer of NFT object using address only.

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);
+ object::transfer_raw(@battle_addr, arena.defender_token_id, defender_addr);
one_shot::transfer_record_only(chall_token_id, @battle_addr, defender_addr);
+ object::transfer_raw(@battle_addr, chall_token_id, 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);
+ object::transfer_raw(@battle_addr, arena.defender_token_id, chall_addr);
one_shot::transfer_record_only(chall_token_id, @battle_addr, chall_addr);
+ object::transfer_raw(@battle_addr, chall_token_id, chall_addr);
};

Support

FAQs

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