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

fix!: add GovVoteDecorator #2912

Merged
merged 7 commits into from
Feb 1, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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/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 @@ -67,6 +67,7 @@ func NewAnteHandler(opts HandlerOptions) (sdk.AnteHandler, error) {
ante.NewValidateMemoDecorator(opts.AccountKeeper),
ante.NewConsumeGasForTxSizeDecorator(opts.AccountKeeper),
NewGovPreventSpamDecorator(opts.Codec, opts.GovKeeper),
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) // 1000.000 uatom (or 1 atom)
mpoke marked this conversation as resolved.
Show resolved Hide resolved
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")
)
Loading