Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ on:
push:
branches-ignore:
- master
pull_request:

jobs:
check-node:
Expand Down
4 changes: 4 additions & 0 deletions testutil/keeper/fund.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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 {
Expand Down
21 changes: 21 additions & 0 deletions x/fund/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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]
Expand All @@ -35,14 +38,19 @@ 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,
cdc: cdc,
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)),
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
Expand Down
8 changes: 6 additions & 2 deletions x/fund/keeper/keeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -20,7 +22,7 @@ import (
)

type fixture struct {
ctx context.Context
ctx sdk.Context
keeper keeper.Keeper
addressCodec address.Codec
}
Expand All @@ -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)
}
Expand Down
18 changes: 1 addition & 17 deletions x/fund/keeper/msg_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ package keeper
import (
"context"

sdk "github.com/cosmos/cosmos-sdk/types"

"uagd/x/fund/types"
)

Expand All @@ -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
}
23 changes: 23 additions & 0 deletions x/fund/keeper/msg_server_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
15 changes: 11 additions & 4 deletions x/fund/module/depinject.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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}
}
5 changes: 5 additions & 0 deletions x/fund/types/codec.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {}
13 changes: 7 additions & 6 deletions x/fund/types/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
)
23 changes: 17 additions & 6 deletions x/ugov/keeper/keeper.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package keeper

import (
"encoding/binary"
"encoding/json"
"fmt"
"encoding/binary"
"encoding/json"
"fmt"

corestore "cosmossdk.io/core/store"
storetypes "cosmossdk.io/store/types"
Expand All @@ -12,6 +12,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"
)

Expand Down Expand Up @@ -205,8 +206,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)
}

Expand All @@ -220,7 +231,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
Expand Down
23 changes: 19 additions & 4 deletions x/ugov/keeper/msg_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

sdk "github.com/cosmos/cosmos-sdk/types"

fundtypes "uagd/x/fund/types"
"uagd/x/ugov/types"
)

Expand Down Expand Up @@ -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)
}
Comment on lines +109 to +112
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reject non-FundPlan JSON before execution

ExecuteFundPlan now attempts to decode the stored plan JSON into a fund FundPlan (fundtypes.GetFundPlanCodec().UnmarshalJSON), but plans are still accepted in CreatePlan so long as the payload is syntactically valid JSON (keeper.go lines 204‑211). Any previously stored or newly submitted plan that uses arbitrary JSON (e.g., missing delegations/payouts or using different field names) will pass proposal submission and voting but will now fail at execution with "invalid stored plan" from these lines, leaving approved proposals unexecutable. Consider validating the JSON against the FundPlan schema at creation/submission time or normalizing it before storing so governance-approved plans don’t get stuck at execution.

Useful? React with 👍 / 👎.

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()
Expand Down
Loading