DexLyn Disclosed Report

Freeze bypass via fungible-store migration

Company
Created date
Nov 02 2025

Target

https://github.com/hackenproof-public/tokenomics_contract

Vulnerability Details

https://github.com/hackenproof-public/tokenomics_contract/blob/cc3983b3791b373e3632f9b5bf88bd2e669378af/dexlyn_coin/sources/dxlyn_coin.move#L419 checks coin::is_account_registered<DXLYN>(user) to decide whether to call the legacy CoinStore freeze path. The same guard exists for unfreeze at https://github.com/hackenproof-public/tokenomics_contract/blob/cc3983b3791b373e3632f9b5bf88bd2e669378af/dexlyn_coin/sources/dxlyn_coin.move#L443

In the framework, coin::is_account_registered returns true even after coin::migrate_to_fungible_store has deleted the CoinStore, because it also detects migrated primary fungible stores (build/DexlynTokenomics/sources/dependencies/SupraFramework/coin.move (line 816)).

Post-migration the account no longer has a CoinStore, so the guard still routes into coin::freeze_coin_store, which immediately aborts on borrow_global_mut<CoinStore<_>> because the resource is gone. The unfreeze path fails the same way.

As a result any holder can migrate once and become permanently immune to admin freeze or unfreeze attempts. The admin transactions abort and enforcement tooling is broken.

Any holder can migrate their balance once and make every subsequent admin freeze/unfreeze attempt abort because the code still routes into coin::freeze_coin_store, which no longer exists. There’s no direct value theft, but it permanently disables a core enforcement control and lets an attacker disrupt protocol operations at will.

Recommendation

Update the guard to test exists<CoinStore<DXLYN>>(user) (or similar) before calling coin::freeze_coin_store, and fall back to primary_fungible_store::set_frozen_flag when only the fungible store remains.

Validation steps

diff --git a/dexlyn_coin/tests/dxlyn_coin_test.move b/dexlyn_coin/tests/dxlyn_coin_test.move
--- a/dexlyn_coin/tests/dxlyn_coin_test.move
+++ b/dexlyn_coin/tests/dxlyn_coin_test.move
@@
     assert!(coin::is_coin_store_frozen<DXLYN>(alice_addr), 0x1);
 }
 
 #[test(dev = @dexlyn_coin, alice = @0x123)]
+#[expected_failure]
+fun test_freeze_token_after_coin_store_migration(dev: &signer, alice: &signer) {
+    setup_test_with_genesis(dev);
+    account::create_account_for_test(address_of(alice));
+
+    let dev_addr = address_of(dev);
+    let alice_addr = address_of(alice);
+
+    let amount = 1000 * DXLYN_DECIMAL;
+    dxlyn_coin::mint(dev, dev_addr, amount);
+
+    let coins = coin::withdraw<DXLYN>(dev, amount);
+    supra_account::deposit_coins(alice_addr, coins);
+
+    coin::migrate_to_fungible_store<DXLYN>(alice);
+
+    // Fails: freeze still dispatches to legacy CoinStore path after migration.
+    dxlyn_coin::freeze_token(dev, alice_addr);
+}
+
+#[test(dev = @dexlyn_coin, alice = @0x123)]
 #[expected_failure(abort_code = dxlyn_coin::ERROR_NOT_OWNER, location = dxlyn_coin)]
 fun test_freeze_token_non_owner(dev: &signer, alice: &signer) {
     setup_test_with_genesis(dev);
     account::create_account_for_test(address_of(alice));
     dxlyn_coin::freeze_token(alice, address_of(alice));

Attachments

hidden
CommentsReport History
Comments on this report are hidden
Details
Statedisclosed
Severity
Medium
Bounty$150
Visibilitypartially
VulnerabilityBlockchain
Participants (4)
company admin
author
company admin