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/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 fbc6c82..e9c2886 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..0464764 100644 --- a/x/tokenfactory/keeper/msg_server.go +++ b/x/tokenfactory/keeper/msg_server.go @@ -90,8 +90,19 @@ 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 + } + + // 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)) { - if !server.IsCapabilityEnabled(types.EnableBurnFrom) { + // 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) } @@ -109,17 +120,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..770968d 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,213 @@ func (suite *KeeperTestSuite) TestMintDenomMsg() { func (suite *KeeperTestSuite) TestBurnDenomMsg() { // Create a denom. suite.CreateDefaultDenom() - // 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 + 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[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, + //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, + } 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, + }, + { + 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 { + 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") )