One Shot: Reloaded

First Flight #47
Beginner FriendlyNFT
100 EXP
View results
Submission Details
Severity: low
Valid

Single NFT staking limitation block multiple deposit per user

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.

Updates

Lead Judging Commences

bube Lead Judge 18 days ago
Submission Judgement Published
Validated
Assigned finding tags:

Only one Rapper NFT can be staked at a time

Support

FAQs

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