diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d80040c..698d90a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ - Ensure native oracle denoms are always on whitelist and registered as vote targets when updating fee abstraction params - Validate rewards baseDenom using sdk.ValidateDenom to enforce proper denom format (min 3 chars, valid characters, no leading digits) - Ensure feeTokens is not nil at genesis +- Use `DecCoins.Validate()` on `RewardPool.ValidateGenesis` to catch malformed denom formats, duplicate denoms, bad ordering +- Enforce denom consistency in `GenesisState.Validate` with `Params.TokenDenom` ## v7.1.0-mainnet - 2026-03-13 diff --git a/x/rewards/types/genesis.go b/x/rewards/types/genesis.go index 22d1a1ea..3a7bcfd1 100644 --- a/x/rewards/types/genesis.go +++ b/x/rewards/types/genesis.go @@ -1,5 +1,7 @@ package types +import "fmt" + // NewGenesisState constructs a genesis state func NewGenesisState( params Params, rp RewardPool, release ReleaseSchedule, @@ -29,5 +31,24 @@ func (gs *GenesisState) Validate() error { if err := gs.RewardPool.ValidateGenesis(); err != nil { return err } - return gs.ReleaseSchedule.ValidateGenesis() + + if err := gs.ReleaseSchedule.ValidateGenesis(); err != nil { + return err + } + + // Enforce denom consistency: all pool coins and the active schedule must use TokenDenom. + tokenDenom := gs.Params.TokenDenom + for _, coin := range gs.RewardPool.CommunityPool { + if coin.Denom != tokenDenom { + return fmt.Errorf("community pool coin denom %s does not match token denom %s", + coin.Denom, tokenDenom) + } + } + + if gs.ReleaseSchedule.Active && gs.ReleaseSchedule.TotalAmount.Denom != tokenDenom { + return fmt.Errorf("release schedule total amount denom %s does not match token denom %s", + gs.ReleaseSchedule.TotalAmount.Denom, tokenDenom) + } + + return nil } diff --git a/x/rewards/types/genesis_test.go b/x/rewards/types/genesis_test.go index f15ea994..958f9791 100644 --- a/x/rewards/types/genesis_test.go +++ b/x/rewards/types/genesis_test.go @@ -80,6 +80,46 @@ func (suite *GenesisTestSuite) TestValidateGenesis() { }, expectedPass: false, }, + { + name: "active schedule with mismatched total amount denom", + modifyFn: func(gs *types.GenesisState) { + gs.ReleaseSchedule = types.ReleaseSchedule{ + TotalAmount: sdk.NewCoin("notkii", math.NewInt(1000)), + ReleasedAmount: sdk.NewCoin("notkii", math.NewInt(0)), + EndTime: time.Now().Add(time.Hour * 24), + Active: true, + } + }, + expectedPass: false, + }, + { + name: "community pool with foreign denom", + modifyFn: func(gs *types.GenesisState) { + gs.RewardPool = types.RewardPool{ + CommunityPool: sdk.DecCoins{ + {Denom: "notkii", Amount: math.LegacyNewDec(100)}, + }, + } + }, + expectedPass: false, + }, + { + name: "both active schedule and pool with mismatched denoms", + modifyFn: func(gs *types.GenesisState) { + gs.ReleaseSchedule = types.ReleaseSchedule{ + TotalAmount: sdk.NewCoin("notkii", math.NewInt(1000)), + ReleasedAmount: sdk.NewCoin("notkii", math.NewInt(0)), + EndTime: time.Now().Add(time.Hour * 24), + Active: true, + } + gs.RewardPool = types.RewardPool{ + CommunityPool: sdk.DecCoins{ + {Denom: "notkii", Amount: math.LegacyNewDec(100)}, + }, + } + }, + expectedPass: false, + }, } for _, tc := range testCases { diff --git a/x/rewards/types/reward_pool.go b/x/rewards/types/reward_pool.go index 403178c8..b57e67c9 100644 --- a/x/rewards/types/reward_pool.go +++ b/x/rewards/types/reward_pool.go @@ -15,9 +15,8 @@ func InitialRewardPool() RewardPool { // ValidateGenesis validates the reward pool for a genesis state func (rp RewardPool) ValidateGenesis() error { - if rp.CommunityPool.IsAnyNegative() { - return fmt.Errorf("negative CommunityPool in distribution fee pool, is %v", - rp.CommunityPool) + if err := rp.CommunityPool.Validate(); err != nil { + return fmt.Errorf("invalid CommunityPool: %w", err) } return nil diff --git a/x/rewards/types/reward_pool_test.go b/x/rewards/types/reward_pool_test.go index 93a7c9e8..ad08d754 100644 --- a/x/rewards/types/reward_pool_test.go +++ b/x/rewards/types/reward_pool_test.go @@ -13,9 +13,73 @@ import ( ) func TestRewardPoolValidateGenesis(t *testing.T) { - rp := types.InitialRewardPool() - require.Nil(t, rp.ValidateGenesis()) + testCases := []struct { + name string + pool types.RewardPool + expectErr bool + }{ + { + name: "initial empty pool", + pool: types.InitialRewardPool(), + expectErr: false, + }, + { + name: "valid single coin", + pool: types.RewardPool{ + CommunityPool: sdk.DecCoins{ + {Denom: "akii", Amount: math.LegacyNewDec(100)}, + }, + }, + expectErr: false, + }, + { + name: "negative amount", + pool: types.RewardPool{ + CommunityPool: sdk.DecCoins{ + {Denom: "tkii", Amount: math.LegacyNewDec(-1)}, + }, + }, + expectErr: true, + }, + { + name: "invalid denom format (starts with digit)", + pool: types.RewardPool{ + CommunityPool: sdk.DecCoins{ + {Denom: "1invalid", Amount: math.LegacyNewDec(1)}, + }, + }, + expectErr: true, + }, + { + name: "duplicate denoms", + pool: types.RewardPool{ + CommunityPool: sdk.DecCoins{ + {Denom: "akii", Amount: math.LegacyNewDec(1)}, + {Denom: "akii", Amount: math.LegacyNewDec(2)}, + }, + }, + expectErr: true, + }, + { + name: "non-canonical ordering", + pool: types.RewardPool{ + CommunityPool: sdk.DecCoins{ + {Denom: "tkii", Amount: math.LegacyNewDec(1)}, + {Denom: "akii", Amount: math.LegacyNewDec(2)}, + }, + }, + expectErr: true, + }, + } - rp2 := types.RewardPool{CommunityPool: sdk.DecCoins{{Denom: "tkii", Amount: math.LegacyNewDec(-1)}}} - require.NotNil(t, rp2.ValidateGenesis()) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := tc.pool.ValidateGenesis() + if tc.expectErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } }