Skip to content

Commit 650e9e1

Browse files
mpokeMSalopek
andauthored
fix!: add GovVoteDecorator (#2912)
* add GovVoteDecorator * add changelog entries * fix linter * add unit test * fix linter * Update ante/gov_vote_ante.go Co-authored-by: MSalopek <[email protected]> --------- Co-authored-by: MSalopek <[email protected]>
1 parent 1e5cccc commit 650e9e1

File tree

6 files changed

+247
-0
lines changed

6 files changed

+247
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
- Add ante handler that only allows `MsgVote` messages from accounts with at least
2+
1 atom staked. ([\#2912](https://github.com/cosmos/gaia/pull/2912))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
- Add ante handler that only allows `MsgVote` messages from accounts with at least
2+
1 atom staked. ([\#2912](https://github.com/cosmos/gaia/pull/2912))

ante/ante.go

+1
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ func NewAnteHandler(opts HandlerOptions) (sdk.AnteHandler, error) {
6161
ante.NewTxTimeoutHeightDecorator(),
6262
ante.NewValidateMemoDecorator(opts.AccountKeeper),
6363
ante.NewConsumeGasForTxSizeDecorator(opts.AccountKeeper),
64+
NewGovVoteDecorator(opts.Codec, opts.StakingKeeper),
6465
gaiafeeante.NewFeeDecorator(opts.GlobalFeeSubspace, opts.StakingKeeper),
6566
ante.NewDeductFeeDecorator(opts.AccountKeeper, opts.BankKeeper, opts.FeegrantKeeper, opts.TxFeeChecker),
6667
ante.NewSetPubKeyDecorator(opts.AccountKeeper), // SetPubKeyDecorator must be called before all signature verification decorators

ante/gov_vote_ante.go

+116
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
package ante
2+
3+
import (
4+
errorsmod "cosmossdk.io/errors"
5+
6+
"github.com/cosmos/cosmos-sdk/codec"
7+
sdk "github.com/cosmos/cosmos-sdk/types"
8+
"github.com/cosmos/cosmos-sdk/x/authz"
9+
govv1beta1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1"
10+
stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper"
11+
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
12+
13+
gaiaerrors "github.com/cosmos/gaia/v15/types/errors"
14+
)
15+
16+
var (
17+
minStakedTokens = sdk.NewDec(1000000) // 1_000_000 uatom (or 1 atom)
18+
maxDelegationsChecked = 100 // number of delegation to check for the minStakedTokens
19+
)
20+
21+
type GovVoteDecorator struct {
22+
stakingKeeper *stakingkeeper.Keeper
23+
cdc codec.BinaryCodec
24+
}
25+
26+
func NewGovVoteDecorator(cdc codec.BinaryCodec, stakingKeeper *stakingkeeper.Keeper) GovVoteDecorator {
27+
return GovVoteDecorator{
28+
stakingKeeper: stakingKeeper,
29+
cdc: cdc,
30+
}
31+
}
32+
33+
func (g GovVoteDecorator) AnteHandle(
34+
ctx sdk.Context, tx sdk.Tx,
35+
simulate bool, next sdk.AnteHandler,
36+
) (newCtx sdk.Context, err error) {
37+
// do not run check during simulations
38+
if simulate {
39+
return next(ctx, tx, simulate)
40+
}
41+
42+
msgs := tx.GetMsgs()
43+
if err = g.ValidateVoteMsgs(ctx, msgs); err != nil {
44+
return ctx, err
45+
}
46+
47+
return next(ctx, tx, simulate)
48+
}
49+
50+
// ValidateVoteMsgs checks if a voter has enough stake to vote
51+
func (g GovVoteDecorator) ValidateVoteMsgs(ctx sdk.Context, msgs []sdk.Msg) error {
52+
validMsg := func(m sdk.Msg) error {
53+
if msg, ok := m.(*govv1beta1.MsgVote); ok {
54+
accAddr, err := sdk.AccAddressFromBech32(msg.Voter)
55+
if err != nil {
56+
return err
57+
}
58+
enoughStake := false
59+
delegationCount := 0
60+
stakedTokens := sdk.NewDec(0)
61+
g.stakingKeeper.IterateDelegatorDelegations(ctx, accAddr, func(delegation stakingtypes.Delegation) bool {
62+
validatorAddr, err := sdk.ValAddressFromBech32(delegation.ValidatorAddress)
63+
if err != nil {
64+
panic(err) // shouldn't happen
65+
}
66+
validator, found := g.stakingKeeper.GetValidator(ctx, validatorAddr)
67+
if found {
68+
shares := delegation.Shares
69+
tokens := validator.TokensFromSharesTruncated(shares)
70+
stakedTokens = stakedTokens.Add(tokens)
71+
if stakedTokens.GTE(minStakedTokens) {
72+
enoughStake = true
73+
return true // break the iteration
74+
}
75+
}
76+
delegationCount++
77+
// break the iteration if maxDelegationsChecked were already checked
78+
return delegationCount >= maxDelegationsChecked
79+
})
80+
if !enoughStake {
81+
return errorsmod.Wrapf(gaiaerrors.ErrInsufficientStake, "insufficient stake for voting - min required %v", minStakedTokens)
82+
}
83+
}
84+
85+
return nil
86+
}
87+
88+
validAuthz := func(execMsg *authz.MsgExec) error {
89+
for _, v := range execMsg.Msgs {
90+
var innerMsg sdk.Msg
91+
if err := g.cdc.UnpackAny(v, &innerMsg); err != nil {
92+
return errorsmod.Wrap(gaiaerrors.ErrUnauthorized, "cannot unmarshal authz exec msgs")
93+
}
94+
if err := validMsg(innerMsg); err != nil {
95+
return err
96+
}
97+
}
98+
99+
return nil
100+
}
101+
102+
for _, m := range msgs {
103+
if msg, ok := m.(*authz.MsgExec); ok {
104+
if err := validAuthz(msg); err != nil {
105+
return err
106+
}
107+
continue
108+
}
109+
110+
// validate normal msgs
111+
if err := validMsg(m); err != nil {
112+
return err
113+
}
114+
}
115+
return nil
116+
}

ante/gov_vote_ante_test.go

+123
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package ante_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
8+
tmproto "github.com/cometbft/cometbft/proto/tendermint/types"
9+
10+
"cosmossdk.io/math"
11+
12+
"github.com/cosmos/cosmos-sdk/crypto/keys/ed25519"
13+
sdk "github.com/cosmos/cosmos-sdk/types"
14+
govv1beta1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1"
15+
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
16+
17+
"github.com/cosmos/gaia/v15/ante"
18+
"github.com/cosmos/gaia/v15/app/helpers"
19+
)
20+
21+
func TestVoteSpamDecorator(t *testing.T) {
22+
gaiaApp := helpers.Setup(t)
23+
ctx := gaiaApp.NewUncachedContext(true, tmproto.Header{})
24+
decorator := ante.NewGovVoteDecorator(gaiaApp.AppCodec(), gaiaApp.StakingKeeper)
25+
stakingKeeper := gaiaApp.StakingKeeper
26+
27+
// Get validator
28+
valAddr1 := stakingKeeper.GetAllValidators(ctx)[0].GetOperator()
29+
30+
// Create one more validator
31+
pk := ed25519.GenPrivKeyFromSecret([]byte{uint8(13)}).PubKey()
32+
validator2, err := stakingtypes.NewValidator(
33+
sdk.ValAddress(pk.Address()),
34+
pk,
35+
stakingtypes.Description{},
36+
)
37+
valAddr2 := validator2.GetOperator()
38+
require.NoError(t, err)
39+
// Make sure the validator is bonded so it's not removed on Undelegate
40+
validator2.Status = stakingtypes.Bonded
41+
stakingKeeper.SetValidator(ctx, validator2)
42+
err = stakingKeeper.SetValidatorByConsAddr(ctx, validator2)
43+
require.NoError(t, err)
44+
stakingKeeper.SetNewValidatorByPowerIndex(ctx, validator2)
45+
err = stakingKeeper.Hooks().AfterValidatorCreated(ctx, validator2.GetOperator())
46+
require.NoError(t, err)
47+
48+
// Get delegator (this account was created during setup)
49+
addr := gaiaApp.AccountKeeper.GetAccountAddressByID(ctx, 0)
50+
delegator, err := sdk.AccAddressFromBech32(addr)
51+
require.NoError(t, err)
52+
53+
tests := []struct {
54+
name string
55+
bondAmt math.Int
56+
validators []sdk.ValAddress
57+
expectPass bool
58+
}{
59+
{
60+
name: "delegate 0.1 atom",
61+
bondAmt: sdk.NewInt(100000),
62+
validators: []sdk.ValAddress{valAddr1},
63+
expectPass: false,
64+
},
65+
{
66+
name: "delegate 1 atom",
67+
bondAmt: sdk.NewInt(1000000),
68+
validators: []sdk.ValAddress{valAddr1},
69+
expectPass: true,
70+
},
71+
{
72+
name: "delegate 1 atom to two validators",
73+
bondAmt: sdk.NewInt(1000000),
74+
validators: []sdk.ValAddress{valAddr1, valAddr2},
75+
expectPass: true,
76+
},
77+
{
78+
name: "delegate 0.9 atom to two validators",
79+
bondAmt: sdk.NewInt(900000),
80+
validators: []sdk.ValAddress{valAddr1, valAddr2},
81+
expectPass: false,
82+
},
83+
{
84+
name: "delegate 10 atom",
85+
bondAmt: sdk.NewInt(10000000),
86+
validators: []sdk.ValAddress{valAddr1},
87+
expectPass: true,
88+
},
89+
}
90+
91+
for _, tc := range tests {
92+
// Unbond all tokens for this delegator
93+
delegations := stakingKeeper.GetAllDelegatorDelegations(ctx, delegator)
94+
for _, del := range delegations {
95+
_, err := stakingKeeper.Undelegate(ctx, delegator, del.GetValidatorAddr(), del.GetShares())
96+
require.NoError(t, err)
97+
}
98+
99+
// Delegate tokens
100+
amt := tc.bondAmt.Quo(sdk.NewInt(int64(len(tc.validators))))
101+
for _, valAddr := range tc.validators {
102+
val, found := stakingKeeper.GetValidator(ctx, valAddr)
103+
require.True(t, found)
104+
_, err := stakingKeeper.Delegate(ctx, delegator, amt, stakingtypes.Unbonded, val, true)
105+
require.NoError(t, err)
106+
}
107+
108+
// Create vote message
109+
msg := govv1beta1.NewMsgVote(
110+
delegator,
111+
0,
112+
govv1beta1.OptionYes,
113+
)
114+
115+
// Validate vote message
116+
err := decorator.ValidateVoteMsgs(ctx, []sdk.Msg{msg})
117+
if tc.expectPass {
118+
require.NoError(t, err, "expected %v to pass", tc.name)
119+
} else {
120+
require.Error(t, err, "expected %v to fail", tc.name)
121+
}
122+
}
123+
}

types/errors/errors.go

+3
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,7 @@ var (
3131

3232
// ErrNotFound defines an error when requested entity doesn't exist in the state.
3333
ErrNotFound = errorsmod.Register(codespace, 8, "not found")
34+
35+
// ErrInsufficientStake is used when the account has insufficient staked tokens.
36+
ErrInsufficientStake = errorsmod.Register(codespace, 9, "insufficient stake")
3437
)

0 commit comments

Comments
 (0)