Skip to content

Commit eddb2f3

Browse files
authored
Merge pull request cosmos#37 from 0xPolygon/pos-2755
Implement anti-spam mechanism for proposals in gov
2 parents f6e5561 + 1ef2089 commit eddb2f3

File tree

6 files changed

+271
-14
lines changed

6 files changed

+271
-14
lines changed

x/gov/abci.go

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ func EndBlocker(ctx sdk.Context, keeper *keeper.Keeper) error {
4646
return false, err
4747
}
4848

49-
// TODO HV2: https://polygon.atlassian.net/browse/POS-2755
50-
err = keeper.RefundAndDeleteDeposits(ctx, proposal.Id) // refund deposit if proposal got removed without getting 100% of the proposal
49+
// Distribute the deposits if the proposal got removed without getting 100% of the proposal
50+
err = keeper.DistributeAndDeleteDeposits(ctx, proposal.Id)
5151
if err != nil {
5252
return false, err
5353
}
@@ -135,10 +135,17 @@ func EndBlocker(ctx sdk.Context, keeper *keeper.Keeper) error {
135135
return false, err
136136
}
137137

138-
// HV2: heimdall refunds and deletes deposits in all cases of proposal failures, without caring about burnDeposits
139-
err = keeper.RefundAndDeleteDeposits(ctx, proposal.Id)
140-
if err != nil {
141-
return false, err
138+
// HV2: heimdall distributes and deletes deposits in all cases of proposal failures, without caring about burnDeposits
139+
if passes {
140+
err = keeper.RefundAndDeleteDeposits(ctx, proposal.Id)
141+
if err != nil {
142+
return false, err
143+
}
144+
} else {
145+
err = keeper.DistributeAndDeleteDeposits(ctx, proposal.Id)
146+
if err != nil {
147+
return false, err
148+
}
142149
}
143150

144151
// If an expedited proposal fails, we do not want to update
@@ -315,7 +322,7 @@ func failUnsupportedProposal(
315322
return err
316323
}
317324

318-
if err := keeper.RefundAndDeleteDeposits(ctx, proposal.Id); err != nil {
325+
if err := keeper.DistributeAndDeleteDeposits(ctx, proposal.Id); err != nil {
319326
return err
320327
}
321328

x/gov/keeper/common_test.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,15 @@ import (
55
"testing"
66

77
borTypes "github.com/0xPolygon/heimdall-v2/x/bor/types"
8+
chainmanagertypes "github.com/0xPolygon/heimdall-v2/x/chainmanager/types"
89
checkpointTypes "github.com/0xPolygon/heimdall-v2/x/checkpoint/types"
910
milestoneTypes "github.com/0xPolygon/heimdall-v2/x/milestone/types"
11+
stakingtypes "github.com/0xPolygon/heimdall-v2/x/stake/types"
1012
topupTypes "github.com/0xPolygon/heimdall-v2/x/topup/types"
1113

12-
chainmanagertypes "github.com/0xPolygon/heimdall-v2/x/chainmanager/types"
13-
consensustypes "github.com/cosmos/cosmos-sdk/x/consensus/types"
14-
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
15-
1614
cmtproto "github.com/cometbft/cometbft/proto/tendermint/types"
1715
cmttime "github.com/cometbft/cometbft/types/time"
16+
1817
"github.com/golang/mock/gomock"
1918
"github.com/stretchr/testify/require"
2019

@@ -30,6 +29,7 @@ import (
3029
moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil"
3130
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
3231
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
32+
consensustypes "github.com/cosmos/cosmos-sdk/x/consensus/types"
3333
disttypes "github.com/cosmos/cosmos-sdk/x/distribution/types"
3434
"github.com/cosmos/cosmos-sdk/x/gov/keeper"
3535
govtestutil "github.com/cosmos/cosmos-sdk/x/gov/testutil"
@@ -111,7 +111,6 @@ func setupGovKeeper(t *testing.T) (
111111
}).AnyTimes()
112112

113113
stakingKeeper.EXPECT().BondDenom(ctx).Return("pol", nil).AnyTimes()
114-
stakingKeeper.EXPECT().IterateCurrentValidatorsAndApplyFn(gomock.Any(), gomock.Any()).AnyTimes()
115114
stakingKeeper.EXPECT().TokensFromConsensusPower(gomock.Any(), gomock.Any()).AnyTimes()
116115
stakingKeeper.EXPECT().ValidatorAddressCodec().Return(address.NewHexCodec()).AnyTimes()
117116
distributionKeeper.EXPECT().FundCommunityPool(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()

x/gov/keeper/deposit.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package keeper
33
import (
44
"context"
55
"fmt"
6+
"math/rand"
67
"strings"
78

89
"cosmossdk.io/collections"
@@ -14,6 +15,8 @@ import (
1415
disttypes "github.com/cosmos/cosmos-sdk/x/distribution/types"
1516
"github.com/cosmos/cosmos-sdk/x/gov/types"
1617
v1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1"
18+
19+
stakeTypes "github.com/0xPolygon/heimdall-v2/x/stake/types"
1720
)
1821

1922
// SetDeposit sets a Deposit to the gov store
@@ -285,6 +288,94 @@ func (keeper Keeper) RefundAndDeleteDeposits(ctx context.Context, proposalID uin
285288
})
286289
}
287290

291+
// DistributeAndDeleteDeposits distributes the deposits evenly among all the active validators and deletes the deposits on a specific proposal.
292+
func (keeper Keeper) DistributeAndDeleteDeposits(ctx context.Context, proposalID uint64) error {
293+
var validatorAddresses []sdk.AccAddress
294+
295+
err := keeper.sk.IterateCurrentValidatorsAndApplyFn(ctx, func(validator stakeTypes.Validator) bool {
296+
validatorAddr, err := sdk.AccAddressFromHex(validator.Signer)
297+
if err != nil {
298+
keeper.Logger(ctx).Error("Failed to parse validator address from hex", "error", err)
299+
return true
300+
}
301+
validatorAddresses = append(validatorAddresses, validatorAddr)
302+
return false
303+
})
304+
if err != nil {
305+
keeper.Logger(ctx).Error("Error iterating over validators", "error", err)
306+
return err
307+
}
308+
309+
numValidators := len(validatorAddresses)
310+
311+
deposits, err := keeper.GetDeposits(ctx, proposalID)
312+
if err != nil {
313+
keeper.Logger(ctx).Error("Failed to retrieve deposits for proposal", "proposalID", proposalID, "error", err)
314+
return err
315+
}
316+
317+
var totalDeposits sdk.Coins
318+
var amountPerValidator sdk.Coins
319+
320+
for _, deposit := range deposits {
321+
depositorAddress, err := keeper.authKeeper.AddressCodec().StringToBytes(deposit.Depositor)
322+
if err != nil {
323+
keeper.Logger(ctx).Error("Failed to decode depositor address", "error", err)
324+
return err
325+
}
326+
327+
totalDeposits = totalDeposits.Add(deposit.Amount...)
328+
329+
for _, coin := range deposit.Amount {
330+
amountPerValidatorPerCoin := sdkmath.LegacyNewDecFromInt(coin.Amount).QuoInt64(int64(numValidators)).TruncateInt()
331+
amountPerValidator = amountPerValidator.Add(
332+
sdk.NewCoin(
333+
coin.Denom,
334+
amountPerValidatorPerCoin,
335+
),
336+
)
337+
}
338+
339+
err = keeper.Deposits.Remove(ctx, collections.Join(deposit.ProposalId, sdk.AccAddress(depositorAddress)))
340+
if err != nil {
341+
keeper.Logger(ctx).Error("Failed to remove deposit", "proposalID", proposalID, "depositor", depositorAddress, "error", err)
342+
return err
343+
}
344+
}
345+
346+
for _, validator := range validatorAddresses {
347+
err = keeper.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, validator, amountPerValidator)
348+
if err != nil {
349+
keeper.Logger(ctx).Error("Failed to send coins to validator", "error", err)
350+
return err
351+
}
352+
}
353+
354+
// Calculate any remaining amount due to truncating issues
355+
usedAmount := amountPerValidator.MulInt(sdkmath.NewInt(int64(numValidators)))
356+
remainingAmount, isAnyNegative := totalDeposits.SafeSub(usedAmount...)
357+
if isAnyNegative {
358+
keeper.Logger(ctx).Error("subtraction resulted in a negative amount", "totalDeposits", totalDeposits, "usedAmount", usedAmount)
359+
return fmt.Errorf("subtraction resulted in a negative amount")
360+
}
361+
362+
if !remainingAmount.IsZero() {
363+
// Send remaining amount to a random validator
364+
rand := rand.New(rand.NewSource(int64(numValidators)))
365+
randomValidator := validatorAddresses[rand.Intn(numValidators)]
366+
367+
err = keeper.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, randomValidator, remainingAmount)
368+
if err != nil {
369+
keeper.Logger(ctx).Error("Failed to send remaining coins to a random validator", "validator", randomValidator, "error", err)
370+
return err
371+
}
372+
}
373+
374+
keeper.Logger(ctx).Info("Successfully distributed and deleted deposits")
375+
376+
return err
377+
}
378+
288379
// validateInitialDeposit validates if initial deposit is greater than or equal to the minimum
289380
// required at the time of proposal submission. This threshold amount is determined by
290381
// the deposit parameters. Returns nil on success, error otherwise.

x/gov/keeper/deposit_test.go

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
package keeper_test
22

33
import (
4+
"context"
45
"fmt"
56
"math/big"
67
"testing"
78

9+
"github.com/golang/mock/gomock"
10+
811
"github.com/stretchr/testify/require"
912

1013
"cosmossdk.io/collections"
@@ -16,6 +19,8 @@ import (
1619
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
1720
disttypes "github.com/cosmos/cosmos-sdk/x/distribution/types"
1821
v1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1"
22+
23+
stakeTypes "github.com/0xPolygon/heimdall-v2/x/stake/types"
1924
)
2025

2126
const (
@@ -461,3 +466,141 @@ func TestChargeDeposit(t *testing.T) {
461466
}
462467
}
463468
}
469+
470+
func TestDistributeAndDeleteDeposits(t *testing.T) {
471+
testcases := []struct {
472+
name string
473+
numValidators uint64
474+
}{
475+
{
476+
name: "Single validator case",
477+
numValidators: 1,
478+
},
479+
{
480+
name: "Equal distribution case",
481+
numValidators: 2,
482+
},
483+
{
484+
name: "Edge case: Distribution of the remaining amount to a random validator",
485+
numValidators: 3,
486+
},
487+
}
488+
489+
for _, tc := range testcases {
490+
t.Run(tc.name, func(t *testing.T) {
491+
govKeeper, authKeeper, bankKeeper, stakingKeeper, distKeeper, _, ctx := setupGovKeeper(t)
492+
trackMockBalances(bankKeeper, distKeeper)
493+
494+
accAmt := sdkmath.NewIntFromBigInt(new(big.Int).Mul(big.NewInt(10), new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil)))
495+
TestAddrs := simtestutil.AddTestAddrsIncremental(bankKeeper, ctx, 5, accAmt.Mul(sdkmath.NewInt(1)))
496+
authKeeper.EXPECT().AddressCodec().Return(address.NewHexCodec()).AnyTimes()
497+
498+
var mockValidators []stakeTypes.Validator
499+
500+
switch {
501+
case tc.numValidators == 1:
502+
mockValidators = []stakeTypes.Validator{
503+
{Signer: TestAddrs[2].String()},
504+
}
505+
case tc.numValidators == 2:
506+
mockValidators = []stakeTypes.Validator{
507+
{Signer: TestAddrs[2].String()},
508+
{Signer: TestAddrs[3].String()},
509+
}
510+
case tc.numValidators == 3:
511+
mockValidators = []stakeTypes.Validator{
512+
{Signer: TestAddrs[2].String()},
513+
{Signer: TestAddrs[3].String()},
514+
{Signer: TestAddrs[4].String()},
515+
}
516+
}
517+
518+
stakingKeeper.EXPECT().IterateCurrentValidatorsAndApplyFn(gomock.Any(), gomock.Any()).DoAndReturn(
519+
func(ctx context.Context, fn func(stakeTypes.Validator) bool) error {
520+
for _, validator := range mockValidators {
521+
if stop := fn(validator); stop {
522+
break
523+
}
524+
}
525+
return nil
526+
},
527+
).AnyTimes()
528+
529+
tp := TestProposal
530+
proposal, err := govKeeper.SubmitProposal(ctx, tp, "", "title", "summary", TestAddrs[0], false)
531+
require.NoError(t, err)
532+
proposalID := proposal.Id
533+
534+
stakeAmount := sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, stakingKeeper.TokensFromConsensusPower(ctx, 5)))
535+
536+
addr0Initial := bankKeeper.GetAllBalances(ctx, TestAddrs[0])
537+
addr1Initial := bankKeeper.GetAllBalances(ctx, TestAddrs[1])
538+
539+
addr2Initial := bankKeeper.GetAllBalances(ctx, TestAddrs[2])
540+
addr3Initial := bankKeeper.GetAllBalances(ctx, TestAddrs[3])
541+
addr4Initial := bankKeeper.GetAllBalances(ctx, TestAddrs[4])
542+
543+
// 1st deposit from TestAddrs[0]
544+
_, err = govKeeper.AddDeposit(ctx, proposalID, TestAddrs[0], stakeAmount)
545+
require.NoError(t, err)
546+
547+
// 2nd deposit from TestAddrs[1]
548+
_, err = govKeeper.AddDeposit(ctx, proposalID, TestAddrs[1], stakeAmount)
549+
require.NoError(t, err)
550+
551+
// Check deposits length
552+
deposits, _ := govKeeper.GetDeposits(ctx, proposalID)
553+
require.Len(t, deposits, 2)
554+
555+
// Check TestAddrs[0] and TestAddrs[1] balances
556+
require.Equal(t, addr0Initial.Sub(stakeAmount...), bankKeeper.GetAllBalances(ctx, TestAddrs[0]))
557+
require.Equal(t, addr1Initial.Sub(stakeAmount...), bankKeeper.GetAllBalances(ctx, TestAddrs[1]))
558+
559+
addr0After := bankKeeper.GetAllBalances(ctx, TestAddrs[0])
560+
addr1After := bankKeeper.GetAllBalances(ctx, TestAddrs[1])
561+
562+
// 10 pol deposited in total from TestAddrs[0] and TestAddrs[1]
563+
564+
// Test DistributeAndDeleteDeposits
565+
err = govKeeper.DistributeAndDeleteDeposits(ctx, proposalID)
566+
require.NoError(t, err)
567+
568+
// Check balances
569+
570+
// Balances of TestAddrs[0] and TestAddrs[1] should be the same
571+
// as the deposits will be distributed to the validators
572+
require.Equal(t, addr0After, bankKeeper.GetAllBalances(ctx, TestAddrs[0]))
573+
require.Equal(t, addr1After, bankKeeper.GetAllBalances(ctx, TestAddrs[1]))
574+
575+
// Balances of validators will be dependent on numValidators
576+
switch {
577+
case tc.numValidators == 1:
578+
// All the deposits will be transferred to the single validator
579+
require.Equal(t, addr2Initial.Add(stakeAmount...).Add(stakeAmount...), bankKeeper.GetAllBalances(ctx, TestAddrs[2]))
580+
case tc.numValidators == 2:
581+
// Equal distribution of deposits among validators
582+
require.Equal(t, addr2Initial.Add(stakeAmount...), bankKeeper.GetAllBalances(ctx, TestAddrs[2]))
583+
require.Equal(t, addr3Initial.Add(stakeAmount...), bankKeeper.GetAllBalances(ctx, TestAddrs[3]))
584+
case tc.numValidators == 3:
585+
// A random validator will get a little bit of pol (NOT in the power 10**18) more because of truncation in division
586+
addr2After := bankKeeper.GetAllBalances(ctx, TestAddrs[2])
587+
addr3After := bankKeeper.GetAllBalances(ctx, TestAddrs[3])
588+
addr4After := bankKeeper.GetAllBalances(ctx, TestAddrs[4])
589+
590+
if addr2After.AmountOf("pol").GT(addr3After.AmountOf("pol")) && addr2After.AmountOf("pol").GT(addr4After.AmountOf("pol")) {
591+
require.Equal(t, addr2Initial.Add(sdk.NewCoin("pol", sdkmath.NewInt(3333333333333333336))), addr2After)
592+
require.Equal(t, addr3Initial.Add(sdk.NewCoin("pol", sdkmath.NewInt(3333333333333333332))), addr3After)
593+
require.Equal(t, addr4Initial.Add(sdk.NewCoin("pol", sdkmath.NewInt(3333333333333333332))), addr4After)
594+
} else if addr3After.AmountOf("pol").GT(addr2After.AmountOf("pol")) && addr3After.AmountOf("pol").GT(addr4After.AmountOf("pol")) {
595+
require.Equal(t, addr2Initial.Add(sdk.NewCoin("pol", sdkmath.NewInt(3333333333333333332))), addr2After)
596+
require.Equal(t, addr3Initial.Add(sdk.NewCoin("pol", sdkmath.NewInt(3333333333333333336))), addr3After)
597+
require.Equal(t, addr4Initial.Add(sdk.NewCoin("pol", sdkmath.NewInt(3333333333333333332))), addr4After)
598+
} else {
599+
require.Equal(t, addr2Initial.Add(sdk.NewCoin("pol", sdkmath.NewInt(3333333333333333332))), addr2After)
600+
require.Equal(t, addr3Initial.Add(sdk.NewCoin("pol", sdkmath.NewInt(3333333333333333332))), addr3After)
601+
require.Equal(t, addr4Initial.Add(sdk.NewCoin("pol", sdkmath.NewInt(3333333333333333336))), addr4After)
602+
}
603+
}
604+
})
605+
}
606+
}

x/gov/keeper/hooks_test.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import (
1616
"github.com/cosmos/cosmos-sdk/x/gov/keeper"
1717
"github.com/cosmos/cosmos-sdk/x/gov/types"
1818
v1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1"
19+
20+
stakeTypes "github.com/0xPolygon/heimdall-v2/x/stake/types"
1921
)
2022

2123
var _ types.GovHooks = &MockGovHooksReceiver{}
@@ -61,7 +63,22 @@ func TestHooks(t *testing.T) {
6163

6264
authKeeper.EXPECT().AddressCodec().Return(address.NewHexCodec()).AnyTimes()
6365
stakingKeeper.EXPECT().ValidatorAddressCodec().Return(address.NewHexCodec()).AnyTimes()
64-
stakingKeeper.EXPECT().IterateCurrentValidatorsAndApplyFn(gomock.Any(), gomock.Any()).AnyTimes()
66+
67+
mockValidators := []stakeTypes.Validator{
68+
{Signer: "0xb316fa9fa91700d7084d377bfdc81eb9f232f5ff"},
69+
{Signer: "0xb316fa9fa91700d7084d377bfdc81eb9f232f5fd"},
70+
}
71+
72+
stakingKeeper.EXPECT().IterateCurrentValidatorsAndApplyFn(gomock.Any(), gomock.Any()).DoAndReturn(
73+
func(ctx context.Context, fn func(stakeTypes.Validator) bool) error {
74+
for _, validator := range mockValidators {
75+
if stop := fn(validator); stop {
76+
break
77+
}
78+
}
79+
return nil
80+
},
81+
).AnyTimes()
6582

6683
govHooksReceiver := MockGovHooksReceiver{}
6784

x/gov/testutil/expected_keepers.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
package testutil
44

55
import (
6-
context "context"
6+
"context"
77

88
"cosmossdk.io/math"
99

0 commit comments

Comments
 (0)