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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
23 changes: 22 additions & 1 deletion x/rewards/types/genesis.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package types

import "fmt"

// NewGenesisState constructs a genesis state
func NewGenesisState(
params Params, rp RewardPool, release ReleaseSchedule,
Expand Down Expand Up @@ -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)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return nil
}
40 changes: 40 additions & 0 deletions x/rewards/types/genesis_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
5 changes: 2 additions & 3 deletions x/rewards/types/reward_pool.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
72 changes: 68 additions & 4 deletions x/rewards/types/reward_pool_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
})
}
}
Loading