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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,14 @@
- 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
- Ensure feeTokenMetadata initial prices after updateFeeTokenMetadata is picked up from oracle
- Use `DecCoins.Validate()` on `RewardPool.ValidateGenesis` to catch malformed denom formats, duplicate denoms, bad ordering
- Enforce denom consistency in `GenesisState.Validate` with `Params.TokenDenom`

### Removed

- Removed price field input in updateTokenMetadata request

## v7.1.0-mainnet - 2026-03-13

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion proto/kiichain/feeabstraction/v1beta1/params.proto
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ message FeeTokenMetadata {
(gogoproto.nullable) = false
];
// Enabled indicates if the token is enabled for fee abstraction
bool enabled = 6;
bool enabled = 5;
}

// Defines a collection of fee token metadata
Expand Down
23 changes: 21 additions & 2 deletions proto/kiichain/feeabstraction/v1beta1/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,29 @@ message MsgUpdateFeeTokens {
// authority is the address of the governance account.
string authority = 1 [ (cosmos_proto.scalar) = "cosmos.AddressString" ];

// fee_tokens defines the fee tokens to update.
FeeTokenMetadataCollection fee_tokens = 2 [ (gogoproto.nullable) = false ];
// tokens defines the fee tokens to update.
UpdateTokenMetadataCollection tokens = 2 [ (gogoproto.nullable) = false ];
}

// UpdateTokenMetadata defines the metadata for a fee token to be updated
message UpdateTokenMetadata {
// Denom is the token denom
string denom = 1;
// Identifier on the oracle module
string oracle_denom = 2;
// Decimals is the number of decimals for the token
uint32 decimals = 3;
// Enabled indicates if the token is enabled for fee abstraction
bool enabled = 4;
}

// Defines a collection of fee token metadata to be updated
message UpdateTokenMetadataCollection {
// Items is a repeated field of UpdateTokenMetadata
repeated UpdateTokenMetadata items = 1 [ (gogoproto.nullable) = false ];
}


// MsgUpdateFeeTokensResponse defines the response structure for update fee
// tokens
message MsgUpdateFeeTokensResponse {}
32 changes: 22 additions & 10 deletions x/feeabstraction/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,13 +115,6 @@ message Params {
];
// TwapLookbackWindow is the lookback window for calculating TWAPs
uint64 twap_lookback_window = 5;
// FallbackNativePrice is the fallback price for the native token if the
// oracle price is not available (in USD)
string fallback_native_price = 6 [
(gogoproto.moretags) = "yaml:\"fallback_native_price\"",
(gogoproto.customtype) = "cosmossdk.io/math.LegacyDec",
(gogoproto.nullable) = false
];
}
```

Expand All @@ -147,7 +140,7 @@ message FeeTokenMetadata {
(gogoproto.nullable) = false
];
// Enabled indicates if the token is enabled for fee abstraction
bool enabled = 6;
bool enabled = 5;
}

// Defines a collection of fee token metadata
Expand Down Expand Up @@ -188,6 +181,7 @@ Only the governance account can update the fee tokens, and it requires a valid s
On each update, all fee tokens must be provided, even if they are not changed:

- Ordering is important, since the balance will be calculated based on the order of the fee tokens.
- Prices are always set to zero so that BeginBlocker adopts the current TWAP.

It is defined as:

Expand All @@ -200,8 +194,26 @@ message MsgUpdateFeeTokens {
// authority is the address of the governance account.
string authority = 1 [ (cosmos_proto.scalar) = "cosmos.AddressString" ];

// fee_tokens defines the fee tokens to update.
FeeTokenMetadataCollection fee_tokens = 2 [ (gogoproto.nullable) = false ];
// tokens defines the fee tokens to update.
UpdateTokenMetadataCollection tokens = 2 [ (gogoproto.nullable) = false ];
}

// UpdateTokenMetadata defines the metadata for a fee token to be updated
message UpdateTokenMetadata {
// Denom is the token denom
string denom = 1;
// Identifier on the oracle module
string oracle_denom = 2;
// Decimals is the number of decimals for the token
uint32 decimals = 3;
// Enabled indicates if the token is enabled for fee abstraction
bool enabled = 4;
}

// Defines a collection of fee token metadata to be updated
message UpdateTokenMetadataCollection {
// Items is a repeated field of UpdateTokenMetadata
repeated UpdateTokenMetadata items = 1 [ (gogoproto.nullable) = false ];
}
```

Expand Down
14 changes: 12 additions & 2 deletions x/feeabstraction/keeper/msg_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"

"cosmossdk.io/errors"
"cosmossdk.io/math"

sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
Expand Down Expand Up @@ -103,14 +104,23 @@ func (ms MsgServer) UpdateFeeTokens(ctx context.Context, msg *types.MsgUpdateFee
for _, denom := range voteTargets {
voteTargetMap[denom] = struct{}{}
}
for _, feeToken := range msg.FeeTokens.Items {
for _, feeToken := range msg.Tokens.Items {
if _, ok := voteTargetMap[feeToken.OracleDenom]; !ok {
return nil, sdkerrors.ErrInvalidRequest.Wrapf("fee token denom %s is not registered on the oracle module", feeToken.OracleDenom)
}
}

// Zero out all token prices so BeginBlocker adopts the current TWAP immediately
Comment thread
Thaleszh marked this conversation as resolved.
// rather than being clamped from a stale governance-submitted price.
// Clamp skip can be found here on /x/feeabstraction/types/fee.go#L55
resetItems := make([]types.FeeTokenMetadata, len(msg.Tokens.Items))
for i, token := range msg.Tokens.Items {
resetItems[i] = types.NewFeeTokenMetadata(token.Denom, token.OracleDenom, token.Decimals, math.LegacyZeroDec())
}
resetFeeTokens := types.NewFeeTokenMetadataCollection(resetItems...)
Comment thread
Thaleszh marked this conversation as resolved.

// Update the fee tokens
if err := ms.FeeTokens.Set(ctx, msg.FeeTokens); err != nil {
if err := ms.FeeTokens.Set(ctx, *resetFeeTokens); err != nil {
return nil, sdkerrors.ErrInvalidRequest.Wrapf("failed to update fee tokens: %s", err)
}

Expand Down
24 changes: 14 additions & 10 deletions x/feeabstraction/keeper/msg_server_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package keeper_test

import (
"cosmossdk.io/math"

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"
Expand Down Expand Up @@ -129,10 +127,10 @@ func (s *KeeperTestSuite) TestUpdateParams() {

// TestUpdateFeeTokens tests the UpdateFeeTokens method
func (s *KeeperTestSuite) TestUpdateFeeTokens() {
defaultFeeTokens := types.NewFeeTokenMetadataCollection(
types.NewFeeTokenMetadata("one", "oracleone", 6, math.LegacyMustNewDecFromStr("0.01")),
types.NewFeeTokenMetadata("two", "oracletwo", 6, math.LegacyMustNewDecFromStr("0.01")),
types.NewFeeTokenMetadata("three", "oraclethree", 6, math.LegacyMustNewDecFromStr("0.01")))
defaultFeeTokens := types.NewUpdateTokenMetadataCollection(
types.NewUpdateTokenMetadata("one", "oracleone", 6),
types.NewUpdateTokenMetadata("two", "oracletwo", 6),
types.NewUpdateTokenMetadata("three", "oraclethree", 6))

// Prepare all the test cases
testCases := []struct {
Expand Down Expand Up @@ -183,16 +181,16 @@ func (s *KeeperTestSuite) TestUpdateFeeTokens() {
name: "invalid - wrong authority",
msg: &types.MsgUpdateFeeTokens{
Authority: authtypes.NewModuleAddress(types.ModuleName).String(),
FeeTokens: *defaultFeeTokens,
Tokens: *defaultFeeTokens,
},
errContains: "expected gov account as only signer for proposal message",
},
{
name: "invalid - invalid fee tokens (bad denom)",
msg: types.NewMessageUpdateFeeTokens(
authtypes.NewModuleAddress(govtypes.ModuleName).String(),
*types.NewFeeTokenMetadataCollection(
types.NewFeeTokenMetadata("invalid denom!", "oracleCoin", 6, math.LegacyMustNewDecFromStr("0.01")),
*types.NewUpdateTokenMetadataCollection(
types.NewUpdateTokenMetadata("invalid denom!", "oracleCoin", 6),
),
),
errContains: "denom is invalid: invalid fee token metadata: invalid request",
Expand Down Expand Up @@ -223,7 +221,13 @@ func (s *KeeperTestSuite) TestUpdateFeeTokens() {
// Verify the fee tokens were updated
tokens, err := s.keeper.FeeTokens.Get(cachedCtx)
s.Require().NoError(err)
s.Require().Equal(tc.msg.FeeTokens, tokens)
s.Require().Len(tokens.Items, len(tc.msg.Tokens.Items))
for i, token := range tokens.Items {
s.Require().Equal(tc.msg.Tokens.Items[i].Denom, token.Denom)
s.Require().Equal(tc.msg.Tokens.Items[i].OracleDenom, token.OracleDenom)
s.Require().Equal(tc.msg.Tokens.Items[i].Decimals, token.Decimals)
s.Require().True(token.Price.IsZero(), "expected price to be 0 for denom %s, got %s", token.Denom, token.Price)
}
}
})
}
Expand Down
6 changes: 3 additions & 3 deletions x/feeabstraction/types/msg.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@ func (msg *MsgUpdateParams) Validate() error {
}

// NewMessageUpdateFeeTokens creates a new MsgUpdateFeeTokens instance
func NewMessageUpdateFeeTokens(authority string, feeTokens FeeTokenMetadataCollection) *MsgUpdateFeeTokens {
func NewMessageUpdateFeeTokens(authority string, tokens UpdateTokenMetadataCollection) *MsgUpdateFeeTokens {
return &MsgUpdateFeeTokens{
Authority: authority,
FeeTokens: feeTokens,
Tokens: tokens,
}
}

Expand All @@ -52,5 +52,5 @@ func (msg *MsgUpdateFeeTokens) Validate() error {
}

// Validate the fee tokens
return msg.FeeTokens.Validate()
return msg.Tokens.Validate()
}
18 changes: 8 additions & 10 deletions x/feeabstraction/types/msg_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import (

"github.com/stretchr/testify/require"

"cosmossdk.io/math"

authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"

Expand Down Expand Up @@ -78,23 +76,23 @@ func TestMsgUpdateFeeTokensValidate(t *testing.T) {
name: "valid - empty fee tokens",
msg: types.NewMessageUpdateFeeTokens(
authtypes.NewModuleAddress(govtypes.ModuleName).String(),
*types.NewFeeTokenMetadataCollection(),
*types.NewUpdateTokenMetadataCollection(),
),
},
{
name: "valid - fee tokens",
msg: types.NewMessageUpdateFeeTokens(
authtypes.NewModuleAddress(govtypes.ModuleName).String(),
*types.NewFeeTokenMetadataCollection(
types.NewFeeTokenMetadata("coin", "oracleCoin", 6, math.LegacyMustNewDecFromStr("0.01")),
*types.NewUpdateTokenMetadataCollection(
types.NewUpdateTokenMetadata("coin", "oracleCoin", 6),
),
),
},
Comment thread
Thaleszh marked this conversation as resolved.
{
name: "invalid - empty authority",
msg: types.NewMessageUpdateFeeTokens("",
*types.NewFeeTokenMetadataCollection(
types.NewFeeTokenMetadata("coin", "oracleCoin", 6, math.LegacyMustNewDecFromStr("0.01")),
*types.NewUpdateTokenMetadataCollection(
types.NewUpdateTokenMetadata("coin", "oracleCoin", 6),
),
),
errContains: "empty address string is not allowed",
Expand All @@ -103,9 +101,9 @@ func TestMsgUpdateFeeTokensValidate(t *testing.T) {
name: "invalid - duplicate fee tokens",
msg: types.NewMessageUpdateFeeTokens(
authtypes.NewModuleAddress(govtypes.ModuleName).String(),
*types.NewFeeTokenMetadataCollection(
types.NewFeeTokenMetadata("coin", "oracleCoin", 6, math.LegacyMustNewDecFromStr("0.01")),
types.NewFeeTokenMetadata("coin", "oracleCoin", 6, math.LegacyMustNewDecFromStr("0.01")),
*types.NewUpdateTokenMetadataCollection(
types.NewUpdateTokenMetadata("coin", "oracleCoin", 6),
types.NewUpdateTokenMetadata("coin", "oracleCoin", 6),
),
),
errContains: "duplicate denom found: coin: invalid fee token metadata",
Expand Down
61 changes: 61 additions & 0 deletions x/feeabstraction/types/params.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,64 @@ func (c *FeeTokenMetadataCollection) Validate() error {

return nil
}

// NewUpdateTokenMetadata creates a new fee token with the given denom and decimals
func NewUpdateTokenMetadata(
denom, oracleDenom string,
decimals uint32,
) UpdateTokenMetadata {
return UpdateTokenMetadata{
Denom: denom,
OracleDenom: oracleDenom,
Decimals: decimals,
Enabled: true,
}
}

// Validate validates the UpdateTokenMetadata
func (f UpdateTokenMetadata) Validate() error {
// Validate the denom
if err := sdk.ValidateDenom(f.Denom); err != nil {
return errorsmod.Wrap(ErrInvalidFeeTokenMetadata, "denom is invalid")
}
// Validate the oracle denom
if err := sdk.ValidateDenom(f.OracleDenom); err != nil {
return errorsmod.Wrap(ErrInvalidFeeTokenMetadata, "oracle denom is invalid")
}

// Validate the decimals, must be between bigger than 0 and less than or equal to 18
if f.Decimals < 1 || f.Decimals > 18 {
return errorsmod.Wrap(ErrInvalidFeeTokenMetadata, "decimals must be between 1 and 18")
}

return nil
}

// NewUpdateTokenMetadataCollection creates a new UpdateTokenMetadataCollection
func NewUpdateTokenMetadataCollection(tokens ...UpdateTokenMetadata) *UpdateTokenMetadataCollection {
return &UpdateTokenMetadataCollection{
Items: tokens,
}
}

// Validate validates the UpdateTokenMetadataCollection
func (c *UpdateTokenMetadataCollection) Validate() error {
// Check if the collection is nil
if c == nil {
return errorsmod.Wrap(ErrInvalidFeeTokenMetadata, "fee token metadata collection cannot be nil")
}

// Validate each fee token metadata and check for duplicates
denomSet := make(map[string]struct{})
for _, token := range c.Items {
if err := token.Validate(); err != nil {
return err
}
if _, exists := denomSet[token.Denom]; exists {
return errorsmod.Wrapf(ErrInvalidFeeTokenMetadata, "duplicate denom found: %s", token.Denom)
}
denomSet[token.Denom] = struct{}{}
}

return nil
}
Loading
Loading