From 3f09630df3fe0719e3143f627dc41d836aaa0a3b Mon Sep 17 00:00:00 2001 From: Honchar Denys Date: Sun, 14 Dec 2025 10:50:19 +0200 Subject: [PATCH 1/4] removed admin backdoors --- testutil/keeper/fund.go | 4 ++ x/fund/keeper/keeper.go | 21 ++++++ x/fund/keeper/keeper_test.go | 8 ++- x/fund/keeper/msg_server.go | 18 +---- x/fund/keeper/msg_server_test.go | 23 ++++++ x/fund/module/depinject.go | 15 ++-- x/fund/types/codec.go | 5 ++ x/fund/types/errors.go | 13 ++-- x/ugov/keeper/msg_server.go | 23 ++++-- x/ugov/keeper/msg_server_test.go | 119 +++++++++++++++++++++++++++++++ 10 files changed, 216 insertions(+), 33 deletions(-) create mode 100644 x/fund/keeper/msg_server_test.go create mode 100644 x/ugov/keeper/msg_server_test.go diff --git a/testutil/keeper/fund.go b/testutil/keeper/fund.go index 40dbf91..0b4b8ab 100644 --- a/testutil/keeper/fund.go +++ b/testutil/keeper/fund.go @@ -12,6 +12,8 @@ import ( "github.com/cosmos/cosmos-sdk/testutil" sdk "github.com/cosmos/cosmos-sdk/types" moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" _ "uagd/app" // ensure bech32 prefix init runs in tests @@ -44,6 +46,7 @@ func FundKeeper(t testing.TB) (fundkeeper.Keeper, context.Context) { addressCodec := addresscodec.NewBech32Codec(sdk.GetConfig().GetBech32AccountAddrPrefix()) bankKeeper := noOpBankKeeper{} stakingKeeper := noOpStakingKeeper{addrCodec: addressCodec} + govAuthority := authtypes.NewModuleAddress(govtypes.ModuleName) storeKey := storetypes.NewKVStoreKey(types.StoreKey) storeService := runtime.NewKVStoreService(storeKey) @@ -55,6 +58,7 @@ func FundKeeper(t testing.TB) (fundkeeper.Keeper, context.Context) { addressCodec, bankKeeper, stakingKeeper, + govAuthority, ) if err := k.Params.Set(ctx, types.DefaultParams()); err != nil { diff --git a/x/fund/keeper/keeper.go b/x/fund/keeper/keeper.go index 446da7c..bcc9a5b 100644 --- a/x/fund/keeper/keeper.go +++ b/x/fund/keeper/keeper.go @@ -10,6 +10,8 @@ import ( sdkmath "cosmossdk.io/math" "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" "uagd/x/fund/types" @@ -23,6 +25,7 @@ type Keeper struct { bankKeeper types.BankKeeper stakingKeeper types.StakingKeeper + govAuthority sdk.AccAddress Schema collections.Schema FundStore collections.Map[string, types.Fund] @@ -35,7 +38,11 @@ func NewKeeper( addressCodec address.Codec, bankKeeper types.BankKeeper, stakingKeeper types.StakingKeeper, + govAuthority sdk.AccAddress, ) Keeper { + if govAuthority == nil { + govAuthority = authtypes.NewModuleAddress(govtypes.ModuleName) + } sb := collections.NewSchemaBuilder(storeService) k := Keeper{ storeService: storeService, @@ -43,6 +50,7 @@ func NewKeeper( addressCodec: addressCodec, bankKeeper: bankKeeper, stakingKeeper: stakingKeeper, + govAuthority: govAuthority, FundStore: collections.NewMap(sb, types.FundKeyPrefix, "funds", collections.StringKey, codec.CollValue[types.Fund](cdc)), Params: collections.NewItem(sb, types.ParamsKey, "params", codec.CollValue[types.Params](cdc)), } @@ -113,6 +121,16 @@ func (k Keeper) SetParams(ctx context.Context, params types.Params) error { return k.Params.Set(ctx, params) } +func (k Keeper) assertGovAuthority(authority sdk.AccAddress) error { + if k.govAuthority == nil || k.govAuthority.Empty() { + return fmt.Errorf("gov authority not configured") + } + if !authority.Equals(k.govAuthority) { + return types.ErrUnauthorized + } + return nil +} + func (k Keeper) ValidateFundPlan(ctx context.Context, plan types.FundPlan) error { fundAddr, err := k.addressCodec.StringToBytes(plan.FundAddress) if err != nil { @@ -155,6 +173,9 @@ func (k Keeper) ValidateFundPlan(ctx context.Context, plan types.FundPlan) error } func (k Keeper) ExecuteFundPlan(ctx context.Context, plan types.FundPlan, authority sdk.AccAddress) error { + if err := k.assertGovAuthority(authority); err != nil { + return err + } if err := k.ValidateFundPlan(ctx, plan); err != nil { return err } diff --git a/x/fund/keeper/keeper_test.go b/x/fund/keeper/keeper_test.go index 88f0b5e..c390629 100644 --- a/x/fund/keeper/keeper_test.go +++ b/x/fund/keeper/keeper_test.go @@ -12,6 +12,8 @@ import ( "github.com/cosmos/cosmos-sdk/testutil" sdk "github.com/cosmos/cosmos-sdk/types" moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" "uagd/x/fund/keeper" @@ -20,7 +22,7 @@ import ( ) type fixture struct { - ctx context.Context + ctx sdk.Context keeper keeper.Keeper addressCodec address.Codec } @@ -33,8 +35,10 @@ func initFixture(t *testing.T) *fixture { storeKey := storetypes.NewKVStoreKey(types.StoreKey) storeService := runtime.NewKVStoreService(storeKey) ctx := testutil.DefaultContextWithDB(t, storeKey, storetypes.NewTransientStoreKey("transient_fund")).Ctx + ctx = ctx.WithContext(sdk.WrapSDKContext(ctx)) - k := keeper.NewKeeper(storeService, encCfg.Codec, addressCodec, mockBankKeeper{}, mockStakingKeeper{validator: stakingValidator(addressCodec)}) + govAuthority := authtypes.NewModuleAddress(govtypes.ModuleName) + k := keeper.NewKeeper(storeService, encCfg.Codec, addressCodec, mockBankKeeper{}, mockStakingKeeper{validator: stakingValidator(addressCodec)}, govAuthority) if err := k.Params.Set(ctx, types.DefaultParams()); err != nil { t.Fatalf("failed to set params: %v", err) } diff --git a/x/fund/keeper/msg_server.go b/x/fund/keeper/msg_server.go index 88bc9b5..5e901d1 100644 --- a/x/fund/keeper/msg_server.go +++ b/x/fund/keeper/msg_server.go @@ -3,8 +3,6 @@ package keeper import ( "context" - sdk "github.com/cosmos/cosmos-sdk/types" - "uagd/x/fund/types" ) @@ -20,19 +18,5 @@ func NewMsgServerImpl(k Keeper) types.MsgServer { } func (m msgServer) ExecuteFundPlan(ctx context.Context, msg *types.MsgExecuteFundPlan) (*types.MsgExecuteFundPlanResponse, error) { - params, err := m.GetParams(ctx) - if err != nil { - return nil, err - } - if params.Admin != msg.Authority { - return nil, types.ErrUnauthorized - } - authAddr, err := sdk.AccAddressFromBech32(msg.Authority) - if err != nil { - return nil, err - } - if err := m.Keeper.ExecuteFundPlan(ctx, *msg.Plan, authAddr); err != nil { - return nil, err - } - return &types.MsgExecuteFundPlanResponse{}, nil + return nil, types.ErrDirectExecDisabled } diff --git a/x/fund/keeper/msg_server_test.go b/x/fund/keeper/msg_server_test.go new file mode 100644 index 0000000..ac2112f --- /dev/null +++ b/x/fund/keeper/msg_server_test.go @@ -0,0 +1,23 @@ +package keeper_test + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + + testkeeper "uagd/testutil/keeper" + "uagd/x/fund/keeper" + "uagd/x/fund/types" +) + +func TestExecuteFundPlanDirectDisabled(t *testing.T) { + k, ctx := testkeeper.FundKeeper(t) + msgServer := keeper.NewMsgServerImpl(k) + + zeroAddr := sdk.AccAddress(make([]byte, 20)) + msg := &types.MsgExecuteFundPlan{Authority: zeroAddr.String(), Plan: &types.FundPlan{FundAddress: zeroAddr.String()}} + _, err := msgServer.ExecuteFundPlan(ctx, msg) + if err == nil || err != types.ErrDirectExecDisabled { + t.Fatalf("expected direct execution disabled error, got %v", err) + } +} diff --git a/x/fund/module/depinject.go b/x/fund/module/depinject.go index c3cc6d0..0670a6e 100644 --- a/x/fund/module/depinject.go +++ b/x/fund/module/depinject.go @@ -6,7 +6,9 @@ import ( "cosmossdk.io/core/store" "cosmossdk.io/depinject" "cosmossdk.io/depinject/appconfig" - "github.com/cosmos/cosmos-sdk/codec" +"github.com/cosmos/cosmos-sdk/codec" +authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" +govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" "uagd/x/fund/keeper" "uagd/x/fund/types" @@ -44,7 +46,12 @@ type ModuleOutputs struct { } func ProvideModule(in ModuleInputs) ModuleOutputs { - k := keeper.NewKeeper(in.StoreService, in.Cdc, in.AddressCodec, in.BankKeeper, in.StakingKeeper) - m := NewAppModule(in.Cdc, k, in.AccountKeeper, in.BankKeeper) - return ModuleOutputs{FundKeeper: k, Module: m} +authority := authtypes.NewModuleAddress(govtypes.ModuleName) +if in.Config != nil && in.Config.Authority != "" { +authority = authtypes.NewModuleAddressOrBech32Address(in.Config.Authority) +} + +k := keeper.NewKeeper(in.StoreService, in.Cdc, in.AddressCodec, in.BankKeeper, in.StakingKeeper, authority) +m := NewAppModule(in.Cdc, k, in.AccountKeeper, in.BankKeeper) +return ModuleOutputs{FundKeeper: k, Module: m} } diff --git a/x/fund/types/codec.go b/x/fund/types/codec.go index 134d1cf..a743ed6 100644 --- a/x/fund/types/codec.go +++ b/x/fund/types/codec.go @@ -34,4 +34,9 @@ func getModuleCodec() codec.Codec { return ModuleCdc } +// GetFundPlanCodec exposes the JSON/proto codec for fund plan serialization. +func GetFundPlanCodec() codec.Codec { + return getModuleCodec() +} + func RegisterLegacyAminoCodec(_ *codec.LegacyAmino) {} diff --git a/x/fund/types/errors.go b/x/fund/types/errors.go index 4cbb439..8941b01 100644 --- a/x/fund/types/errors.go +++ b/x/fund/types/errors.go @@ -5,10 +5,11 @@ import ( ) var ( - ErrFundNotFound = errorsmod.Register(ModuleName, 1, "fund not found") - ErrFundInactive = errorsmod.Register(ModuleName, 2, "fund is inactive") - ErrInvalidDenom = errorsmod.Register(ModuleName, 3, "invalid denom") - ErrDelegationLimit = errorsmod.Register(ModuleName, 4, "delegation limit exceeded") - ErrPayrollLimit = errorsmod.Register(ModuleName, 5, "payroll limit exceeded") - ErrUnauthorized = errorsmod.Register(ModuleName, 6, "unauthorized") +ErrFundNotFound = errorsmod.Register(ModuleName, 1, "fund not found") +ErrFundInactive = errorsmod.Register(ModuleName, 2, "fund is inactive") +ErrInvalidDenom = errorsmod.Register(ModuleName, 3, "invalid denom") +ErrDelegationLimit = errorsmod.Register(ModuleName, 4, "delegation limit exceeded") +ErrPayrollLimit = errorsmod.Register(ModuleName, 5, "payroll limit exceeded") +ErrUnauthorized = errorsmod.Register(ModuleName, 6, "unauthorized") +ErrDirectExecDisabled = errorsmod.Register(ModuleName, 7, "direct execution disabled; use governance-approved plan") ) diff --git a/x/ugov/keeper/msg_server.go b/x/ugov/keeper/msg_server.go index 9cd4476..6fa3002 100644 --- a/x/ugov/keeper/msg_server.go +++ b/x/ugov/keeper/msg_server.go @@ -6,6 +6,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" + fundtypes "uagd/x/fund/types" "uagd/x/ugov/types" ) @@ -98,12 +99,26 @@ func (s MsgServer) ExecuteFundPlan(goCtx context.Context, msg *types.MsgExecuteF if !ok { return nil, fmt.Errorf("plan not found") } - if plan.Status != types.PLAN_STATUS_SUBMITTED && plan.Status != types.PLAN_STATUS_DRAFT { - return nil, fmt.Errorf("plan must be SUBMITTED (or DRAFT for dev) to execute") + if plan.Status == types.PLAN_STATUS_EXECUTED { + return nil, fmt.Errorf("plan already executed") } + if plan.Status != types.PLAN_STATUS_SUBMITTED { + return nil, fmt.Errorf("plan must be SUBMITTED before execution") + } + + var fundPlan fundtypes.FundPlan + if err := fundtypes.GetFundPlanCodec().UnmarshalJSON(plan.PlanJSON, &fundPlan); err != nil { + return nil, fmt.Errorf("invalid stored plan: %w", err) + } + if fundPlan.Id == 0 { + fundPlan.Id = plan.Id + } + fundPlan.FundAddress = plan.FundAddress - // TODO: decode plan.PlanJSON -> fundtypes.FundPlan and call: - // err := s.fundKeeper.ExecuteFundPlan(ctx, decodedPlan, sdk.MustAccAddressFromBech32(msg.Authority)) + plan.Status = types.PLAN_STATUS_APPROVED + if err := s.fundKeeper.ExecuteFundPlan(ctx, fundPlan, sdk.MustAccAddressFromBech32(msg.Authority)); err != nil { + return nil, err + } plan.Status = types.PLAN_STATUS_EXECUTED plan.ExecutedAtHeight = ctx.BlockHeight() diff --git a/x/ugov/keeper/msg_server_test.go b/x/ugov/keeper/msg_server_test.go new file mode 100644 index 0000000..5e21295 --- /dev/null +++ b/x/ugov/keeper/msg_server_test.go @@ -0,0 +1,119 @@ +package keeper_test + +import ( + "context" + "testing" + + "cosmossdk.io/log" + storetypes "cosmossdk.io/store/types" + "github.com/cosmos/cosmos-sdk/runtime" + "github.com/cosmos/cosmos-sdk/testutil" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + paramskeeper "github.com/cosmos/cosmos-sdk/x/params/keeper" + + fundtypes "uagd/x/fund/types" + ugovkeeper "uagd/x/ugov/keeper" + "uagd/x/ugov/types" +) + +type stubFundKeeper struct { + called int + lastPlan fundtypes.FundPlan + lastAuth sdk.AccAddress + err error +} + +func (s *stubFundKeeper) ExecuteFundPlan(ctx context.Context, plan fundtypes.FundPlan, authority sdk.AccAddress) error { + s.called++ + s.lastPlan = plan + s.lastAuth = authority + return s.err +} + +func setupKeeper(t *testing.T) (ugovkeeper.Keeper, sdk.Context, *stubFundKeeper) { + t.Helper() + encCfg := moduletestutil.MakeTestEncodingConfig() + storeKey := storetypes.NewKVStoreKey(types.StoreKey) + tkey := storetypes.NewTransientStoreKey("transient_ugov_test") + ctx := testutil.DefaultContextWithDB(t, storeKey, tkey).Ctx.WithLogger(log.NewNopLogger()) + ctx = ctx.WithContext(sdk.WrapSDKContext(ctx)) + + pk := paramskeeper.NewKeeper(encCfg.Codec, encCfg.Amino, storeKey, tkey) + subspace := pk.Subspace(types.ModuleName) + + fund := &stubFundKeeper{} + authority := authtypes.NewModuleAddress(govtypes.ModuleName) + k := ugovkeeper.NewKeeper(encCfg.Codec, runtime.NewKVStoreService(storeKey), subspace, fund, authority) + + // set national president to satisfy plan creation + president := types.President{RoleType: types.PRESIDENT_TYPE_NATIONAL, Address: sampleAcc(), Active: true} + k.SetPresident(ctx, president) + + return k, ctx, fund +} + +func sampleAcc() string { + pk := authtypes.NewModuleAddress("sampler") + return sdk.AccAddress(pk).String() +} + +func TestExecuteFundPlanRequiresSubmittedStatus(t *testing.T) { + k, ctx, _ := setupKeeper(t) + msgServer := ugovkeeper.NewMsgServerImpl(k) + + planJSON, _ := fundtypes.GetFundPlanCodec().MarshalJSON(&fundtypes.FundPlan{FundAddress: sampleAcc()}) + id, err := k.CreatePlan(ctx, sampleAcc(), sampleAcc(), "title", "desc", types.PRESIDENT_TYPE_NATIONAL, "", planJSON) + if err != nil { + t.Fatalf("create plan: %v", err) + } + + _, err = msgServer.ExecuteFundPlan(ctx, &types.MsgExecuteFundPlan{Authority: authtypes.NewModuleAddress(govtypes.ModuleName).String(), PlanId: id}) + if err == nil { + t.Fatalf("expected error for non-submitted plan") + } +} + +func TestExecuteFundPlanHappyPath(t *testing.T) { + k, ctx, fund := setupKeeper(t) + ctx = ctx.WithBlockHeight(10) + msgServer := ugovkeeper.NewMsgServerImpl(k) + + planJSON, _ := fundtypes.GetFundPlanCodec().MarshalJSON(&fundtypes.FundPlan{FundAddress: sampleAcc()}) + id, err := k.CreatePlan(ctx, sampleAcc(), sampleAcc(), "title", "desc", types.PRESIDENT_TYPE_NATIONAL, "", planJSON) + if err != nil { + t.Fatalf("create plan: %v", err) + } + if err := k.MarkSubmitted(ctx, id, 1); err != nil { + t.Fatalf("mark submitted: %v", err) + } + + authority := authtypes.NewModuleAddress(govtypes.ModuleName) + res, err := msgServer.ExecuteFundPlan(ctx, &types.MsgExecuteFundPlan{Authority: authority.String(), PlanId: id}) + if err != nil { + t.Fatalf("execute plan: %v", err) + } + + if fund.called != 1 { + t.Fatalf("expected fund keeper to be called") + } + if !fund.lastAuth.Equals(authority) { + t.Fatalf("unexpected authority used for execution") + } + if res == nil { + t.Fatalf("expected response") + } + + stored, ok := k.GetPlan(ctx, id) + if !ok { + t.Fatalf("plan missing after execution") + } + if stored.Status != types.PLAN_STATUS_EXECUTED { + t.Fatalf("expected executed status, got %d", stored.Status) + } + if stored.ExecutedAtHeight != ctx.BlockHeight() { + t.Fatalf("expected executed height %d got %d", ctx.BlockHeight(), stored.ExecutedAtHeight) + } +} From 7661b3d378c387df19573c6731b04e3e643ad310 Mon Sep 17 00:00:00 2001 From: Honchar Denys Date: Sun, 14 Dec 2025 11:19:30 +0200 Subject: [PATCH 2/4] fix validating JSON --- x/ugov/keeper/keeper.go | 18 ++++++++++++++---- x/ugov/keeper/msg_server_test.go | 14 ++++++++++++++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/x/ugov/keeper/keeper.go b/x/ugov/keeper/keeper.go index da7e71a..a60260c 100644 --- a/x/ugov/keeper/keeper.go +++ b/x/ugov/keeper/keeper.go @@ -2,7 +2,6 @@ package keeper import ( "encoding/binary" - "encoding/json" "fmt" corestore "cosmossdk.io/core/store" @@ -12,6 +11,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" paramtypes "github.com/cosmos/cosmos-sdk/x/params/types" + fundtypes "uagd/x/fund/types" "uagd/x/ugov/types" ) @@ -205,8 +205,18 @@ func (k Keeper) CreatePlan(ctx sdk.Context, creator, fundAddr, title, desc strin if err := k.MustBePresident(ctx, creator, role, regionId); err != nil { return 0, err } - var tmp any - if err := json.Unmarshal(planJSON, &tmp); err != nil { + var plan fundtypes.FundPlan + if err := fundtypes.GetFundPlanCodec().UnmarshalJSON(planJSON, &plan); err != nil { + return 0, fmt.Errorf("invalid plan_json: %w", err) + } + + plan.Id = 0 + plan.FundAddress = fundAddr + plan.Title = title + plan.Description = desc + + normalizedJSON, err := fundtypes.GetFundPlanCodec().MarshalJSON(&plan) + if err != nil { return 0, fmt.Errorf("invalid plan_json: %w", err) } @@ -220,7 +230,7 @@ func (k Keeper) CreatePlan(ctx sdk.Context, creator, fundAddr, title, desc strin Status: types.PLAN_STATUS_DRAFT, GovProposalId: 0, CreatedAtHeight: ctx.BlockHeight(), - PlanJSON: planJSON, + PlanJSON: normalizedJSON, } k.SetPlan(ctx, sp) return id, nil diff --git a/x/ugov/keeper/msg_server_test.go b/x/ugov/keeper/msg_server_test.go index 5e21295..5cf7841 100644 --- a/x/ugov/keeper/msg_server_test.go +++ b/x/ugov/keeper/msg_server_test.go @@ -2,6 +2,7 @@ package keeper_test import ( "context" + "strings" "testing" "cosmossdk.io/log" @@ -117,3 +118,16 @@ func TestExecuteFundPlanHappyPath(t *testing.T) { t.Fatalf("expected executed height %d got %d", ctx.BlockHeight(), stored.ExecutedAtHeight) } } + +func TestCreatePlanRejectsInvalidJSON(t *testing.T) { + k, ctx, _ := setupKeeper(t) + + invalidJSON := []byte(`{"foo":"bar"}`) + _, err := k.CreatePlan(ctx, sampleAcc(), sampleAcc(), "title", "desc", types.PRESIDENT_TYPE_NATIONAL, "", invalidJSON) + if err == nil { + t.Fatalf("expected invalid plan_json error") + } + if !strings.Contains(err.Error(), "invalid plan_json") { + t.Fatalf("unexpected error: %v", err) + } +} From d364c7058545b19e067f436175b8b877a138a4ab Mon Sep 17 00:00:00 2001 From: Honchar Denys Date: Sun, 14 Dec 2025 11:21:55 +0200 Subject: [PATCH 3/4] fixed CI/CD --- .github/workflows/build.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ad3bcc3..9c7bf52 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,7 +4,6 @@ on: push: branches-ignore: - master - pull_request: jobs: check-node: From 26d855c4e85f3b9e860341e3c196499f0826b2b1 Mon Sep 17 00:00:00 2001 From: Honchar Denys Date: Sun, 14 Dec 2025 11:33:42 +0200 Subject: [PATCH 4/4] fixed errors --- x/ugov/keeper/keeper.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/x/ugov/keeper/keeper.go b/x/ugov/keeper/keeper.go index a60260c..a424098 100644 --- a/x/ugov/keeper/keeper.go +++ b/x/ugov/keeper/keeper.go @@ -1,8 +1,9 @@ package keeper import ( - "encoding/binary" - "fmt" + "encoding/binary" + "encoding/json" + "fmt" corestore "cosmossdk.io/core/store" storetypes "cosmossdk.io/store/types"