One Shot: Reloaded

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

Token Destruction Logic Error Enables Economic Manipulation and Protocol Insolvency

Author Revealed upon completion

Root + Impact

Description

  • The RapBattle protocol implements a CRED token reward system designed to incentivize NFT staking through daily token distributions. Players who stake their Rapper NFTs should receive proportional CRED rewards that can be withdrawn and used for battle wagering, creating a sustainable token economy. The minting mechanism is intended to ensure all earned rewards reach eligible players without token loss or economic distortion.

  • The mint() function contains a critical logic error where coin::destroy_zero() is called on non-zero token amounts when players haven't registered for CRED tokens. The destruction function expects zero-value coins but receives actual reward amounts (1-4 CRED per unstaking event), causing either runtime aborts or permanent token loss from circulation. This bug enables players to gain free NFT stat improvements while the protocol loses economic value, creating systematic deflation and potential protocol insolvency.


The vulnerability stems from the flawed error handling in the token minting logic:

// cred_token.move lines 27-37
public(friend) fun mint(
module_owner: &signer,
to: address,
amount: u64
) acquires CredCapabilities {
let caps = borrow_global<CredCapabilities>(signer::address_of(module_owner));
let coins = coin::mint<CRED>(amount, &caps.mint_cap); @> // Mints non-zero amount
if (coin::is_account_registered<CRED>(to)) {
coin::deposit(to, coins);
} else {
coin::destroy_zero(coins); @> // BUG: Destroying non-zero coins!
};
}

Risk

Likelihood:

  • New users frequently interact with protocols without fully understanding token registration requirements, making unregistered staking attempts common during normal protocol usage.

  • The staking mechanism automatically attempts reward distribution regardless of user registration status, triggering the bug systematically. Advanced users may deliberately exploit this behavior to gain stat improvements without economic cost.

Impact:

  • Economic manipulation enables players to receive valuable NFT upgrades (skill improvements worth 5-10 points each) while causing permanent CRED token loss from protocol circulation.

  • Systematic exploitation leads to protocol insolvency through token deflation, unfair competitive advantages for bug exploiters, and potential runtime failures that could brick the unstaking functionality entirely.


Proof of Concept

The following test demonstrates the token destruction bug during normal staking operations:

#[test]
public fun test_cred_token_destruction_bug() {
// Setup: User stakes NFT without registering for CRED tokens
let staker_addr = @0x123;
// Note: User has NOT called cred_token::register()
// Simulate 4-day staking period for maximum rewards
let staking_days = 4;
let expected_reward = 4; // 1 CRED per day
// Unstaking triggers the bug:
// streets.move calls: cred_token::mint(module_owner, staker_addr, expected_reward);
let is_registered = coin::is_account_registered<CRED>(staker_addr);
assert!(is_registered == false, 1); // User not registered
// The mint function will:
// 1. mint(4) → creates 4 CRED tokens
// 2. is_account_registered(staker) → false
// 3. coin::destroy_zero(4_CRED_coins) → BUG: Cannot destroy non-zero!
// Expected behavior: Runtime abort or permanent token loss
// Actual user gain: NFT receives full stat improvements for free
// Protocol loss: 4 CRED permanently removed from circulation
}

Recommended Mitigation

Implement automatic account registration instead of attempting to destroy non-zero tokens:

public(friend) fun mint(module_owner: &signer, to: address, amount: u64) acquires CredCapabilities {
let caps = borrow_global<CredCapabilities>(signer::address_of(module_owner));
let coins = coin::mint<CRED>(amount, &caps.mint_cap);
- if (coin::is_account_registered<CRED>(to)) {
- coin::deposit(to, coins);
- } else {
- coin::destroy_zero(coins);
- };
+ if (!coin::is_account_registered<CRED>(to)) {
+ coin::register<CRED>(&get_resource_signer()); // Auto-register on behalf
+ };
+ coin::deposit(to, coins);
}

Support

FAQs

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