One Shot: Reloaded

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

Unstake Not Bound to the Originally Staked Token

Author Revealed upon completion

Root + Impact

Description

Normal behavior: When a user stakes an NFT, the protocol records that the user has an active stake. Later, the user should only be able to unstake the same token they originally staked.

Issue: StakeInfo does not record the token ID. During unstake, the function trusts the caller-supplied Object without verifying that it matches the originally staked token. This creates a confused-deputy risk: if the module owner’s signer co-signs blindly, a malicious staker could request the wrong token back.

public entry fun unstake(staker: &signer, module_owner: &signer, rapper_token: Object<Token>) acquires StakeInfo {
let staker_addr = signer::address_of(staker);
let token_id = object::object_address(&rapper_token);
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);
// @> stake_info does not store or check token_id
}

Risk

Likelihood:

This occurs whenever a staker convinces the module owner to co-sign an unstake transaction with a mismatched token. Automated co-signers or bots increase likelihood.

Impact:

Wrong tokens can be transferred to the staker, breaking asset custody and enabling theft of other users’ NFTs.

Recommended Mitigation

- struct StakeInfo has key, store {
- start_time_seconds: u64,
- owner: address,
- }
+ struct StakeInfo has key, store {
+ start_time_seconds: u64,
+ owner: address,
+ token_id: address,
+ }
@@
- move_to(staker, StakeInfo { start_time_seconds: timestamp::now_seconds(), owner: staker_addr });
+ move_to(staker, StakeInfo {
+ start_time_seconds: timestamp::now_seconds(),
+ owner: staker_addr,
+ token_id,
+ });
@@
- assert!(stake_info.owner == staker_addr, E_NOT_OWNER);
+ assert!(stake_info.owner == staker_addr, E_NOT_OWNER);
+ assert!(stake_info.token_id == token_id, E_TOKEN_NOT_STAKED);

Support

FAQs

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