One Shot: Reloaded

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

Vulnerability 2

Author Revealed upon completion

Root + Impact

Description

  • Players stake their Rapper NFTs into the protocol (“streets”), which should improve stats and earn CRED rewards over time. Players also use CRED in battle wagers, and Rapper NFTs should return to their rightful owners after staking/battling.

  • The protocol never actually transfers NFTs back to the player’s address. Instead, NFTs are stored under the module account (@battle_addr), with an internal registry mapping owners. This registry can desynchronize from real ownership or be abused. Simultaneously, the same privileged address holds the mint/burn capabilities for CRED and Rapper NFTs, meaning it can arbitrarily inflate supply or rug players.

// Root cause in the codebase with @> marks to highlight the relevant section
store_rapper_under(@battle_addr); //@> NFTs custody centralized here
record_owner_in_table(player_addr); //@> Only tracked in internal table
// Mint authority
mint<CRED>(@battle_addr, amount); //@> Full mint/burn capability centralized

Risk

Likelihood:

  • Always active: every staked or battled Rapper is in module custody

  • Module owner always holds the mint/burn caps, so the risk is permanent until refactored.

Impact:

  • Loss of trust: users cannot verify real ownership of their NFTs beyond the registry.

Rug potential: privileged account can mint infinite CRED and drain wagers.

  • Protocol fragility: a bug or malicious signer in @battle_addr can freeze or steal all assets.

Proof of Concept

Attack / failure scenario:

  • Module owner (or compromised key) calls internal mint to flood CRED supply, devaluing the token.

  • Owner withholds NFT transfer back to player by refusing to sign release, leaving the registry out of sync.

  • Players lose funds/NFTs despite registry saying otherwise.

// Custody during staking
move_to(@battle_addr, rapper_nft); //@> NFT leaves player ownership
owner_registry[player] = rapper_id; //@> Only internal record
// CRED mint control
public fun mint_cred(amount: u64) {
coin::mint<CRED>(amount, @battle_addr); //@> Unlimited mint power
}

Recommended Mitigation

NFT custody: Avoid centralizing ownership under the module account. Use Token V2/Object transfer APIs to return real object ownership back to players. Keep registries as indices, not canonical records.

  • Mint/burn authority: Move CRED and Rapper mint/burn capabilities into a resource account controlled by a multisig or DAO. This minimizes single-signer failure.

  • Safety guards:

    • Add invariants that enforce supply caps.

    • Emit events on custody/minting actions for transparency.

    • Consider time-lock or governance approval for sensitive functions.

- remove this code
+ add this code

Support

FAQs

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