Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v19: Enforce Validator's Max 24 Hour Change Rate To 5% #875

Merged
merged 18 commits into from
Dec 12, 2023
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
3 changes: 2 additions & 1 deletion app/ante.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,14 @@ func NewAnteHandler(options HandlerOptions) (sdk.AnteHandler, error) {
gfd := globalfeeante.NewFeeDecorator(options.BypassMinFeeMsgTypes, options.GlobalFeeKeeper, options.StakingKeeper, maxBypassMinFeeMsgGasUsage, &isFeePayTx)

anteDecorators := []sdk.AnteDecorator{
// GLobalFee query params for minimum fee
// GlobalFee query params for minimum fee
ante.NewSetUpContextDecorator(), // outermost AnteDecorator. SetUpContext must be called first
wasmkeeper.NewLimitSimulationGasDecorator(options.WasmConfig.SimulationGasLimit),
wasmkeeper.NewCountTXDecorator(options.TxCounterStoreKey),
ante.NewExtensionOptionsDecorator(options.ExtensionOptionChecker),
decorators.MsgFilterDecorator{},
ante.NewValidateBasicDecorator(),
decorators.NewChangeRateDecorator(&options.StakingKeeper),
Reecepbcups marked this conversation as resolved.
Show resolved Hide resolved
ante.NewTxTimeoutHeightDecorator(),
ante.NewValidateMemoDecorator(options.AccountKeeper),
ante.NewConsumeGasForTxSizeDecorator(options.AccountKeeper),
Expand Down
112 changes: 112 additions & 0 deletions app/decorators/change_rate_decorator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package decorators

import (
"fmt"

sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/authz"
stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
)

const (
MaxChangeRate = "0.05"
)

// MsgChangeRateDecorator defines the AnteHandler that filters & prevents messages
// that create validators and exceed the max change rate of 5%.
type MsgChangeRateDecorator struct {
sk *stakingkeeper.Keeper
maxCommissionChangeRate sdk.Dec
}

// Create new Change Rate Decorator
func NewChangeRateDecorator(sk *stakingkeeper.Keeper) MsgChangeRateDecorator {
rate, err := sdk.NewDecFromStr(MaxChangeRate)
if err != nil {
panic(err)
}

return MsgChangeRateDecorator{
sk: sk,
maxCommissionChangeRate: rate,
}
}

// The AnteHandle checks for transactions that exceed the max change rate of 5% on the
// creation of a validator.
func (mcr MsgChangeRateDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) {
err := mcr.hasInvalidCommissionRateMsgs(ctx, tx.GetMsgs())
if err != nil {
return ctx, err
}

return next(ctx, tx, simulate)
}

// Check if a tx's messages exceed a validator's max change rate
func (mcr MsgChangeRateDecorator) hasInvalidCommissionRateMsgs(ctx sdk.Context, msgs []sdk.Msg) error {
for _, msg := range msgs {

// Check if an authz message, loop through all inner messages, and recursively call this function
if execMsg, ok := msg.(*authz.MsgExec); ok {

msgs, err := execMsg.GetMessages()
if err != nil {
return err
}

// Recursively call this function with the inner messages
err = mcr.hasInvalidCommissionRateMsgs(ctx, msgs)
if err != nil {
return err
}
}

// Check for create validator messages
if msg, ok := msg.(*stakingtypes.MsgCreateValidator); ok && mcr.isInvalidCreateMessage(msg) {
return fmt.Errorf("max change rate must not exceed %f%%", mcr.maxCommissionChangeRate)
}

// Check for edit validator messages
if msg, ok := msg.(*stakingtypes.MsgEditValidator); ok {
err := mcr.isInvalidEditMessage(ctx, msg)
if err != nil {
return err
}
}
}

return nil
}

// Check if the create validator message is invalid
func (mcr MsgChangeRateDecorator) isInvalidCreateMessage(msg *stakingtypes.MsgCreateValidator) bool {
return msg.Commission.MaxChangeRate.GT(mcr.maxCommissionChangeRate)
}

// Check if the edit validator message is invalid
func (mcr MsgChangeRateDecorator) isInvalidEditMessage(ctx sdk.Context, msg *stakingtypes.MsgEditValidator) error {
// Skip if the commission rate is not being modified
if msg.CommissionRate == nil {
return nil
}

bech32Addr, err := sdk.ValAddressFromBech32(msg.ValidatorAddress)
if err != nil {
return fmt.Errorf("invalid validator address")
}

// Get validator info, if exists
valInfo, found := mcr.sk.GetValidator(ctx, bech32Addr)
if !found {
return fmt.Errorf("validator not found")
}

// Check if new commission rate is out of bounds of the max change rate
if msg.CommissionRate.LT(valInfo.Commission.Rate.Sub(mcr.maxCommissionChangeRate)) || msg.CommissionRate.GT(valInfo.Commission.Rate.Add(mcr.maxCommissionChangeRate)) {
return fmt.Errorf("commission rate cannot change by more than %f%%", mcr.maxCommissionChangeRate)
}

return nil
}
237 changes: 237 additions & 0 deletions app/decorators/change_rate_decorator_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
package decorators_test

import (
"fmt"
"testing"
"time"

"github.com/stretchr/testify/suite"
protov2 "google.golang.org/protobuf/proto"

tmproto "github.com/cometbft/cometbft/proto/tendermint/types"

"cosmossdk.io/math"

"github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1"
cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/authz"
stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"

"github.com/CosmosContracts/juno/v19/app"
decorators "github.com/CosmosContracts/juno/v19/app/decorators"
appparams "github.com/CosmosContracts/juno/v19/app/params"
)

// Define an empty ante handle
var (
EmptyAnte = func(ctx sdk.Context, tx sdk.Tx, simulate bool) (sdk.Context, error) {
return ctx, nil
}
)

type AnteTestSuite struct {
suite.Suite

ctx sdk.Context
app *app.App
stakingKeeper *stakingkeeper.Keeper
}

func (s *AnteTestSuite) SetupTest() {
isCheckTx := false
s.app = app.Setup(s.T())

s.ctx = s.app.BaseApp.NewContext(isCheckTx, tmproto.Header{
ChainID: "testing",
Height: 10,
Time: time.Now().UTC(),
})

s.stakingKeeper = s.app.AppKeepers.StakingKeeper
}

func TestAnteTestSuite(t *testing.T) {
suite.Run(t, new(AnteTestSuite))
}

// Test the change rate decorator with standard create msgs,
// authz create messages, and inline authz create messages
func (s *AnteTestSuite) TestAnteCreateValidator() {
// Grantee used for authz msgs
grantee := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address())

// Loop through all possible change rates
for i := 0; i <= 100; i++ {

// Calculate change rate
maxChangeRate := getChangeRate(i)

// Create change rate decorator
ante := decorators.NewChangeRateDecorator(s.stakingKeeper)

// Create validator params
_, msg, err := createValidatorMsg(maxChangeRate)
s.Require().NoError(err)

// Submit the creation tx
_, err = ante.AnteHandle(s.ctx, NewMockTx(msg), false, EmptyAnte)
validateCreateMsg(s, err, i)

// Submit the creation tx with authz
authzMsg := authz.NewMsgExec(grantee, []sdk.Msg{msg})
_, err = ante.AnteHandle(s.ctx, NewMockTx(&authzMsg), false, EmptyAnte)
validateCreateMsg(s, err, i)

// Submit the creation tx with inline authz
inlineAuthzMsg := authz.NewMsgExec(grantee, []sdk.Msg{&authzMsg})
_, err = ante.AnteHandle(s.ctx, NewMockTx(&inlineAuthzMsg), false, EmptyAnte)
validateCreateMsg(s, err, i)
}
}

// Test the change rate decorator with standard edit msgs,
// authz edit messages, and inline authz edit messages
func (s *AnteTestSuite) TestAnteEditValidator() {
// Grantee used for authz msgs
grantee := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address())

// Loop through all possible change rates
for i := 0; i <= 100; i++ {

// Calculate change rate
maxChangeRate := getChangeRate(i)

// Create change rate decorator
ante := decorators.NewChangeRateDecorator(s.stakingKeeper)

// Create validator
valPub, createMsg, err := createValidatorMsg("0.05")
s.Require().NoError(err)

// Submit the creation tx
_, err = ante.AnteHandle(s.ctx, NewMockTx(createMsg), false, EmptyAnte)
s.Require().NoError(err)

// Create the validator
val, err := stakingtypes.NewValidator(
sdk.ValAddress(valPub.Address()),
valPub,
createMsg.Description,
)
s.Require().NoError(err)

// Set the validator
s.stakingKeeper.SetValidator(s.ctx, val)
s.Require().NoError(err)

// Edit validator params
valAddr := sdk.ValAddress(valPub.Address())
newRate := math.LegacyMustNewDecFromStr(maxChangeRate)
minDelegation := sdk.OneInt()

// Edit existing validator msg
editMsg := stakingtypes.NewMsgEditValidator(
valAddr,
createMsg.Description,
&newRate,
&minDelegation,
)

// Submit the edit tx
_, err = ante.AnteHandle(s.ctx, NewMockTx(editMsg), false, EmptyAnte)
validateEditMsg(s, err, i)

// Submit the edit tx with authz
authzMsg := authz.NewMsgExec(grantee, []sdk.Msg{editMsg})
_, err = ante.AnteHandle(s.ctx, NewMockTx(&authzMsg), false, EmptyAnte)
validateEditMsg(s, err, i)

// Submit the edit tx with inline authz
inlineAuthzMsg := authz.NewMsgExec(grantee, []sdk.Msg{&authzMsg})
_, err = ante.AnteHandle(s.ctx, NewMockTx(&inlineAuthzMsg), false, EmptyAnte)
validateEditMsg(s, err, i)
}
}

// Convert an integer to a percentage, formatted as a string
// Example: 5 -> "0.05", 10 -> "0.1"
func getChangeRate(i int) string {
if i >= 100 {
return "1.00"
}

return fmt.Sprintf("0.%02d", i)
}

// A helper function for getting a validator create msg
func createValidatorMsg(maxChangeRate string) (cryptotypes.PubKey, *stakingtypes.MsgCreateValidator, error) {
// Create validator params
valPub := secp256k1.GenPrivKey().PubKey()
valAddr := sdk.ValAddress(valPub.Address())
bondDenom := appparams.BondDenom
selfBond := sdk.NewCoins(sdk.Coin{Amount: sdk.NewInt(100), Denom: bondDenom})
stakingCoin := sdk.NewCoin(bondDenom, selfBond[0].Amount)
description := stakingtypes.NewDescription("test_moniker", "", "", "", "")
commission := stakingtypes.NewCommissionRates(
math.LegacyMustNewDecFromStr("0.1"),
math.LegacyMustNewDecFromStr("1"),
math.LegacyMustNewDecFromStr(maxChangeRate),
)

// Creating a Validator
msg, err := stakingtypes.NewMsgCreateValidator(
valAddr,
valPub,
stakingCoin,
description,
commission,
sdk.OneInt(),
)

// Return generated pub address, creation msg, and err
return valPub, msg, err
}

// Validate the create msg err is expected
func validateCreateMsg(s *AnteTestSuite, err error, i int) {
if i <= 5 {
s.Require().NoError(err)
} else {
s.Require().Error(err)
s.Require().Contains(err.Error(), "max change rate must not exceed")
}
}

// Validate the edit msg err is expected
func validateEditMsg(s *AnteTestSuite, err error, i int) {
if i <= 5 {
s.Require().NoError(err)
} else {
s.Require().Error(err)
s.Require().Contains(err.Error(), "commission rate cannot change by more than")
}
}

type MockTx struct {
msgs []sdk.Msg
}

func NewMockTx(msgs ...sdk.Msg) MockTx {
return MockTx{
msgs: msgs,
}
}

func (tx MockTx) GetMsgs() []sdk.Msg {
return tx.msgs
}

func (tx MockTx) GetMsgsV2() ([]protov2.Message, error) {
return nil, nil
}

func (tx MockTx) ValidateBasic() error {
return nil
}
Loading
Loading