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.
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);
};
}
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:
#[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);
}
#[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
}
#[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)]
public fun test_abort_if_not_registered(
module_owner: signer, alice: signer, aptos_framework: signer
) {
let std = account::create_account_for_test(@std);
features::change_feature_flags_for_testing(&std, vector[], vector[90]);
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);
cred_token::init(&module_owner);
rap_battle::init(&module_owner);
one_shot::mint_rapper(&module_owner, alice_address);
let balance = one_shot::balance_of(alice_address);
assert!(balance == 1, 1);
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));
timestamp::fast_forward_seconds(86401);
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);
- };
}