One Shot: Reloaded

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

L03. Centralized Control / Single Point of Failure in Unstake

Author Revealed upon completion

Root + Impact

Description

  • Normal behavior: Players can stake their rapper NFTs along with a CRED token and earn rewards over time. When a player decides to unstake, the contract should return the NFT and any earned rewards.

  • Issue: The unstake function requires the module_owner signer to mint the earned CRED and transfer the NFT back. This centralizes control over staking rewards and NFT recovery. If the module_owner is unavailable, malicious, or compromised, players cannot retrieve their NFT or rewards, creating a single point of failure.

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);
let staked_duration = timestamp::now_seconds() - stake_info.start_time_seconds;
let days_staked = staked_duration / 86400;
if (days_staked > 0) {
@> if (days_staked >= 1) { cred_token::mint(module_owner, staker_addr, 1); };
@> if (days_staked >= 2) { cred_token::mint(module_owner, staker_addr, 1); };
@> if (days_staked >= 3) { cred_token::mint(module_owner, staker_addr, 1); };
@> if (days_staked >= 4) { cred_token::mint(module_owner, staker_addr, 1); };
};
one_shot::transfer_record_only(token_id, @battle_addr, staker_addr);
object::transfer(module_owner, rapper_token, staker_addr);
}

Risk

Likelihood:

  • A player can stake and later attempt to unstake. If the module_owner signer is unavailable or refuses to participate, unstake cannot be completed.

  • Any compromise of the module_owner account immediately impacts all stakers, because it controls minting and NFT transfers.

Impact:

  • Players’ staked NFTs and earned CRED tokens can become permanently inaccessible.

  • Centralized control introduces a single point of failure, making the staking system dependent on one entity for correct operation.


Proof of Concept

// Player stakes NFT
stake(&player_signer, player_rapper_nft);
// Module owner key is lost or compromised
// Attempt to unstake
unstake(&player_signer, &module_owner_signer, player_rapper_nft);
// Transaction fails because module_owner signature is required for both minting rewards and NFT transfer
// Player is unable to recover their NFT or earned CRED

Recommended Mitigation

- require module_owner signer to mint rewards and transfer NFT
+ separate minting/reward logic from NFT transfer
+ allow unstake to be executed by staker alone
+ use pre-minted reward escrow or decentralized minting mechanism
+ ensure NFT transfer back to staker does not require central authority
  • Example mitigation strategies:

    • Pre-mint CRED rewards into a staking pool that can be released by the staker.

    • Use a Friend module with delegated authority or escrow so NFT transfer back to the staker does not require module_owner.

    • Reduce trust in a single account to avoid centralization.

Support

FAQs

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