From 8f945f154299d63ff6d1a4c99e120b5d03b97222 Mon Sep 17 00:00:00 2001 From: Peter Bukva Date: Wed, 13 May 2026 00:24:44 +0100 Subject: [PATCH 1/3] Introducing the EnableBurnOwnUnregistered capability --- app/app.go | 5 +- x/tokenfactory/keeper/admins.go | 4 + x/tokenfactory/keeper/msg_server.go | 15 +- x/tokenfactory/keeper/msg_server_test.go | 199 ++++++++++++++++++++++- x/tokenfactory/types/capabilities.go | 9 +- x/tokenfactory/types/errors.go | 1 + 6 files changed, 215 insertions(+), 18 deletions(-) diff --git a/app/app.go b/app/app.go index 1e44d9a..3f87fc4 100644 --- a/app/app.go +++ b/app/app.go @@ -147,8 +147,9 @@ var ( DefaultNodeHome = ".tokend" BinaryName = "tokend" - tokenFactoryCapabilities = []string{ + TokenFactoryAllCapabilities = []string{ tokenfactorytypes.EnableBurnOwn, + tokenfactorytypes.EnableBurnOwnUnregistered, tokenfactorytypes.EnableBurnFrom, tokenfactorytypes.EnableForceTransfer, tokenfactorytypes.EnableSetMetadata, @@ -551,7 +552,7 @@ func NewApp( app.AccountKeeper, app.BankKeeper, app.DistrKeeper, - tokenFactoryCapabilities, + TokenFactoryAllCapabilities, govModAddress, ) wasmOpts = append(wasmOpts, bindings.RegisterCustomPlugins(app.BankKeeper, &app.TokenFactoryKeeper)...) diff --git a/x/tokenfactory/keeper/admins.go b/x/tokenfactory/keeper/admins.go index fbc6c82..4b43f60 100644 --- a/x/tokenfactory/keeper/admins.go +++ b/x/tokenfactory/keeper/admins.go @@ -14,6 +14,10 @@ import ( func (k Keeper) GetAuthorityMetadata(ctx context.Context, denom string) (types.DenomAuthorityMetadata, error) { bz := k.GetDenomPrefixStore(sdk.UnwrapSDKContext(ctx), denom).Get([]byte(types.DenomAuthorityMetadataKey)) + if bz == nil { + return types.DenomAuthorityMetadata{}, types.ErrDenomIsNotRegistered.Wrapf("denom: \"%s\"", denom) + } + metadata := types.DenomAuthorityMetadata{} err := proto.Unmarshal(bz, &metadata) if err != nil { diff --git a/x/tokenfactory/keeper/msg_server.go b/x/tokenfactory/keeper/msg_server.go index f2d8c9c..e4ba84e 100644 --- a/x/tokenfactory/keeper/msg_server.go +++ b/x/tokenfactory/keeper/msg_server.go @@ -90,6 +90,12 @@ func (server msgServer) Burn(goCtx context.Context, msg *types.MsgBurn) (*types. isBurningOwn = true } + authorityMetadata, err := server.Keeper.GetAuthorityMetadata(ctx, msg.Amount.GetDenom()) + isRegistered := err == nil + if !isRegistered && !server.IsCapabilityEnabled(types.EnableBurnOwnUnregistered) { + return nil, err + } + if !(isBurningOwn && server.IsCapabilityEnabled(types.EnableBurnOwn)) { if !server.IsCapabilityEnabled(types.EnableBurnFrom) { return nil, types.ErrCapabilityNotEnabled.Wrapf("the '%s' capability is NOT enabled", types.EnableBurnFrom) @@ -109,17 +115,12 @@ func (server msgServer) Burn(goCtx context.Context, msg *types.MsgBurn) (*types. } } - authorityMetadata, err := server.Keeper.GetAuthorityMetadata(ctx, msg.Amount.GetDenom()) - if err != nil { - return nil, err - } - - if msg.Sender != authorityMetadata.GetAdmin() { + if !isRegistered || msg.Sender != authorityMetadata.GetAdmin() { return nil, types.ErrUnauthorized.Wrapf("the '%s' sender is NOT '%s' admin of the '%s' denomination", msg.Sender, authorityMetadata.GetAdmin(), msg.Amount.GetDenom()) } } - err := server.Keeper.burnFrom(ctx, msg.Amount, msg.BurnFromAddress) + err = server.Keeper.burnFrom(ctx, msg.Amount, msg.BurnFromAddress) if err != nil { return nil, err } diff --git a/x/tokenfactory/keeper/msg_server_test.go b/x/tokenfactory/keeper/msg_server_test.go index 9957729..1f001d0 100644 --- a/x/tokenfactory/keeper/msg_server_test.go +++ b/x/tokenfactory/keeper/msg_server_test.go @@ -3,6 +3,7 @@ package keeper_test import ( "fmt" + "github.com/strangelove-ventures/tokenfactory/app" "github.com/strangelove-ventures/tokenfactory/x/tokenfactory/keeper" "github.com/strangelove-ventures/tokenfactory/x/tokenfactory/types" @@ -84,36 +85,222 @@ func (suite *KeeperTestSuite) TestMintDenomMsg() { func (suite *KeeperTestSuite) TestBurnDenomMsg() { // Create a denom. suite.CreateDefaultDenom() + nonFactoryDenom := "unique" + unregisteredNonFactoryDenom := "unregistered" + amount := int64(10) + + unboundCoins := sdk.NewCoins(sdk.NewInt64Coin(nonFactoryDenom, amount)) + suite.Assert().NoError(suite.App.BankKeeper.MintCoins(suite.Ctx, types.ModuleName, unboundCoins)) + suite.Assert().NoError(suite.App.BankKeeper.SendCoinsFromModuleToAccount(suite.Ctx, types.ModuleName, suite.TestAccs[0], unboundCoins)) + + unregisteredUnboundCoins := sdk.NewCoins(sdk.NewInt64Coin(unregisteredNonFactoryDenom, amount)) + suite.Assert().NoError(suite.App.BankKeeper.MintCoins(suite.Ctx, types.ModuleName, unregisteredUnboundCoins)) + suite.Assert().NoError(suite.App.BankKeeper.SendCoinsFromModuleToAccount(suite.Ctx, types.ModuleName, suite.TestAccs[0], unregisteredUnboundCoins)) + + ctx := suite.Ctx.WithEventManager(sdk.NewEventManager()) + + admin2 := suite.TestAccs[1].String() + + udc := keeper.NewUnboundDenomCreator(suite.App.TokenFactoryKeeper) + suite.Assert().NoError(udc.CreateDenom(ctx, admin2, nonFactoryDenom)) + + //factoryDenom, err := suite.App.TokenFactoryKeeper.CreateDenom(ctx, admin2, nonFactoryDenom) + //suite.Assert().NoError(err) + + ////expectedFactoryDenom, err := types.GetTokenDenom(admin2, nonFactoryDenom) + ////suite.Assert().NoError(err) + ////suite.Assert().Equal(factoryDenom, expectedFactoryDenom) + + // Proof, that both denoms have been correctly created and the `admin2` account is their admin: + res, _ := suite.App.TokenFactoryKeeper.DenomsFromAdmin(ctx, &types.QueryDenomsFromAdminRequest{Admin: admin2}) + denoms := types.NewSet[string](res.GetDenoms()...) + suite.Assert().True(denoms.Contains(nonFactoryDenom)) + //suite.Assert().True(denoms.Contains(factoryDenom)) + // mint 10 default token for testAcc[0] - suite.msgServer.Mint(suite.Ctx, types.NewMsgMint(suite.TestAccs[0].String(), sdk.NewInt64Coin(suite.defaultDenom, 10))) //nolint:errcheck + suite.msgServer.Mint(suite.Ctx, types.NewMsgMintTo(suite.TestAccs[0].String(), sdk.NewInt64Coin(suite.defaultDenom, amount), suite.TestAccs[2].String())) //nolint:errcheck + + capabilities_EnableBurnUnregistered_DISABLED := []string{ + types.EnableBurnOwn, + //types.EnableBurnOwnUnregistered, + types.EnableBurnFrom, + types.EnableForceTransfer, + types.EnableSetMetadata, + types.EnableSudoMint, + types.EnableCommunityPoolFeeFunding, + } + + capabilities_EnableBurnOwn_DISABLED := []string{ + //types.EnableBurnOwn, + types.EnableBurnOwnUnregistered, + types.EnableBurnFrom, + types.EnableForceTransfer, + types.EnableSetMetadata, + types.EnableSudoMint, + types.EnableCommunityPoolFeeFunding, + } + + capabilities_EnableBurnFrom_DISABLED := []string{ + types.EnableBurnOwn, + types.EnableBurnOwnUnregistered, + //types.EnableBurnFrom, + types.EnableForceTransfer, + types.EnableSetMetadata, + types.EnableSudoMint, + types.EnableCommunityPoolFeeFunding, + } + + //capabilities_ALLBurnOwn_DISABLED := []string{ + // //types.EnableBurnOwn, + // //types.EnableBurnOwnUnregistered, + // types.EnableBurnFrom, + // types.EnableForceTransfer, + // types.EnableSetMetadata, + // types.EnableSudoMint, + // types.EnableCommunityPoolFeeFunding, + //} + + //capabilities_Burn_DISABLED := []string{ + // //types.EnableBurnOwn, + // //types.EnableBurnOwnUnregistered, + // //types.EnableBurnFrom, + // types.EnableForceTransfer, + // types.EnableSetMetadata, + // types.EnableSudoMint, + // types.EnableCommunityPoolFeeFunding, + //} for _, tc := range []struct { desc string amount int64 burnDenom string admin string + burnFrom string valid bool expectedMessageEvents int + capabilities []string }{ { - desc: "denom does not exist", - burnDenom: "factory/osmo1t7egva48prqmzl59x5ngv4zx0dtrwewc9m7z44/evmos", - admin: suite.TestAccs[0].String(), - valid: false, + desc: "denom does not exist", + burnDenom: "factory/osmo1t7egva48prqmzl59x5ngv4zx0dtrwewc9m7z44/evmos", + admin: suite.TestAccs[0].String(), + burnFrom: suite.TestAccs[2].String(), + valid: false, + amount: 1, + expectedMessageEvents: 0, }, { desc: "success case", burnDenom: suite.defaultDenom, admin: suite.TestAccs[0].String(), + burnFrom: suite.TestAccs[2].String(), + valid: true, + amount: 1, + expectedMessageEvents: 1, + }, + { + desc: "EnableBurnOwnUnregistered ENABLED: successful burn of OWN Registerd non-factory denom coins", + burnDenom: nonFactoryDenom, + admin: suite.TestAccs[0].String(), + valid: true, + amount: 1, + expectedMessageEvents: 1, + }, + { + desc: "EnableBurnOwnUnregistered DISABLED: successful burn of OWN Registerd non-factory denom coins", + burnDenom: nonFactoryDenom, + admin: suite.TestAccs[0].String(), + valid: true, + amount: 1, + expectedMessageEvents: 1, + capabilities: capabilities_EnableBurnUnregistered_DISABLED, + }, + { + desc: "EnableBurnOwnUnregistered ENABLED: successful burn of OWN UNregisterd non-factory denom coins", + burnDenom: unregisteredNonFactoryDenom, + admin: suite.TestAccs[0].String(), + valid: true, + amount: 1, + expectedMessageEvents: 1, + }, + { + desc: "EnableBurnOwnUnregistered DISABLED: failed burn of OWN UNregisterd non-factory denom coins", + burnDenom: unregisteredNonFactoryDenom, + admin: suite.TestAccs[0].String(), + valid: false, + amount: 1, + expectedMessageEvents: 0, + capabilities: capabilities_EnableBurnUnregistered_DISABLED, + }, + { + desc: "EnableBurnOwn DISABLED: failed burn of OWN non-factory denom coins", + burnDenom: nonFactoryDenom, + admin: suite.TestAccs[0].String(), + valid: false, + amount: 1, + expectedMessageEvents: 0, + capabilities: capabilities_EnableBurnOwn_DISABLED, + }, + { + desc: "EnableBurnOwn DISABLED: successful burn of non-factory denom coins as admin", + burnDenom: nonFactoryDenom, + admin: admin2, + burnFrom: suite.TestAccs[0].String(), + valid: true, + amount: 1, + expectedMessageEvents: 1, + capabilities: capabilities_EnableBurnOwn_DISABLED, + }, + { + desc: "EnableBurnFrom DISABLED: successful burn of OWN non-factory denom coins", + burnDenom: nonFactoryDenom, + admin: suite.TestAccs[0].String(), valid: true, + amount: 1, expectedMessageEvents: 1, + capabilities: capabilities_EnableBurnFrom_DISABLED, + }, + { + desc: "EnableBurnFrom DISABLED: failed burn of non-factory denom coins as admin", + burnDenom: nonFactoryDenom, + admin: admin2, + burnFrom: suite.TestAccs[0].String(), + valid: false, + amount: 1, + expectedMessageEvents: 0, + capabilities: capabilities_EnableBurnFrom_DISABLED, + }, + { + desc: "EnableBurnFrom DISABLED: failed burn of factory denom coins as admin", + burnDenom: suite.defaultDenom, + admin: suite.TestAccs[0].String(), + burnFrom: suite.TestAccs[2].String(), + valid: false, + amount: 1, + expectedMessageEvents: 0, + capabilities: capabilities_EnableBurnFrom_DISABLED, }, } { suite.Run(fmt.Sprintf("Case %s", tc.desc), func() { + if tc.capabilities == nil { + tc.capabilities = app.TokenFactoryAllCapabilities + } + + suite.App.TokenFactoryKeeper.SetEnabledCapabilities(suite.Ctx, tc.capabilities) + suite.msgServer = keeper.NewMsgServerImpl(suite.App.TokenFactoryKeeper) + ctx := suite.Ctx.WithEventManager(sdk.NewEventManager()) suite.Require().Equal(0, len(ctx.EventManager().Events())) + // Test burn message - suite.msgServer.Burn(ctx, types.NewMsgBurn(tc.admin, sdk.NewInt64Coin(tc.burnDenom, 10))) //nolint:errcheck + var msgBurn *types.MsgBurn + if tc.burnFrom == "" { + msgBurn = types.NewMsgBurn(tc.admin, sdk.NewInt64Coin(tc.burnDenom, tc.amount)) + } else { + msgBurn = types.NewMsgBurnFrom(tc.admin, sdk.NewInt64Coin(tc.burnDenom, tc.amount), tc.burnFrom) + } + suite.msgServer.Burn(ctx, msgBurn) //nolint:errcheck + // Ensure current number and type of event is emitted suite.AssertEventEmitted(ctx, types.TypeMsgBurn, tc.expectedMessageEvents) }) diff --git a/x/tokenfactory/types/capabilities.go b/x/tokenfactory/types/capabilities.go index b7f1997..7c6b56d 100644 --- a/x/tokenfactory/types/capabilities.go +++ b/x/tokenfactory/types/capabilities.go @@ -4,9 +4,12 @@ const ( EnableSetMetadata = "enable_metadata" EnableForceTransfer = "enable_force_transfer" // Allows to *ANY* owner of tokens (with *ANY* denomination) to burn these self-owned tokens. - // If disabled (not present), only admin of a denom or sudoer can execute the burn. - EnableBurnOwn = "enable_burn_own" - EnableBurnFrom = "enable_burn_from" + // If disabled (not present), only admin of a denom or sudoer can execute the burn *IF* EnableBurnFrom is enabled. + EnableBurnOwn = "enable_burn_own" + // If enabled, token owner can burn its own tokens of any denomination which is *NOT* registred in tokenfactory. + // This capability depends on EnableBurnOwn = so, if enabled, it has *no* effect if the EnableBurnOwn is not enabled. + EnableBurnOwnUnregistered = "enable_burn_unregistered" + EnableBurnFrom = "enable_burn_from" // Allows addresses registered as sudo admins imn genesis store to mint tokens of *ANY* denominations. // NOTE: with SudoMint enabled, the sudo admin can mint `any` token, not just tokenfactory tokens. // This is intended behavior as requested by other teams, rather than having its own module with very minor logic. diff --git a/x/tokenfactory/types/errors.go b/x/tokenfactory/types/errors.go index 5a3c587..d2c6cbe 100644 --- a/x/tokenfactory/types/errors.go +++ b/x/tokenfactory/types/errors.go @@ -20,4 +20,5 @@ var ( ErrCreatorTooLong = errorsmod.Register(ModuleName, 9, fmt.Sprintf("creator too long, max length is %d bytes", MaxCreatorLength)) ErrDenomDoesNotExist = errorsmod.Register(ModuleName, 10, "denom does not exist") ErrCapabilityNotEnabled = errorsmod.Register(ModuleName, 11, "this capability is not enabled on chain") + ErrDenomIsNotRegistered = errorsmod.Register(ModuleName, 12, "denom is not registered with tokenfactory") ) From f458f92c41c0a32f138e10f561882dc391fb8bfa Mon Sep 17 00:00:00 2001 From: Peter Bukva Date: Wed, 13 May 2026 01:27:55 +0100 Subject: [PATCH 2/3] Fixing failing tests --- x/tokenfactory/bindings/validate_msg_test.go | 26 ++++++++++--------- .../bindings/validate_queries_test.go | 24 ++++++++++++++--- x/tokenfactory/keeper/admins.go | 2 +- x/tokenfactory/keeper/msg_server.go | 2 +- 4 files changed, 37 insertions(+), 17 deletions(-) diff --git a/x/tokenfactory/bindings/validate_msg_test.go b/x/tokenfactory/bindings/validate_msg_test.go index b27c8b4..63f0092 100644 --- a/x/tokenfactory/bindings/validate_msg_test.go +++ b/x/tokenfactory/bindings/validate_msg_test.go @@ -68,12 +68,13 @@ func TestChangeAdmin(t *testing.T) { tokenCreator := RandomAccountAddress() - specs := map[string]struct { + type Spec struct { actor sdk.AccAddress changeAdmin *bindings.ChangeAdmin + expErrMsg func(Spec) string + } - expErrMsg string - }{ + specs := map[string]Spec{ "valid": { changeAdmin: &bindings.ChangeAdmin{ Denom: fmt.Sprintf("factory/%s/%s", tokenCreator.String(), validDenom), @@ -87,7 +88,7 @@ func TestChangeAdmin(t *testing.T) { NewAdminAddress: RandomBech32AccountAddress(), }, actor: tokenCreator, - expErrMsg: "denom prefix is incorrect. Is: facory. Should be: factory: invalid denom", + expErrMsg: func(_ Spec) string { return "denom prefix is incorrect. Is: facory. Should be: factory: invalid denom" }, }, "invalid address in denom": { changeAdmin: &bindings.ChangeAdmin{ @@ -95,7 +96,7 @@ func TestChangeAdmin(t *testing.T) { NewAdminAddress: RandomBech32AccountAddress(), }, actor: tokenCreator, - expErrMsg: "failed changing admin from message: unauthorized account", + expErrMsg: func(s Spec) string { return fmt.Sprintf("failed changing admin from message: denom \"%s\": denom is not registered with tokenfactory", s.changeAdmin.Denom) }, }, "other denom name in 3 part name": { changeAdmin: &bindings.ChangeAdmin{ @@ -103,7 +104,7 @@ func TestChangeAdmin(t *testing.T) { NewAdminAddress: RandomBech32AccountAddress(), }, actor: tokenCreator, - expErrMsg: fmt.Sprintf("invalid denom: factory/%s/invalid denom", tokenCreator.String()), + expErrMsg: func(s Spec) string { return fmt.Sprintf("invalid denom: %s", s.changeAdmin.Denom) }, }, "empty denom": { changeAdmin: &bindings.ChangeAdmin{ @@ -111,7 +112,7 @@ func TestChangeAdmin(t *testing.T) { NewAdminAddress: RandomBech32AccountAddress(), }, actor: tokenCreator, - expErrMsg: "invalid denom: ", + expErrMsg: func(_ Spec) string { return "invalid denom: " }, }, "empty address": { changeAdmin: &bindings.ChangeAdmin{ @@ -119,7 +120,7 @@ func TestChangeAdmin(t *testing.T) { NewAdminAddress: "", }, actor: tokenCreator, - expErrMsg: "address from bech32: empty address string is not allowed", + expErrMsg: func(_ Spec) string { return "address from bech32: empty address string is not allowed" }, }, "creator is a different address": { changeAdmin: &bindings.ChangeAdmin{ @@ -127,7 +128,7 @@ func TestChangeAdmin(t *testing.T) { NewAdminAddress: RandomBech32AccountAddress(), }, actor: RandomAccountAddress(), - expErrMsg: "failed changing admin from message: unauthorized account", + expErrMsg: func(_ Spec) string { return "failed changing admin from message: unauthorized account" }, }, "change to the same address": { changeAdmin: &bindings.ChangeAdmin{ @@ -138,7 +139,7 @@ func TestChangeAdmin(t *testing.T) { }, "nil binding": { actor: tokenCreator, - expErrMsg: "invalid request: changeAdmin is nil - original request: ", + expErrMsg: func(_ Spec) string { return "invalid request: changeAdmin is nil - original request: " }, }, } for name, spec := range specs { @@ -156,10 +157,11 @@ func TestChangeAdmin(t *testing.T) { require.NoError(t, err) err = wasmbinding.ChangeAdmin(&app.TokenFactoryKeeper, ctx, spec.actor, spec.changeAdmin) - if len(spec.expErrMsg) > 0 { + if spec.expErrMsg != nil { require.Error(t, err) actualErrMsg := err.Error() - require.Equal(t, spec.expErrMsg, actualErrMsg) + expectedMsg := spec.expErrMsg(spec) + require.Equal(t, expectedMsg, actualErrMsg) return } require.NoError(t, err) diff --git a/x/tokenfactory/bindings/validate_queries_test.go b/x/tokenfactory/bindings/validate_queries_test.go index 6b7abe8..32b075a 100644 --- a/x/tokenfactory/bindings/validate_queries_test.go +++ b/x/tokenfactory/bindings/validate_queries_test.go @@ -5,6 +5,7 @@ import ( "testing" wasmbinding "github.com/strangelove-ventures/tokenfactory/x/tokenfactory/bindings" + "github.com/strangelove-ventures/tokenfactory/x/tokenfactory/keeper" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -74,10 +75,15 @@ func TestDenomAdmin(t *testing.T) { // create a subdenom via the token factory admin := sdk.AccAddress([]byte("addr1_______________")) - tfDenom, err := app.TokenFactoryKeeper.CreateDenom(ctx, admin.String(), "subdenom") + validSubDenom := "validdenom" + tfDenom, err := app.TokenFactoryKeeper.CreateDenom(ctx, admin.String(), validSubDenom) require.NoError(t, err) require.NotEmpty(t, tfDenom) + registeredUnboundDenom := "registered" + udc := keeper.NewUnboundDenomCreator(app.TokenFactoryKeeper) + assert.NoError(t, udc.CreateDenom(ctx, admin.String(), registeredUnboundDenom)) + queryPlugin := wasmbinding.NewQueryPlugin(app.BankKeeper, &app.TokenFactoryKeeper) testCases := []struct { @@ -92,11 +98,23 @@ func TestDenomAdmin(t *testing.T) { expectAdmin: admin.String(), }, { - name: "invalid token factory denom", + name: "unregistered valid token factory denom", + denom: fmt.Sprintf("factory/%s/%s", RandomBech32AccountAddress(), validSubDenom), + expectErr: true, + expectAdmin: "", + }, + { + name: "unregistered unbound denom", denom: "uosmo", - expectErr: false, + expectErr: true, expectAdmin: "", }, + { + name: "registered unbound denom", + denom: registeredUnboundDenom, + expectErr: false, + expectAdmin: admin.String(), + }, } for _, tc := range testCases { diff --git a/x/tokenfactory/keeper/admins.go b/x/tokenfactory/keeper/admins.go index 4b43f60..e9c2886 100644 --- a/x/tokenfactory/keeper/admins.go +++ b/x/tokenfactory/keeper/admins.go @@ -15,7 +15,7 @@ func (k Keeper) GetAuthorityMetadata(ctx context.Context, denom string) (types.D bz := k.GetDenomPrefixStore(sdk.UnwrapSDKContext(ctx), denom).Get([]byte(types.DenomAuthorityMetadataKey)) if bz == nil { - return types.DenomAuthorityMetadata{}, types.ErrDenomIsNotRegistered.Wrapf("denom: \"%s\"", denom) + return types.DenomAuthorityMetadata{}, types.ErrDenomIsNotRegistered.Wrapf("denom \"%s\"", denom) } metadata := types.DenomAuthorityMetadata{} diff --git a/x/tokenfactory/keeper/msg_server.go b/x/tokenfactory/keeper/msg_server.go index e4ba84e..9b71c48 100644 --- a/x/tokenfactory/keeper/msg_server.go +++ b/x/tokenfactory/keeper/msg_server.go @@ -97,7 +97,7 @@ func (server msgServer) Burn(goCtx context.Context, msg *types.MsgBurn) (*types. } if !(isBurningOwn && server.IsCapabilityEnabled(types.EnableBurnOwn)) { - if !server.IsCapabilityEnabled(types.EnableBurnFrom) { + if !isBurningOwn && !server.IsCapabilityEnabled(types.EnableBurnFrom) { return nil, types.ErrCapabilityNotEnabled.Wrapf("the '%s' capability is NOT enabled", types.EnableBurnFrom) } From 366d44d2c423c687b8def73015448c8292975c55 Mon Sep 17 00:00:00 2001 From: Peter Bukva Date: Wed, 13 May 2026 01:45:37 +0100 Subject: [PATCH 3/3] Extending burn unit tests covering outstading case + coments in code --- x/tokenfactory/keeper/msg_server.go | 5 ++++ x/tokenfactory/keeper/msg_server_test.go | 33 +++++++++--------------- 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/x/tokenfactory/keeper/msg_server.go b/x/tokenfactory/keeper/msg_server.go index 9b71c48..0464764 100644 --- a/x/tokenfactory/keeper/msg_server.go +++ b/x/tokenfactory/keeper/msg_server.go @@ -96,7 +96,12 @@ func (server msgServer) Burn(goCtx context.Context, msg *types.MsgBurn) (*types. return nil, err } + // The following code section is exclusively for case when: + // * either burning someone's else's tokens + // * or burning own tokens, but EnableBurnOwn is disabled if !(isBurningOwn && server.IsCapabilityEnabled(types.EnableBurnOwn)) { + // Denom admin *can* burn its own tokens even if the EnableBurnFrom is *disabled*. + // This is sensical, as the admin burns it sown tokens and *not* tokens from another account. if !isBurningOwn && !server.IsCapabilityEnabled(types.EnableBurnFrom) { return nil, types.ErrCapabilityNotEnabled.Wrapf("the '%s' capability is NOT enabled", types.EnableBurnFrom) } diff --git a/x/tokenfactory/keeper/msg_server_test.go b/x/tokenfactory/keeper/msg_server_test.go index 1f001d0..770968d 100644 --- a/x/tokenfactory/keeper/msg_server_test.go +++ b/x/tokenfactory/keeper/msg_server_test.go @@ -117,8 +117,10 @@ func (suite *KeeperTestSuite) TestBurnDenomMsg() { suite.Assert().True(denoms.Contains(nonFactoryDenom)) //suite.Assert().True(denoms.Contains(factoryDenom)) - // mint 10 default token for testAcc[0] + // mint 10 default token for testAcc[2] suite.msgServer.Mint(suite.Ctx, types.NewMsgMintTo(suite.TestAccs[0].String(), sdk.NewInt64Coin(suite.defaultDenom, amount), suite.TestAccs[2].String())) //nolint:errcheck + // mint 10 default token for admin testAcc[0] + suite.msgServer.Mint(suite.Ctx, types.NewMsgMintTo(suite.TestAccs[0].String(), sdk.NewInt64Coin(suite.defaultDenom, amount), suite.TestAccs[0].String())) //nolint:errcheck capabilities_EnableBurnUnregistered_DISABLED := []string{ types.EnableBurnOwn, @@ -150,26 +152,6 @@ func (suite *KeeperTestSuite) TestBurnDenomMsg() { types.EnableCommunityPoolFeeFunding, } - //capabilities_ALLBurnOwn_DISABLED := []string{ - // //types.EnableBurnOwn, - // //types.EnableBurnOwnUnregistered, - // types.EnableBurnFrom, - // types.EnableForceTransfer, - // types.EnableSetMetadata, - // types.EnableSudoMint, - // types.EnableCommunityPoolFeeFunding, - //} - - //capabilities_Burn_DISABLED := []string{ - // //types.EnableBurnOwn, - // //types.EnableBurnOwnUnregistered, - // //types.EnableBurnFrom, - // types.EnableForceTransfer, - // types.EnableSetMetadata, - // types.EnableSudoMint, - // types.EnableCommunityPoolFeeFunding, - //} - for _, tc := range []struct { desc string amount int64 @@ -280,6 +262,15 @@ func (suite *KeeperTestSuite) TestBurnDenomMsg() { expectedMessageEvents: 0, capabilities: capabilities_EnableBurnFrom_DISABLED, }, + { + desc: "EnableBurnFrom DISABLED: successful burn of admin's own factory denom coins", + burnDenom: suite.defaultDenom, + admin: suite.TestAccs[0].String(), + valid: true, + amount: 1, + expectedMessageEvents: 1, + capabilities: capabilities_EnableBurnFrom_DISABLED, + }, } { suite.Run(fmt.Sprintf("Case %s", tc.desc), func() { if tc.capabilities == nil {