Minting non zero CRED to unregistered users aborts with EDESTRUCTION_OF_NONZERO_TOKEN
Description
Minting CRED to a recipient should either deposit tokens to that account if registered, or safely handle the case when the recipient is unregistered without causing unexpected reverts. However, minting non zero amount to an unregistered account triggers an abort due to clean up because the code mints coins and then calls coin::destroy_zero
which aborts for non zero amounts.
Risk
Likelihood: Low
The issue impacts all unregistered users who try to unstake.
Impact: Low
The transaction aborts, however, the user can registered and the issue resolves.
Proof of Concept
#[test]
#[expected_failure(abort_code = 65543, location = 0x1::coin)]
public fun test_owner_mint_cred_to_unregistered_destroys_minted () {
let module_owner = account::create_account_for_test(@battle_addr);
let framework = account::create_account_for_test(@0x1);
let recipient = @0x1234;
cred_token::initialize_for_test(&module_owner, &framework);
assert!(cred_token::is_account_registered(recipient) == false);
cred_token::mint(&module_owner, recipient, 1000);
assert!(cred_token::balance_of(recipient) == 0);
}
The test proves that transaction aborts when the module owner tries to mint coins to an unregistered account.
Recommended Mitigation
It is recommended to update the flow as:
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);
- if (coin::is_account_registered<CRED>(to)) {
- coin::deposit(to, coins);
- } else {
- coin::destroy_zero(coins);
- };
+ if(amount > 0) {
+ let to_addr = signer::address_of(to);
+ if (!coin::is_account_registered<CRED>(to_addr)) {
+ coin::register<CRED>(to);
+ }
+ let coins = coin::mint<CRED>(amount, &caps.mint_cap);
+ coin::deposit(to_addr, coins);
+ }
}