One Shot: Reloaded

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

User can't unstake if they are not registered

Author Revealed upon completion

Root + Impact

Description

When a user unstakes their rapper after at least one day of staking, they are expected to receive CRED tokens.

However, the code enforce them to register first, event though they could be automatically registered since they sign the transaction. The transaction aborts if they don't.

// cred_token
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 {
// @audit reverts when trying to destroy with an amount > 0
@> coin::destroy_zero(coins);
};
}

Risk

Likelihood:

This issue happens everytime a user tries to unstake before registering

Impact:

The transaction reverts. The user is forced to execute another transaction first, to register.

Proof of Concept

You will first need to add test_only functions in the code:

// In cred_token:
#[test_only]
public fun mint_cred(to: address,amount: u64) acquires CredCapabilities {
let caps = borrow_global<CredCapabilities>(@battle_addr);
let coins = coin::mint(amount, &caps.mint_cap);
coin::deposit(to, coins);
}
#[test_only]
public entry fun init(account: &signer) {
init_module(account);
}
// In one_shot:
#[test_only]
public fun get_mint_rapper_event_minter(event: &MintRapperEvent): address {
event.minter
}
#[test_only]
public fun get_mint_rapper_event_token_id(event: &MintRapperEvent): address {
event.token_id
}
// In rap_battle:
#[test_only]
public entry fun init(sender: &signer) {
init_module(sender);
}

Add this code in the test file:

use std::features;
use aptos_framework::timestamp;
use aptos_framework::aptos_coin;
use battle_addr::cred_token;
use battle_addr::rap_battle;
use aptos_framework::event;
use battle_addr::streets;
use aptos_framework::object;
use aptos_token_objects::token;
#[test(
module_owner = @battle_addr, alice = @0x999, aptos_framework = @aptos_framework
)]
#[expected_failure(abort_code = 65543)] // EDESTRUCTION_OF_NONZERO_TOKEN
public fun test_abort_if_not_registered(
module_owner: signer, alice: signer, aptos_framework: signer
) {
// Disable the feature that makes all accounts auto-registered for fungible assets
let std = account::create_account_for_test(@std);
features::change_feature_flags_for_testing(&std, vector[], vector[90]); // 90 is NEW_ACCOUNTS_DEFAULT_TO_FA_STORE
// Initialize the test framework
timestamp::set_time_has_started_for_testing(&aptos_framework);
aptos_coin::ensure_initialized_with_apt_fa_metadata_for_test();
let alice_address = signer::address_of(&alice);
// Initialize modules
cred_token::init(&module_owner);
rap_battle::init(&module_owner);
// Alice gets one rapper
one_shot::mint_rapper(&module_owner, alice_address);
let balance = one_shot::balance_of(alice_address);
assert!(balance == 1, 1);
// Alice stakes her rapper
let events = event::emitted_events<one_shot::MintRapperEvent>();
let event = events.borrow(0);
let rapper_id = one_shot::get_mint_rapper_event_token_id(event);
streets::stake(&alice, object::address_to_object<token::Token>(rapper_id));
// After 1 day, alice should get 1 CRED if she unstakes
timestamp::fast_forward_seconds(86401);
// Aborts
streets::unstake(
&alice, &module_owner, object::address_to_object<token::Token>(rapper_id)
);
}

Run the test with the following command:

aptos move test --dev --filter test_abort_if_not_registered

Recommended Mitigation

Update the mint function to automatically register users if needed. This considerably reduces friction for users.

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

Support

FAQs

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