Skip to content

Commit 8dcdde3

Browse files
tbruyellegiunatale
andauthored
feat(x/gov): burn deposit if no votes > BurnDepositNoThreshold (#90)
`BurnDepositNoThreshold` has a default value of `0.8`. --------- Co-authored-by: Giuseppe Natale <[email protected]>
1 parent cefa710 commit 8dcdde3

File tree

16 files changed

+489
-188
lines changed

16 files changed

+489
-188
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131

3232
- Add the photon module and use photon as the only fee denom [#57](https://github.com/atomone-hub/atomone/pull/57)
3333
- Make `x/gov` proposals deposits dynamic [#69](https://github.com/atomone-hub/atomone/pull/69)
34+
- Burn proposals deposit if percentage of no votes > `params.BurnDepositNoThreshold` when tallying [#90](https://github.com/atomone-hub/atomone/pull/90)
3435

3536
### DEPENDENCIES
3637

proto/atomone/gov/v1/gov.proto

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,4 +346,7 @@ message Params {
346346
MinDepositThrottler min_deposit_throttler = 23;
347347

348348
MinInitialDepositThrottler min_initial_deposit_throttler = 24;
349+
350+
// Minimum proportion of No Votes for a proposal deposit to be burnt.
351+
string burn_deposit_no_threshold = 25 [(cosmos_proto.scalar) = "cosmos.Dec"];
349352
}

tests/e2e/e2e_gov_test.go

Lines changed: 68 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,8 @@ func (s *IntegrationTestSuite) testGovCommunityPoolSpend() {
132132
sendAmount := sdk.NewInt64Coin(uatoneDenom, 10_000_000) // 10atone
133133
s.writeGovCommunitySpendProposal(s.chainA, sendAmount, recipient)
134134

135+
beforeSenderBalance, err := getSpecificBalance(chainAAPIEndpoint, sender, uatoneDenom)
136+
s.Require().NoError(err)
135137
beforeRecipientBalance, err := getSpecificBalance(chainAAPIEndpoint, recipient, uatoneDenom)
136138
s.Require().NoError(err)
137139

@@ -140,8 +142,20 @@ func (s *IntegrationTestSuite) testGovCommunityPoolSpend() {
140142
submitGovFlags := []string{configFile(proposalCommunitySpendFilename)}
141143
depositGovFlags := []string{strconv.Itoa(proposalCounter), depositAmount.String()}
142144
voteGovFlags := []string{strconv.Itoa(proposalCounter), "yes"}
143-
s.submitGovProposal(chainAAPIEndpoint, sender, proposalCounter, "CommunityPoolSpend", submitGovFlags, depositGovFlags, voteGovFlags, "vote")
145+
s.submitGovProposal(chainAAPIEndpoint, sender, proposalCounter, "CommunityPoolSpend", submitGovFlags, depositGovFlags, voteGovFlags, "vote", govtypesv1beta1.StatusPassed)
146+
147+
// Check that sender is refunded with the proposal deposit
148+
s.Require().Eventually(
149+
func() bool {
150+
afterSenderBalance, err := getSpecificBalance(chainAAPIEndpoint, sender, uatoneDenom)
151+
s.Require().NoError(err)
144152

153+
return afterSenderBalance.IsEqual(beforeSenderBalance)
154+
},
155+
10*time.Second,
156+
time.Second,
157+
)
158+
// Check that recipient received the community pool spend
145159
s.Require().Eventually(
146160
func() bool {
147161
afterRecipientBalance, err := getSpecificBalance(chainAAPIEndpoint, recipient, uatoneDenom)
@@ -153,6 +167,53 @@ func (s *IntegrationTestSuite) testGovCommunityPoolSpend() {
153167
time.Second,
154168
)
155169
})
170+
s.Run("community pool spend with number of no votes exceeds threshold", func() {
171+
s.fundCommunityPool()
172+
chainAAPIEndpoint := fmt.Sprintf("http://%s", s.valResources[s.chainA.id][0].GetHostPort("1317/tcp"))
173+
senderAddress, _ := s.chainA.validators[0].keyInfo.GetAddress()
174+
sender := senderAddress.String()
175+
recipientAddress, _ := s.chainA.validators[1].keyInfo.GetAddress()
176+
recipient := recipientAddress.String()
177+
sendAmount := sdk.NewInt64Coin(uatoneDenom, 10_000_000) // 10atone
178+
s.writeGovCommunitySpendProposal(s.chainA, sendAmount, recipient)
179+
180+
beforeSenderBalance, err := getSpecificBalance(chainAAPIEndpoint, sender, uatoneDenom)
181+
s.Require().NoError(err)
182+
beforeRecipientBalance, err := getSpecificBalance(chainAAPIEndpoint, recipient, uatoneDenom)
183+
s.Require().NoError(err)
184+
185+
// Gov tests may be run in arbitrary order, each test must increment proposalCounter to have the correct proposal id to submit and query
186+
proposalCounter++
187+
submitGovFlags := []string{configFile(proposalCommunitySpendFilename)}
188+
depositGovFlags := []string{strconv.Itoa(proposalCounter), depositAmount.String()}
189+
voteGovFlags := []string{strconv.Itoa(proposalCounter), "no"}
190+
s.submitGovProposal(chainAAPIEndpoint, sender, proposalCounter, "CommunityPoolSpend", submitGovFlags, depositGovFlags, voteGovFlags, "vote", govtypesv1beta1.StatusRejected)
191+
192+
// Check that sender is not refunded with the proposal deposit
193+
s.Require().Eventually(
194+
func() bool {
195+
afterSenderBalance, err := getSpecificBalance(chainAAPIEndpoint, sender, uatoneDenom)
196+
s.Require().NoError(err)
197+
198+
return afterSenderBalance.Add(depositAmount).Add(initialDepositAmount).
199+
IsEqual(beforeSenderBalance)
200+
},
201+
10*time.Second,
202+
time.Second,
203+
)
204+
// Check that recipient didnt receive the community pool spend since the
205+
// proposal was rejected
206+
s.Require().Eventually(
207+
func() bool {
208+
afterRecipientBalance, err := getSpecificBalance(chainAAPIEndpoint, recipient, uatoneDenom)
209+
s.Require().NoError(err)
210+
211+
return afterRecipientBalance.IsEqual(beforeRecipientBalance)
212+
},
213+
10*time.Second,
214+
time.Second,
215+
)
216+
})
156217
}
157218

158219
// testGovParamChange tests passing a param change proposal.
@@ -173,7 +234,7 @@ func (s *IntegrationTestSuite) testGovParamChange() {
173234
submitGovFlags := []string{configFile(proposalParamChangeFilename)}
174235
depositGovFlags := []string{strconv.Itoa(proposalCounter), depositAmount.String()}
175236
voteGovFlags := []string{strconv.Itoa(proposalCounter), "yes"}
176-
s.submitGovProposal(chainAAPIEndpoint, sender, proposalCounter, "cosmos.staking.v1beta1.MsgUpdateParams", submitGovFlags, depositGovFlags, voteGovFlags, "vote")
237+
s.submitGovProposal(chainAAPIEndpoint, sender, proposalCounter, "cosmos.staking.v1beta1.MsgUpdateParams", submitGovFlags, depositGovFlags, voteGovFlags, "vote", govtypesv1beta1.StatusPassed)
177238

178239
newParams := s.queryStakingParams(chainAAPIEndpoint)
179240
s.Assert().NotEqual(oldMaxValidator, newParams.Params.MaxValidators)
@@ -196,7 +257,7 @@ func (s *IntegrationTestSuite) testGovParamChange() {
196257
submitGovFlags := []string{configFile(proposalParamChangeFilename)}
197258
depositGovFlags := []string{strconv.Itoa(proposalCounter), depositAmount.String()}
198259
voteGovFlags := []string{strconv.Itoa(proposalCounter), "yes"}
199-
s.submitGovProposal(chainAAPIEndpoint, sender, proposalCounter, "atomone.photon.v1.MsgUpdateParams", submitGovFlags, depositGovFlags, voteGovFlags, "vote")
260+
s.submitGovProposal(chainAAPIEndpoint, sender, proposalCounter, "atomone.photon.v1.MsgUpdateParams", submitGovFlags, depositGovFlags, voteGovFlags, "vote", govtypesv1beta1.StatusPassed)
200261

201262
newParams := s.queryPhotonParams(chainAAPIEndpoint)
202263
s.Assert().True(newParams.Params.MintDisabled, "expected photon param mint disabled to be true")
@@ -207,7 +268,7 @@ func (s *IntegrationTestSuite) testGovParamChange() {
207268
depositGovFlags = []string{strconv.Itoa(proposalCounter), depositAmount.String()}
208269
voteGovFlags = []string{strconv.Itoa(proposalCounter), "yes"}
209270
s.writePhotonParamChangeProposal(s.chainA, params.Params)
210-
s.submitGovProposal(chainAAPIEndpoint, sender, proposalCounter, "atomone.photon.v1.MsgUpdateParams", submitGovFlags, depositGovFlags, voteGovFlags, "vote")
271+
s.submitGovProposal(chainAAPIEndpoint, sender, proposalCounter, "atomone.photon.v1.MsgUpdateParams", submitGovFlags, depositGovFlags, voteGovFlags, "vote", govtypesv1beta1.StatusPassed)
211272

212273
newParams = s.queryPhotonParams(chainAAPIEndpoint)
213274
s.Require().False(newParams.Params.MintDisabled, "expected photon param mint disabled to be false")
@@ -229,7 +290,7 @@ func (s *IntegrationTestSuite) testGovConstitutionAmendment() {
229290
submitGovFlags := []string{configFile(proposalConstitutionAmendmentFilename)}
230291
depositGovFlags := []string{strconv.Itoa(proposalCounter), depositAmount.String()}
231292
voteGovFlags := []string{strconv.Itoa(proposalCounter), "yes"}
232-
s.submitGovProposal(chainAAPIEndpoint, sender, proposalCounter, "gov/MsgSubmitProposal", submitGovFlags, depositGovFlags, voteGovFlags, "vote")
293+
s.submitGovProposal(chainAAPIEndpoint, sender, proposalCounter, "gov/MsgSubmitProposal", submitGovFlags, depositGovFlags, voteGovFlags, "vote", govtypesv1beta1.StatusPassed)
233294

234295
s.Require().Eventually(
235296
func() bool {
@@ -260,14 +321,14 @@ func (s *IntegrationTestSuite) submitLegacyGovProposal(chainAAPIEndpoint, sender
260321
// Instead, the deposit is added to the "deposit" field of the proposal JSON (usually stored as a file)
261322
// you can use `atomoned tx gov draft-proposal` to create a proposal file that you can use
262323
// min initial deposit of 100uatone is required in e2e tests, otherwise the proposal would be dropped
263-
func (s *IntegrationTestSuite) submitGovProposal(chainAAPIEndpoint, sender string, proposalID int, proposalType string, submitFlags []string, depositFlags []string, voteFlags []string, voteCommand string) {
324+
func (s *IntegrationTestSuite) submitGovProposal(chainAAPIEndpoint, sender string, proposalID int, proposalType string, submitFlags []string, depositFlags []string, voteFlags []string, voteCommand string, expectedStatusAfterVote govtypesv1beta1.ProposalStatus) {
264325
s.T().Logf("Submitting Gov Proposal: %s", proposalType)
265326
sflags := submitFlags
266327
s.submitGovCommand(chainAAPIEndpoint, sender, proposalID, "submit-proposal", sflags, govtypesv1beta1.StatusDepositPeriod)
267328
s.T().Logf("Depositing Gov Proposal: %s", proposalType)
268329
s.submitGovCommand(chainAAPIEndpoint, sender, proposalID, "deposit", depositFlags, govtypesv1beta1.StatusVotingPeriod)
269330
s.T().Logf("Voting Gov Proposal: %s", proposalType)
270-
s.submitGovCommand(chainAAPIEndpoint, sender, proposalID, voteCommand, voteFlags, govtypesv1beta1.StatusPassed)
331+
s.submitGovCommand(chainAAPIEndpoint, sender, proposalID, voteCommand, voteFlags, expectedStatusAfterVote)
271332
}
272333

273334
func (s *IntegrationTestSuite) verifyChainHaltedAtUpgradeHeight(c *chain, valIdx int, upgradeHeight int64) {

tests/e2e/genesis_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ func modifyGenesis(path, moniker, amountStr string, addrAll []sdk.AccAddress, de
202202
govv1.DefaultTargetActiveProposals, sdk.NewCoins(sdk.NewCoin(denom, initialDepositAmount.Amount)), govv1.DefaultMinInitialDepositUpdatePeriod,
203203
govv1.DefaultMinInitialDepositSensitivityTargetDistance, govv1.DefaultMinInitialDepositIncreaseRatio.String(),
204204
govv1.DefaultMinInitialDepositDecreaseRatio.String(), govv1.DefaultTargetProposalsInDepositPeriod,
205+
govv1.DefaultBurnDepositNoThreshold.String(),
205206
),
206207
)
207208
govGenState.Constitution = "This is a test constitution"

x/gov/README.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,18 +41,18 @@ staking token of the chain.
4141
* [Proposal submission](#proposal-submission)
4242
* [Deposit](#deposit)
4343
* [Vote](#vote)
44-
* [Software Upgrade](#software-upgrade)
44+
* [Quorum](#quorum)
4545
* [State](#state)
4646
* [Proposals](#proposals)
4747
* [Parameters and base types](#parameters-and-base-types)
4848
* [Deposit](#deposit-1)
49-
* [ValidatorGovInfo](#validatorgovinfo)
50-
* [Stores](#stores)
49+
* [Stores](#stores)
5150
* [Proposal Processing Queue](#proposal-processing-queue)
5251
* [Legacy Proposal](#legacy-proposal)
5352
* [Quorum Checks and Voting Period Extension](#quorum-checks-and-voting-period-extension)
5453
* [Constitution](#constitution)
5554
* [Law and Constitution Amendment Proposals](#law-and-constitution-amendment-proposals)
55+
* [Last Min Deposit and Last Min Initial Deposit](#last-min-deposit-and-last-min-initial-deposit)
5656
* [Messages](#messages)
5757
* [Proposal Submission](#proposal-submission-1)
5858
* [Deposit](#deposit-2)
@@ -61,6 +61,8 @@ staking token of the chain.
6161
* [EndBlocker](#endblocker)
6262
* [Handlers](#handlers)
6363
* [Parameters](#parameters)
64+
* [MinDepositThrottler (dynamic MinDeposit)](#mindepositthrottler-dynamic-mindeposit)
65+
* [MinInitialDepositThrottler (dynamic MinInitialDeposit)](#mininitialdepositthrottler-dynamic-mininitialdeposit)
6466
* [Client](#client)
6567
* [CLI](#cli)
6668
* [gRPC](#grpc)
@@ -199,6 +201,11 @@ The initial option set includes the following options:
199201
`Abstain` option allows voters to signal that they do not intend to vote in
200202
favor or against the proposal but accept the result of the vote.
201203

204+
At the end of the voting period, if the percentage of `No` votes (excluding
205+
`Abstain` votes) is greater than a specific threshold (see [Burnable
206+
Params](#burnable-params) section), then the proposal is considered as SPAM and
207+
its deposit is burned.
208+
202209
#### Weighted Votes
203210

204211
[ADR-037](https://github.com/cosmos/cosmos-sdk/blob/main/docs/architecture/adr-037-gov-split-vote.md)
@@ -270,6 +277,9 @@ have to sign governance transactions with the sensitive CometBFT PrivKey.
270277
There are three parameters that define if the deposit of a proposal should
271278
be burned or returned to the depositors.
272279

280+
* `BurnDepositNoThreshold` burns the proposal deposit at the end of the voting
281+
period if the percentage of `No` votes (excluding `Abstain` votes) exceeds
282+
the threshold.
273283
* `BurnVoteQuorum` burns the proposal deposit if the proposal deposit if the vote does not reach quorum.
274284
* `BurnProposalDepositPrevote` burns the proposal deposit if it does not enter the voting phase.
275285

x/gov/abci.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,7 @@ func EndBlocker(ctx sdk.Context, keeper *keeper.Keeper) {
6969
// remove from queue
7070
keeper.RemoveFromQuorumCheckQueue(ctx, proposal.Id, endTime)
7171
// check if proposal passed quorum
72-
quorum, err := keeper.HasReachedQuorum(ctx, proposal)
73-
if err != nil {
74-
return false
75-
}
72+
quorum := keeper.HasReachedQuorum(ctx, proposal)
7673
logMsg := "proposal did not pass quorum after timeout, but was removed from quorum check queue"
7774
tagValue := types.AttributeValueProposalQuorumNotMet
7875

x/gov/genesis.go

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,7 @@ func InitGenesis(ctx sdk.Context, ak types.AccountKeeper, bk types.BankKeeper, k
4848
quorumCheckEntry := v1.NewQuorumCheckQueueEntry(quorumTimeoutTime, data.Params.QuorumCheckCount)
4949
quorum := false
5050
if ctx.BlockTime().After(quorumTimeoutTime) {
51-
var err error
52-
quorum, err = k.HasReachedQuorum(ctx, *proposal)
53-
if err != nil {
54-
panic(err)
55-
}
51+
quorum = k.HasReachedQuorum(ctx, *proposal)
5652
if !quorum {
5753
// since we don't export the state of the quorum check queue, we can't know how many checks were actually
5854
// done. However, in order to trigger a vote time extension, it is enough to have QuorumChecksDone > 0 to

x/gov/keeper/msg_server_test.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -924,6 +924,20 @@ func (suite *KeeperTestSuite) TestMsgUpdateParams() {
924924
expErr: true,
925925
expErrMsg: "quorum too large",
926926
},
927+
{
928+
name: "empty threshold",
929+
input: func() *v1.MsgUpdateParams {
930+
params1 := params
931+
params1.Threshold = ""
932+
933+
return &v1.MsgUpdateParams{
934+
Authority: authority,
935+
Params: params1,
936+
}
937+
},
938+
expErr: true,
939+
expErrMsg: "invalid threshold string: decimal string cannot be empty",
940+
},
927941
{
928942
name: "invalid threshold",
929943
input: func() *v1.MsgUpdateParams {
@@ -994,6 +1008,20 @@ func (suite *KeeperTestSuite) TestMsgUpdateParams() {
9941008
expErr: true,
9951009
expErrMsg: "constitution amendment quorum too large",
9961010
},
1011+
{
1012+
name: "empty constitution amendment threshold",
1013+
input: func() *v1.MsgUpdateParams {
1014+
params1 := params
1015+
params1.ConstitutionAmendmentThreshold = ""
1016+
1017+
return &v1.MsgUpdateParams{
1018+
Authority: authority,
1019+
Params: params1,
1020+
}
1021+
},
1022+
expErr: true,
1023+
expErrMsg: "invalid constitution amendment threshold string: decimal string cannot be empty",
1024+
},
9971025
{
9981026
name: "negative constitution amendment threshold",
9991027
input: func() *v1.MsgUpdateParams {
@@ -1205,6 +1233,97 @@ func (suite *KeeperTestSuite) TestMsgUpdateParams() {
12051233
}
12061234
},
12071235
},
1236+
{
1237+
name: "empty burnDepositNoThreshold",
1238+
input: func() *v1.MsgUpdateParams {
1239+
params1 := params
1240+
params1.BurnDepositNoThreshold = ""
1241+
1242+
return &v1.MsgUpdateParams{
1243+
Authority: authority,
1244+
Params: params1,
1245+
}
1246+
},
1247+
expErr: true,
1248+
expErrMsg: "invalid burnDepositNoThreshold string: decimal string cannot be empty",
1249+
},
1250+
{
1251+
name: "invalid burnDepositNoThreshold",
1252+
input: func() *v1.MsgUpdateParams {
1253+
params1 := params
1254+
params1.BurnDepositNoThreshold = "abc"
1255+
1256+
return &v1.MsgUpdateParams{
1257+
Authority: authority,
1258+
Params: params1,
1259+
}
1260+
},
1261+
expErr: true,
1262+
expErrMsg: "invalid burnDepositNoThreshold string",
1263+
},
1264+
{
1265+
name: "burnDepositNoThreshold <= 1 - amendmentThreshold",
1266+
input: func() *v1.MsgUpdateParams {
1267+
params1 := params
1268+
params1.LawThreshold = "0.8"
1269+
params1.ConstitutionAmendmentThreshold = "0.8"
1270+
params1.BurnDepositNoThreshold = "0.199"
1271+
1272+
return &v1.MsgUpdateParams{
1273+
Authority: authority,
1274+
Params: params1,
1275+
}
1276+
},
1277+
expErr: true,
1278+
expErrMsg: "burnDepositNoThreshold cannot be lower than 1-amendmentThreshold",
1279+
},
1280+
{
1281+
name: "burnDepositNoThreshold <= 1 - lawThreshold",
1282+
input: func() *v1.MsgUpdateParams {
1283+
params1 := params
1284+
params1.ConstitutionAmendmentThreshold = "0.9"
1285+
params1.LawThreshold = "0.8"
1286+
params1.BurnDepositNoThreshold = "0.199"
1287+
1288+
return &v1.MsgUpdateParams{
1289+
Authority: authority,
1290+
Params: params1,
1291+
}
1292+
},
1293+
expErr: true,
1294+
expErrMsg: "burnDepositNoThreshold cannot be lower than 1-lawThreshold",
1295+
},
1296+
{
1297+
name: "burnDepositNoThreshold <= 1 - threshold",
1298+
input: func() *v1.MsgUpdateParams {
1299+
params1 := params
1300+
params1.ConstitutionAmendmentThreshold = "0.8"
1301+
params1.LawThreshold = "0.8"
1302+
params1.Threshold = "0.6"
1303+
params1.BurnDepositNoThreshold = "0.399"
1304+
1305+
return &v1.MsgUpdateParams{
1306+
Authority: authority,
1307+
Params: params1,
1308+
}
1309+
},
1310+
expErr: true,
1311+
expErrMsg: "burnDepositNoThreshold cannot be lower than 1-threshold",
1312+
},
1313+
{
1314+
name: "burnDepositNoThreshold > 1",
1315+
input: func() *v1.MsgUpdateParams {
1316+
params1 := params
1317+
params1.BurnDepositNoThreshold = "2"
1318+
1319+
return &v1.MsgUpdateParams{
1320+
Authority: authority,
1321+
Params: params1,
1322+
}
1323+
},
1324+
expErr: true,
1325+
expErrMsg: "burnDepositNoThreshold too large",
1326+
},
12081327
}
12091328

12101329
for _, tc := range testCases {

0 commit comments

Comments
 (0)