One Shot: Reloaded

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

Single NFT staking limitation block multiple deposit per user

Author Revealed upon completion

Description

Users can only stake one NFT at a time due to the staking mechanism using a single StakeInfo resource per user address. When a user attempts to stake a second NFT while already having one staked, the transaction fails with a RESOURCE_ALREADY_EXISTS error, limiting the protocol's usability for users who own multiple rapper NFTs.

Root Cause

In streets::stake() the function uses move_to(staker, StakeInfo {...}) to store staking information. Since Move resources must be unique per address, attempting to stake a second NFT fails:

// streets.move:32-39
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);
move_to(staker, StakeInfo { // @audit-issue: Fails if StakeInfo already exists
start_time_seconds: timestamp::now_seconds(),
owner: staker_addr,
});
// ... rest of function
}

Key issues:

  1. Single resource limitation: Only one StakeInfo resource can exist per address

  2. No multiple NFT support: Users cannot stake multiple NFTs simultaneously

  3. Poor user experience: Users must unstake before staking another NFT

Risk

Likelihood: Medium - Any user with multiple NFTs will encounter this when attempting to stake more than one

Impact: Low - Degrades protocol functionality

Impact

Low severity because:

  • User friction: Forces inefficient workflow where users must constantly unstake/re-stake to manage multiple NFTs

  • Lost opportunity cost: Users cannot optimize staking strategies across their NFT portfolio

  • Protocol utility reduction: Limits the practical use of the staking feature for serious collectors

Proof of Concept

In this test the second staking throw a RESOURCE_ALREADY_EXISTS error even if the token id is different

#[test]
#[expected_failure]
public fun test_user_cannot_stake_twice() {
let now = 1_758_100_000_000;
// Initialize timestamp for testing
let aptos_framework = account::create_account_for_test(@0x1);
timestamp::set_time_has_started_for_testing(&aptos_framework);
timestamp::update_global_time_for_test(now);
// Initialize aggregator factory and coin system
coin::create_coin_conversion_map(&aptos_framework);
let module_owner = account::create_account_for_test(@battle_addr);
let minter = account::create_account_for_test(@minter_addr);
cred_token::init_module_test(&module_owner);
rap_battle::init_module_test(&module_owner);
// Mint two NFTs for the same user
one_shot::mint_rapper(&module_owner, signer::address_of(&minter));
let mint_events = event::emitted_events<MintRapperEvent>();
let first_event = vector::borrow(&mint_events, 0);
let token_id = one_shot::get_mint_event_token_id(first_event);
let rapper_token = object::address_to_object<Token>(token_id);
one_shot::mint_rapper(&module_owner, signer::address_of(&minter));
let second_mint_events = event::emitted_events<MintRapperEvent>();
let second_event = vector::borrow(&second_mint_events, 1);
let token_id2 = one_shot::get_mint_event_token_id(second_event);
let rapper_token2 = object::address_to_object<Token>(token_id2);
// First stake succeeds
streets::stake(&minter, rapper_token);
// Second stake fails with RESOURCE_ALREADY_EXISTS (code 4004)
streets::stake(&minter, rapper_token2);
}

Recommended Mitigation

Support multiple stakes per user

+ struct UserStakes has key {
+ stakes: table::Table<address, StakeInfo>, // token_id -> stake_info
+ }
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);
+ if (!exists<UserStakes>(staker_addr)) {
+ move_to(staker, UserStakes {
+ stakes: table::new<address, StakeInfo>(),
+ });
+ };
+
+ let user_stakes = borrow_global_mut<UserStakes>(staker_addr);
+ table::add(&mut user_stakes.stakes, token_id, StakeInfo {
+ start_time_seconds: timestamp::now_seconds(),
+ owner: staker_addr,
+ });
// ... rest unchanged
}

This solution allows users to stake multiple NFTs simultaneously, improving the user experience and protocol utility.

Support

FAQs

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