-
Notifications
You must be signed in to change notification settings - Fork 54
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(rollappparams): verify denom for gov proposal gas price change (#648
- Loading branch information
Showing
2 changed files
with
262 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
package proposal | ||
|
||
import ( | ||
"encoding/json" | ||
"fmt" | ||
|
||
sdk "github.com/cosmos/cosmos-sdk/types" | ||
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" | ||
bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" | ||
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1" | ||
paramstypes "github.com/cosmos/cosmos-sdk/x/params/types" | ||
"github.com/cosmos/cosmos-sdk/x/params/types/proposal" | ||
"github.com/tendermint/tendermint/libs/log" | ||
|
||
rollappparamstypes "github.com/dymensionxyz/dymension-rdk/x/rollappparams/types" | ||
) | ||
|
||
type ( | ||
paramsKeeper interface { | ||
GetSubspace(string) (paramstypes.Subspace, bool) | ||
Logger(ctx sdk.Context) log.Logger | ||
} | ||
paramsSubspace interface { | ||
Update(ctx sdk.Context, key, value []byte) error | ||
} | ||
getSubspaceFn func(subspace string) (paramsSubspace, bool) | ||
logInfoFn func(msg string, keyvals ...interface{}) | ||
hasDenomMetaDataFn func(ctx sdk.Context, denom string) bool | ||
) | ||
|
||
// NewCustomParamChangeProposalHandler creates a new governance Handler for a ParamChangeProposal | ||
// that includes additional validation logic for the minGasPrices parameter using bankKeeper. | ||
func NewCustomParamChangeProposalHandler(paramKeeper paramsKeeper, bankKeeper bankkeeper.Keeper) govtypes.Handler { | ||
return func(ctx sdk.Context, content govtypes.Content) error { | ||
switch c := content.(type) { | ||
case *proposal.ParameterChangeProposal: | ||
getSubspace := func(subspace string) (paramsSubspace, bool) { | ||
return paramKeeper.GetSubspace(subspace) | ||
} | ||
return handleCustomParameterChangeProposal( | ||
ctx, | ||
getSubspace, | ||
paramKeeper.Logger(ctx).Info, | ||
bankKeeper.HasDenomMetaData, | ||
c.Changes, | ||
) | ||
default: | ||
return sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized param proposal content type: %T", c) | ||
} | ||
} | ||
} | ||
|
||
func handleCustomParameterChangeProposal( | ||
ctx sdk.Context, | ||
getSubspace getSubspaceFn, | ||
logInfo logInfoFn, | ||
hasDenom hasDenomMetaDataFn, | ||
changes []proposal.ParamChange, | ||
) error { | ||
for _, c := range changes { | ||
ss, ok := getSubspace(c.Subspace) | ||
if !ok { | ||
return sdkerrors.Wrap(proposal.ErrUnknownSubspace, c.Subspace) | ||
} | ||
|
||
logInfo( | ||
fmt.Sprintf("attempting to set new parameter value; subspace: %s, key: %s, value: %s", c.Subspace, c.Key, c.Value), | ||
) | ||
|
||
// additional validation for minGasPrices in rollappparams | ||
if err := validateMinGasPriceParamChange(ctx, c, hasDenom); err != nil { | ||
return err | ||
} | ||
|
||
if err := ss.Update(ctx, []byte(c.Key), []byte(c.Value)); err != nil { | ||
return sdkerrors.Wrapf(proposal.ErrSettingParameter, "key: %s, value: %s, err: %s", c.Key, c.Value, err.Error()) | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func validateMinGasPriceParamChange(ctx sdk.Context, c proposal.ParamChange, hasDenom hasDenomMetaDataFn) error { | ||
if c.Subspace == rollappparamstypes.ModuleName && c.Key == string(rollappparamstypes.KeyMinGasPrices) { | ||
var decCoins sdk.DecCoins | ||
if err := json.Unmarshal([]byte(c.Value), &decCoins); err != nil { | ||
return sdkerrors.Wrapf(sdkerrors.ErrJSONUnmarshal, "failed to unmarshal minGasPrices: %v", err) | ||
} | ||
|
||
// validate each denom exists before allowing the param change | ||
for _, decCoin := range decCoins { | ||
if !hasDenom(ctx, decCoin.Denom) { | ||
return sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "denom %s does not exist or has no metadata", decCoin.Denom) | ||
} | ||
} | ||
} | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
package proposal | ||
|
||
import ( | ||
"errors" | ||
"testing" | ||
|
||
sdk "github.com/cosmos/cosmos-sdk/types" | ||
"github.com/cosmos/cosmos-sdk/x/params/types/proposal" | ||
"github.com/stretchr/testify/require" | ||
"github.com/tendermint/tendermint/libs/log" | ||
tmtypes "github.com/tendermint/tendermint/proto/tendermint/types" | ||
|
||
rollappparamstypes "github.com/dymensionxyz/dymension-rdk/x/rollappparams/types" | ||
) | ||
|
||
func TestCustomParameterChangeProposalHandler(t *testing.T) { | ||
logger := log.NewNopLogger() | ||
ctx := sdk.NewContext(nil, tmtypes.Header{}, false, logger) | ||
hasDenom := func(ctx sdk.Context, denom string) bool { return true } | ||
hasNotDenom := func(ctx sdk.Context, denom string) bool { return false } | ||
|
||
tests := []struct { | ||
name string | ||
changes []proposal.ParamChange | ||
getSubspace getSubspaceFn | ||
hasDenom hasDenomMetaDataFn | ||
expectErr bool | ||
errContains string | ||
}{ | ||
{ | ||
name: "non rollappparams subspace (no special checks)", | ||
changes: []proposal.ParamChange{ | ||
{ | ||
Subspace: "othersubspace", | ||
Key: "someKey", | ||
Value: "someValue", | ||
}, | ||
}, | ||
getSubspace: func(subspace string) (paramsSubspace, bool) { | ||
return mockSubspace{}, true | ||
}, | ||
hasDenom: nil, | ||
expectErr: false, | ||
}, | ||
{ | ||
name: "rollappparams but different key than minGasPrices", | ||
changes: []proposal.ParamChange{ | ||
{ | ||
Subspace: rollappparamstypes.ModuleName, | ||
Key: "someOtherKey", | ||
Value: "something", | ||
}, | ||
}, | ||
getSubspace: func(subspace string) (paramsSubspace, bool) { | ||
return mockSubspace{}, true | ||
}, | ||
hasDenom: nil, | ||
expectErr: false, | ||
}, | ||
{ | ||
name: "rollappparams minGasPrices - valid denoms", | ||
changes: []proposal.ParamChange{ | ||
{ | ||
Subspace: rollappparamstypes.ModuleName, | ||
Key: string(rollappparamstypes.KeyMinGasPrices), | ||
Value: `[{"denom":"adenom","amount":"20000000000.0"}]`, | ||
}, | ||
}, | ||
getSubspace: func(subspace string) (paramsSubspace, bool) { | ||
return mockSubspace{}, true | ||
}, | ||
hasDenom: hasDenom, | ||
expectErr: false, | ||
}, | ||
{ | ||
name: "rollappparams minGasPrices - unknown denom", | ||
changes: []proposal.ParamChange{ | ||
{ | ||
Subspace: rollappparamstypes.ModuleName, | ||
Key: string(rollappparamstypes.KeyMinGasPrices), | ||
Value: `[{"denom":"baddenom","amount":"20000000000.0"}]`, | ||
}, | ||
}, | ||
getSubspace: func(subspace string) (paramsSubspace, bool) { | ||
return mockSubspace{}, true | ||
}, | ||
hasDenom: hasNotDenom, | ||
expectErr: true, | ||
errContains: "denom baddenom does not exist", | ||
}, | ||
{ | ||
name: "rollappparams minGasPrices - invalid JSON", | ||
changes: []proposal.ParamChange{ | ||
{ | ||
Subspace: rollappparamstypes.ModuleName, | ||
Key: string(rollappparamstypes.KeyMinGasPrices), | ||
Value: `not-json`, | ||
}, | ||
}, | ||
getSubspace: func(subspace string) (paramsSubspace, bool) { | ||
return mockSubspace{}, true | ||
}, | ||
hasDenom: nil, | ||
expectErr: true, | ||
errContains: "failed to unmarshal minGasPrices", | ||
}, | ||
{ | ||
name: "unknown subspace", | ||
changes: []proposal.ParamChange{ | ||
{ | ||
Subspace: "unknownsubspace", | ||
Key: "someKey", | ||
Value: "someValue", | ||
}, | ||
}, | ||
getSubspace: func(subspace string) (paramsSubspace, bool) { | ||
return mockSubspace{}, false | ||
}, | ||
hasDenom: hasDenom, | ||
expectErr: true, | ||
errContains: "unknown subspace", | ||
}, | ||
{ | ||
name: "error on update", | ||
changes: []proposal.ParamChange{ | ||
{ | ||
Subspace: rollappparamstypes.ModuleName, | ||
Key: "someKey", | ||
Value: "someValue", | ||
}, | ||
}, | ||
getSubspace: func(subspace string) (paramsSubspace, bool) { | ||
return mockSubspace{ | ||
updateErr: errors.New("update failed"), | ||
}, true | ||
}, | ||
hasDenom: hasDenom, | ||
expectErr: true, | ||
errContains: "update failed", | ||
}, | ||
} | ||
|
||
for _, tc := range tests { | ||
t.Run(tc.name, func(t *testing.T) { | ||
err := handleCustomParameterChangeProposal(ctx, tc.getSubspace, logger.Info, tc.hasDenom, tc.changes) | ||
if tc.expectErr { | ||
require.Error(t, err) | ||
if tc.errContains != "" { | ||
require.Contains(t, err.Error(), tc.errContains) | ||
} | ||
} else { | ||
require.NoError(t, err) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
type mockSubspace struct { | ||
updateErr error | ||
} | ||
|
||
func (m mockSubspace) Update(sdk.Context, []byte, []byte) error { | ||
return m.updateErr | ||
} |