Skip to content

Commit

Permalink
fix!: check v1 and v1beta1 MsgVote in VoteSpamDecorator (#2923)
Browse files Browse the repository at this point in the history
* fix!: check v1 and v1beta1 MsgVote in VoteSpamDecorator

#2912 introuced the VoteSpamDecorator.

The implementation was not complete and only v1beta1 gov types were handled.

The CLI supports v1 gov types so the validation was not sufficient.

This commit expands the antehandler validation to allow correct processing of both v1 and v1beta1 messages.

* appease linter

* test: stop app from failing non-determinism test on gov vote

* ante: handle edgecase with 0 minStakedTokens

* appease linter
  • Loading branch information
MSalopek authored Feb 6, 2024
1 parent 382301c commit 52a0f44
Show file tree
Hide file tree
Showing 3 changed files with 190 additions and 32 deletions.
75 changes: 50 additions & 25 deletions ante/gov_vote_ante.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/authz"
govv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1"
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"
Expand All @@ -18,6 +19,12 @@ var (
maxDelegationsChecked = 100 // number of delegation to check for the minStakedTokens
)

// SetMinStakedTokens sets the minimum amount of staked tokens required to vote
// Should only be used in testing
func SetMinStakedTokens(tokens sdk.Dec) {
minStakedTokens = tokens
}

type GovVoteDecorator struct {
stakingKeeper *stakingkeeper.Keeper
cdc codec.BinaryCodec
Expand Down Expand Up @@ -50,36 +57,54 @@ func (g GovVoteDecorator) AnteHandle(
// 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)
var accAddr sdk.AccAddress
var err error

switch msg := m.(type) {
case *govv1beta1.MsgVote:
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
}
case *govv1.MsgVote:
accAddr, err = sdk.AccAddressFromBech32(msg.Voter)
if err != nil {
return err
}
default:
// not a vote message - nothing to validate
return nil
}

if minStakedTokens.IsZero() {
return nil
}

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)
}
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
Expand Down
140 changes: 133 additions & 7 deletions ante/gov_vote_ante_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,17 @@ import (

"github.com/cosmos/cosmos-sdk/crypto/keys/ed25519"
sdk "github.com/cosmos/cosmos-sdk/types"
govv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1"
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) {
// Test that the GovVoteDecorator rejects v1beta1 vote messages from accounts with less than 1 atom staked
// Submitting v1beta1.VoteMsg should not be possible through the CLI, but it's still possible to craft a transaction
func TestVoteSpamDecoratorGovV1Beta1(t *testing.T) {
gaiaApp := helpers.Setup(t)
ctx := gaiaApp.NewUncachedContext(true, tmproto.Header{})
decorator := ante.NewGovVoteDecorator(gaiaApp.AppCodec(), gaiaApp.StakingKeeper)
Expand Down Expand Up @@ -56,6 +59,12 @@ func TestVoteSpamDecorator(t *testing.T) {
validators []sdk.ValAddress
expectPass bool
}{
{
name: "delegate 0 atom",
bondAmt: sdk.ZeroInt(),
validators: []sdk.ValAddress{valAddr1},
expectPass: false,
},
{
name: "delegate 0.1 atom",
bondAmt: sdk.NewInt(100000),
Expand Down Expand Up @@ -97,12 +106,14 @@ func TestVoteSpamDecorator(t *testing.T) {
}

// 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)
if !tc.bondAmt.IsZero() {
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
Expand All @@ -121,3 +132,118 @@ func TestVoteSpamDecorator(t *testing.T) {
}
}
}

// Test that the GovVoteDecorator rejects v1 vote messages from accounts with less than 1 atom staked
// Usually, only v1.VoteMsg can be submitted using the CLI.
func TestVoteSpamDecoratorGovV1(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 atom",
bondAmt: sdk.ZeroInt(),
validators: []sdk.ValAddress{valAddr1},
expectPass: false,
},
{
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
if !tc.bondAmt.IsZero() {
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 := govv1.NewMsgVote(
delegator,
0,
govv1.VoteOption_VOTE_OPTION_YES,
"new-v1-vote-message-test",
)

// 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)
}
}
}
7 changes: 7 additions & 0 deletions app/sim_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import (
dbm "github.com/cometbft/cometbft-db"
"github.com/cometbft/cometbft/libs/log"

"cosmossdk.io/math"

"github.com/cosmos/cosmos-sdk/baseapp"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/server"
Expand All @@ -21,6 +23,7 @@ import (
"github.com/cosmos/cosmos-sdk/x/simulation"
simcli "github.com/cosmos/cosmos-sdk/x/simulation/client/cli"

"github.com/cosmos/gaia/v15/ante"
gaia "github.com/cosmos/gaia/v15/app"
// "github.com/cosmos/gaia/v11/app/helpers"
// "github.com/cosmos/gaia/v11/app/params"
Expand Down Expand Up @@ -97,6 +100,10 @@ func TestAppStateDeterminism(t *testing.T) {
baseapp.SetChainID(AppChainID),
)

// NOTE: setting to zero to avoid failing the simulation
// due to the minimum staked tokens required to submit a vote
ante.SetMinStakedTokens(math.LegacyZeroDec())

fmt.Printf(
"running non-determinism simulation; seed %d: %d/%d, attempt: %d/%d\n",
config.Seed, i+1, numSeeds, j+1, numTimesToRunPerSeed,
Expand Down

0 comments on commit 52a0f44

Please sign in to comment.