One Shot: Reloaded

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

Users can stake only one NFT at a time

Author Revealed upon completion

Root + Impact

Description

In every NFT staking applications, users should be able to stake any amount of NFTs to receive rewards, unless specified in the documentation.

However, staking a rapper in streets::stake can only be done once at a time. This is because move_to sets resources for an address and aborts if the resource already exists, in order to not overwrite it.

Because of that behaviour, it is not possible to stake multiple rappers at the same time.

// in streets::stake
public entry fun stake(staker: &signer, rapper_token: Object<Token>) {
let staker_addr = signer::address_of(staker);
let token_id = object::object_address(&rapper_token);
// @audit `move_to` reverts if the resource already exists
move_to(staker, StakeInfo {
start_time_seconds: timestamp::now_seconds(),
owner: staker_addr,
});
one_shot::transfer_record_only(token_id, staker_addr, @battle_addr);
object::transfer(staker, rapper_token, @battle_addr);
event::emit(StakedEvent {
owner: staker_addr,
token_id,
start_time: timestamp::now_seconds(),
});
}

Risk

Likelihood:

Happens when users try to stake multiple rappers.

Impact:

The transaction aborts, only one rapper can be staked at a time. This cuts rewards for users.

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, aptos_framework = @aptos_framework
)]
public fun test_can_stake_only_rapper(
module_owner: signer, alice: 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);
// Initialize modules
cred_token::init(&module_owner);
rap_battle::init(&module_owner);
// Alice gets 2 rappers
one_shot::mint_rapper(&module_owner, alice_address);
let events = event::emitted_events<one_shot::MintRapperEvent>();
let event = events.borrow(0);
let alice_rapper_1 = one_shot::get_mint_rapper_event_token_id(event);
one_shot::mint_rapper(&module_owner, alice_address);
let events = event::emitted_events<one_shot::MintRapperEvent>();
let event = events.borrow(1);
let alice_rapper_2 = one_shot::get_mint_rapper_event_token_id(event);
// Can stake 1 rapper
streets::stake(&alice, object::address_to_object<token::Token>(alice_rapper_1));
// Can't stake the 2nd because the ressource already exists
streets::stake(&alice, object::address_to_object<token::Token>(alice_rapper_2));
}

Recommended Mitigation

Update the staking mechanics in order to enable staking of mulitple rappers for a user.

Support

FAQs

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