Skip to content

Commit

Permalink
fix!: add GovVoteDecorator (#2912)
Browse files Browse the repository at this point in the history
* add GovVoteDecorator

* add changelog entries

* fix linter

* add unit test

* fix linter

* Update ante/gov_vote_ante.go

Co-authored-by: MSalopek <matija.salopek994@gmail.com>

---------

Co-authored-by: MSalopek <matija.salopek994@gmail.com>
  • Loading branch information
mpoke and MSalopek authored Feb 1, 2024
1 parent 1e5cccc commit 650e9e1
Show file tree
Hide file tree
Showing 6 changed files with 247 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .changelog/unreleased/bug-fixes/2912-vote-spam.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- Add ante handler that only allows `MsgVote` messages from accounts with at least
1 atom staked. ([\#2912](https://github.com/cosmos/gaia/pull/2912))
2 changes: 2 additions & 0 deletions .changelog/unreleased/state-breaking/2912-vote-spam.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- Add ante handler that only allows `MsgVote` messages from accounts with at least
1 atom staked. ([\#2912](https://github.com/cosmos/gaia/pull/2912))
1 change: 1 addition & 0 deletions ante/ante.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ func NewAnteHandler(opts HandlerOptions) (sdk.AnteHandler, error) {
ante.NewTxTimeoutHeightDecorator(),
ante.NewValidateMemoDecorator(opts.AccountKeeper),
ante.NewConsumeGasForTxSizeDecorator(opts.AccountKeeper),
NewGovVoteDecorator(opts.Codec, opts.StakingKeeper),
gaiafeeante.NewFeeDecorator(opts.GlobalFeeSubspace, opts.StakingKeeper),
ante.NewDeductFeeDecorator(opts.AccountKeeper, opts.BankKeeper, opts.FeegrantKeeper, opts.TxFeeChecker),
ante.NewSetPubKeyDecorator(opts.AccountKeeper), // SetPubKeyDecorator must be called before all signature verification decorators
Expand Down
116 changes: 116 additions & 0 deletions ante/gov_vote_ante.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package ante

import (
errorsmod "cosmossdk.io/errors"

"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/authz"
govv1beta1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1"
stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"

gaiaerrors "github.com/cosmos/gaia/v15/types/errors"
)

var (
minStakedTokens = sdk.NewDec(1000000) // 1_000_000 uatom (or 1 atom)
maxDelegationsChecked = 100 // number of delegation to check for the minStakedTokens
)

type GovVoteDecorator struct {
stakingKeeper *stakingkeeper.Keeper
cdc codec.BinaryCodec
}

func NewGovVoteDecorator(cdc codec.BinaryCodec, stakingKeeper *stakingkeeper.Keeper) GovVoteDecorator {
return GovVoteDecorator{
stakingKeeper: stakingKeeper,
cdc: cdc,
}
}

func (g GovVoteDecorator) AnteHandle(
ctx sdk.Context, tx sdk.Tx,
simulate bool, next sdk.AnteHandler,
) (newCtx sdk.Context, err error) {
// do not run check during simulations
if simulate {
return next(ctx, tx, simulate)
}

msgs := tx.GetMsgs()
if err = g.ValidateVoteMsgs(ctx, msgs); err != nil {
return ctx, err
}

return next(ctx, tx, simulate)
}

// ValidateVoteMsgs checks if a voter has enough stake to vote
func (g GovVoteDecorator) ValidateVoteMsgs(ctx sdk.Context, msgs []sdk.Msg) error {
validMsg := func(m sdk.Msg) error {
if msg, ok := m.(*govv1beta1.MsgVote); ok {
accAddr, err := sdk.AccAddressFromBech32(msg.Voter)
if err != nil {
return err
}
enoughStake := false
delegationCount := 0
stakedTokens := sdk.NewDec(0)
g.stakingKeeper.IterateDelegatorDelegations(ctx, accAddr, func(delegation stakingtypes.Delegation) bool {
validatorAddr, err := sdk.ValAddressFromBech32(delegation.ValidatorAddress)
if err != nil {
panic(err) // shouldn't happen
}
validator, found := g.stakingKeeper.GetValidator(ctx, validatorAddr)
if found {
shares := delegation.Shares
tokens := validator.TokensFromSharesTruncated(shares)
stakedTokens = stakedTokens.Add(tokens)
if stakedTokens.GTE(minStakedTokens) {
enoughStake = true
return true // break the iteration
}
}
delegationCount++
// break the iteration if maxDelegationsChecked were already checked
return delegationCount >= maxDelegationsChecked
})
if !enoughStake {
return errorsmod.Wrapf(gaiaerrors.ErrInsufficientStake, "insufficient stake for voting - min required %v", minStakedTokens)
}
}

return nil
}

validAuthz := func(execMsg *authz.MsgExec) error {
for _, v := range execMsg.Msgs {
var innerMsg sdk.Msg
if err := g.cdc.UnpackAny(v, &innerMsg); err != nil {
return errorsmod.Wrap(gaiaerrors.ErrUnauthorized, "cannot unmarshal authz exec msgs")
}
if err := validMsg(innerMsg); err != nil {
return err
}
}

return nil
}

for _, m := range msgs {
if msg, ok := m.(*authz.MsgExec); ok {
if err := validAuthz(msg); err != nil {
return err
}
continue
}

// validate normal msgs
if err := validMsg(m); err != nil {
return err
}
}
return nil
}
123 changes: 123 additions & 0 deletions ante/gov_vote_ante_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package ante_test

import (
"testing"

"github.com/stretchr/testify/require"

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

"cosmossdk.io/math"

"github.com/cosmos/cosmos-sdk/crypto/keys/ed25519"
sdk "github.com/cosmos/cosmos-sdk/types"
govv1beta1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"

"github.com/cosmos/gaia/v15/ante"
"github.com/cosmos/gaia/v15/app/helpers"
)

func TestVoteSpamDecorator(t *testing.T) {
gaiaApp := helpers.Setup(t)
ctx := gaiaApp.NewUncachedContext(true, tmproto.Header{})
decorator := ante.NewGovVoteDecorator(gaiaApp.AppCodec(), gaiaApp.StakingKeeper)
stakingKeeper := gaiaApp.StakingKeeper

// Get validator
valAddr1 := stakingKeeper.GetAllValidators(ctx)[0].GetOperator()

// Create one more validator
pk := ed25519.GenPrivKeyFromSecret([]byte{uint8(13)}).PubKey()
validator2, err := stakingtypes.NewValidator(
sdk.ValAddress(pk.Address()),
pk,
stakingtypes.Description{},
)
valAddr2 := validator2.GetOperator()
require.NoError(t, err)
// Make sure the validator is bonded so it's not removed on Undelegate
validator2.Status = stakingtypes.Bonded
stakingKeeper.SetValidator(ctx, validator2)
err = stakingKeeper.SetValidatorByConsAddr(ctx, validator2)
require.NoError(t, err)
stakingKeeper.SetNewValidatorByPowerIndex(ctx, validator2)
err = stakingKeeper.Hooks().AfterValidatorCreated(ctx, validator2.GetOperator())
require.NoError(t, err)

// Get delegator (this account was created during setup)
addr := gaiaApp.AccountKeeper.GetAccountAddressByID(ctx, 0)
delegator, err := sdk.AccAddressFromBech32(addr)
require.NoError(t, err)

tests := []struct {
name string
bondAmt math.Int
validators []sdk.ValAddress
expectPass bool
}{
{
name: "delegate 0.1 atom",
bondAmt: sdk.NewInt(100000),
validators: []sdk.ValAddress{valAddr1},
expectPass: false,
},
{
name: "delegate 1 atom",
bondAmt: sdk.NewInt(1000000),
validators: []sdk.ValAddress{valAddr1},
expectPass: true,
},
{
name: "delegate 1 atom to two validators",
bondAmt: sdk.NewInt(1000000),
validators: []sdk.ValAddress{valAddr1, valAddr2},
expectPass: true,
},
{
name: "delegate 0.9 atom to two validators",
bondAmt: sdk.NewInt(900000),
validators: []sdk.ValAddress{valAddr1, valAddr2},
expectPass: false,
},
{
name: "delegate 10 atom",
bondAmt: sdk.NewInt(10000000),
validators: []sdk.ValAddress{valAddr1},
expectPass: true,
},
}

for _, tc := range tests {
// Unbond all tokens for this delegator
delegations := stakingKeeper.GetAllDelegatorDelegations(ctx, delegator)
for _, del := range delegations {
_, err := stakingKeeper.Undelegate(ctx, delegator, del.GetValidatorAddr(), del.GetShares())
require.NoError(t, err)
}

// Delegate tokens
amt := tc.bondAmt.Quo(sdk.NewInt(int64(len(tc.validators))))
for _, valAddr := range tc.validators {
val, found := stakingKeeper.GetValidator(ctx, valAddr)
require.True(t, found)
_, err := stakingKeeper.Delegate(ctx, delegator, amt, stakingtypes.Unbonded, val, true)
require.NoError(t, err)
}

// Create vote message
msg := govv1beta1.NewMsgVote(
delegator,
0,
govv1beta1.OptionYes,
)

// Validate vote message
err := decorator.ValidateVoteMsgs(ctx, []sdk.Msg{msg})
if tc.expectPass {
require.NoError(t, err, "expected %v to pass", tc.name)
} else {
require.Error(t, err, "expected %v to fail", tc.name)
}
}
}
3 changes: 3 additions & 0 deletions types/errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,7 @@ var (

// ErrNotFound defines an error when requested entity doesn't exist in the state.
ErrNotFound = errorsmod.Register(codespace, 8, "not found")

// ErrInsufficientStake is used when the account has insufficient staked tokens.
ErrInsufficientStake = errorsmod.Register(codespace, 9, "insufficient stake")
)

0 comments on commit 650e9e1

Please sign in to comment.