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

feat(x/gov): dynamic deposit throttler #69

Open
wants to merge 24 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
56256ce
prototype `GetCurrentMinDeposit`
giunatale Dec 2, 2024
4f5354b
move prototype to separate file
giunatale Dec 2, 2024
2388dba
draft implementation, still missing pieces
giunatale Dec 3, 2024
44d8775
add ways to activate proposals when deposit decreases
giunatale Dec 3, 2024
e9d5c79
make deposit stay stable when at target
giunatale Dec 3, 2024
907b804
improve GetMinDeposit
giunatale Dec 4, 2024
2e78d6e
add mindeposit query
giunatale Dec 4, 2024
4d3cdc2
add params validation
giunatale Dec 4, 2024
dbe2de2
params
giunatale Dec 4, 2024
6d57f40
genesis
giunatale Dec 4, 2024
2a64c3e
move out code from keeper.SetParams
giunatale Dec 10, 2024
cbe9f12
fix sign of percChange
giunatale Dec 10, 2024
dc1b315
fix tests
giunatale Dec 13, 2024
f4aacfa
fix e2e tests
giunatale Dec 16, 2024
95d679a
fix formula for min deposit
giunatale Dec 16, 2024
3367e48
test(gov): GetMinDeposit()
tbruyelle Dec 16, 2024
c107b40
update proto description
giunatale Dec 17, 2024
27f2db6
move throttler params into a struct
giunatale Dec 19, 2024
c3d238f
fix(gov): default genesis state is not valid
tbruyelle Dec 20, 2024
c51d5d7
test(gov): handle when lastMinDeposit has other coins than minDeposit…
tbruyelle Dec 20, 2024
37dda5c
fix(x/gov): α must be strictly between 0 and 1 (#70)
tbruyelle Dec 20, 2024
c043895
test(x/gov): assert ActivateVotingPeriod increments ActiveProposalNumber
tbruyelle Dec 20, 2024
fa0d021
test(x/gov): assert EndBlocker decrements ActiveProposalNumber
tbruyelle Dec 20, 2024
fdca78f
docs(x/gov): min_deposit deprecation details
tbruyelle Dec 20, 2024
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
3 changes: 3 additions & 0 deletions proto/atomone/gov/v1/genesis.proto
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,7 @@ message GenesisState {
//
// Since: cosmos-sdk 0.48
string constitution = 9;

// last updated value for the dynamic min deposit
LastMinDeposit last_min_deposit = 10;
}
47 changes: 46 additions & 1 deletion proto/atomone/gov/v1/gov.proto
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,17 @@ message Deposit {
[ (gogoproto.nullable) = false, (amino.dont_omitempty) = true ];
}

// LastMinDeposit is a record of the last time the minimum deposit
// was updated in the store, both its value and a timestamp
message LastMinDeposit {
// value is the value of the minimum deposit
repeated cosmos.base.v1beta1.Coin value = 1
[ (gogoproto.nullable) = false, (amino.dont_omitempty) = true ];

// time is the time the minimum deposit was last updated
google.protobuf.Timestamp time = 2 [ (gogoproto.stdtime) = true ];
}

// Proposal defines the core field members of a governance proposal.
message Proposal {
// id defines the unique id of the proposal.
Expand Down Expand Up @@ -208,13 +219,45 @@ message TallyParams {
string law_threshold = 6 [(cosmos_proto.scalar) = "cosmos.Dec"];
}

message MinDepositThrottler {
// Floor value for the minimum deposit required for a proposal to enter the voting period.
repeated cosmos.base.v1beta1.Coin floor_value = 1
[ (gogoproto.nullable) = false, (amino.dont_omitempty) = true ];

// Duration that dictates after how long the dynamic minimum deposit should be recalculated
// for time-based updates.
google.protobuf.Duration update_period = 2 [(gogoproto.stdduration) = true];

// The number of active proposals the dynamic minimum deposit should target.
uint64 target_active_proposals = 3;

// The ratio of increase for the minimum deposit when the number of active proposals
// exceeds the target by 1.
string increase_ratio = 4 [(cosmos_proto.scalar) = "cosmos.Dec"];

// The ratio of decrease for the minimum deposit when the number of active proposals
// is 1 less than the target.
string decrease_ratio = 5 [(cosmos_proto.scalar) = "cosmos.Dec"];

// A positive integer representing the sensitivity of the dynamic minimum deposit
// increase/decrease to the distance from the target number of active proposals.
// The higher the number, the lower the sensitivity. A value of 1 represents the
// highest sensitivity.
uint64 sensitivity_target_distance = 6;
}

// Params defines the parameters for the x/gov module.
//
// Since: cosmos-sdk 0.47
message Params {
// Minimum deposit for a proposal to enter voting period.
// Deprecated: a dynamic system now determines the minimum deposit,
// see the other params inside the min_deposit_throttler field.
// While setting this value returns an error, when queried it is set to the
// value of the current minimum deposit value as determined by the dynamic
// system for backward compatibility.
repeated cosmos.base.v1beta1.Coin min_deposit = 1
[ (gogoproto.nullable) = false, (amino.dont_omitempty) = true ];
[ (gogoproto.nullable) = false, (amino.dont_omitempty) = true, deprecated = true ];

// Maximum period for Atom holders to deposit on a proposal. Initial value: 2
// months.
Expand Down Expand Up @@ -272,4 +315,6 @@ message Params {
// Number of times a proposal should be checked for quorum after the quorum timeout
// has elapsed. Used to compute the amount of time in between quorum checks.
uint64 quorum_check_count = 22;

MinDepositThrottler min_deposit_throttler = 23;
}
17 changes: 17 additions & 0 deletions proto/atomone/gov/v1/query.proto
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
syntax = "proto3";
package atomone.gov.v1;

import "cosmos/base/v1beta1/coin.proto";
import "gogoproto/gogo.proto";
import "cosmos/base/query/v1beta1/pagination.proto";
import "google/api/annotations.proto";
import "atomone/gov/v1/gov.proto";
Expand Down Expand Up @@ -61,6 +63,12 @@ service Query {
option (google.api.http).get =
"/atomone/gov/v1/proposals/{proposal_id}/tally";
}

// MinDeposit queries the minimum deposit currently
// required for a proposal to enter voting period.
rpc MinDeposit(QueryMinDepositRequest) returns (QueryMinDepositResponse) {
option (google.api.http).get = "/atomone/gov/v1/mindeposit";
}
}

// QueryConstitutionRequest is the request type for the Query/Constitution RPC method
Expand Down Expand Up @@ -209,3 +217,12 @@ message QueryTallyResultResponse {
// tally defines the requested tally.
TallyResult tally = 1;
}

// QueryMinDepositRequest is the request type for the Query/MinDeposit RPC method.
message QueryMinDepositRequest {}

// QueryMinDepositResponse is the response type for the Query/MinDeposit RPC method.
message QueryMinDepositResponse {
// min_deposit defines the minimum deposit required for a proposal to enter voting period.
repeated cosmos.base.v1beta1.Coin min_deposit = 1 [ (gogoproto.nullable) = false];
}
7 changes: 6 additions & 1 deletion tests/e2e/genesis.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,13 +179,18 @@ func modifyGenesis(path, moniker, amountStr string, addrAll []sdk.AccAddress, de

govGenState := govv1.NewGenesisState(1,
govv1.NewParams(
sdk.NewCoins(sdk.NewCoin(denom, amnt)), maxDepositPeriod,
// sdk.NewCoins(sdk.NewCoin(denom, amnt)),
maxDepositPeriod,
votingPeriod,
quorum.String(), threshold.String(),
amendmentsQuorum.String(), amendmentsThreshold.String(), lawQuorum.String(), lawThreshold.String(),
sdk.ZeroDec().String(),
false, false, govv1.DefaultMinDepositRatio.String(),
govv1.DefaultQuorumTimeout, govv1.DefaultMaxVotingPeriodExtension, govv1.DefaultQuorumCheckCount,
sdk.NewCoins(sdk.NewCoin(denom, amnt)), govv1.DefaultMinDepositUpdatePeriod,
govv1.DefaultMinDepositSensitivityTargetDistance,
govv1.DefaultMinDepositIncreaseRatio.String(), govv1.DefaultMinDepositDecreaseRatio.String(),
govv1.DefaultTargetActiveProposals,
),
)
govGenState.Constitution = "This is a test constitution"
Expand Down
11 changes: 10 additions & 1 deletion x/gov/abci.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ func EndBlocker(ctx sdk.Context, keeper *keeper.Keeper) {
// delete dead proposals from store and returns theirs deposits.
// A proposal is dead when it's inactive and didn't get enough deposit on time to get into voting phase.
keeper.IterateInactiveProposalsQueue(ctx, ctx.BlockHeader().Time, func(proposal v1.Proposal) bool {
// before deleting, check one last time if the proposal has enough deposits to get into voting phase,
// maybe because min deposit decreased in the meantime.
minDeposit := keeper.GetMinDeposit(ctx)
if proposal.Status == v1.StatusDepositPeriod && sdk.NewCoins(proposal.TotalDeposit...).IsAllGTE(minDeposit) {
keeper.ActivateVotingPeriod(ctx, proposal)
return false
}

keeper.DeleteProposal(ctx, proposal.Id)

params := keeper.GetParams(ctx)
Expand All @@ -45,7 +53,7 @@ func EndBlocker(ctx sdk.Context, keeper *keeper.Keeper) {
logger.Info(
"proposal did not meet minimum deposit; deleted",
"proposal", proposal.Id,
"min_deposit", sdk.NewCoins(params.MinDeposit...).String(),
"min_deposit", sdk.NewCoins(minDeposit...).String(),
"total_deposit", sdk.NewCoins(proposal.TotalDeposit...).String(),
)

Expand Down Expand Up @@ -190,6 +198,7 @@ func EndBlocker(ctx sdk.Context, keeper *keeper.Keeper) {

keeper.SetProposal(ctx, proposal)
keeper.RemoveFromActiveProposalQueue(ctx, proposal.Id, *proposal.VotingEndTime)
keeper.DecrementActiveProposalsNumber(ctx)

// when proposal become active
keeper.Hooks().AfterProposalVotingPeriodEnded(ctx, proposal.Id)
Expand Down
6 changes: 5 additions & 1 deletion x/gov/abci_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ func TestTickPassedVotingPeriod(t *testing.T) {
activeQueue = suite.GovKeeper.ActiveProposalQueueIterator(ctx, ctx.BlockHeader().Time)
require.True(t, activeQueue.Valid())

require.EqualValues(t, 1, suite.GovKeeper.GetActiveProposalsNumber(ctx))
activeProposalID := types.GetProposalIDFromBytes(activeQueue.Value())
proposal, ok := suite.GovKeeper.GetProposal(ctx, activeProposalID)
require.True(t, ok)
Expand All @@ -285,6 +286,7 @@ func TestTickPassedVotingPeriod(t *testing.T) {

gov.EndBlocker(ctx, suite.GovKeeper)

require.EqualValues(t, 0, suite.GovKeeper.GetActiveProposalsNumber(ctx))
activeQueue = suite.GovKeeper.ActiveProposalQueueIterator(ctx, ctx.BlockHeader().Time)
require.False(t, activeQueue.Valid())
activeQueue.Close()
Expand Down Expand Up @@ -379,10 +381,12 @@ func TestEndBlockerProposalHandlerFailed(t *testing.T) {
newHeader := ctx.BlockHeader()
newHeader.Time = ctx.BlockHeader().Time.Add(*suite.GovKeeper.GetParams(ctx).MaxDepositPeriod).Add(*suite.GovKeeper.GetParams(ctx).VotingPeriod)
ctx = ctx.WithBlockHeader(newHeader)
require.EqualValues(t, 1, suite.GovKeeper.GetActiveProposalsNumber(ctx))

// validate that the proposal fails/has been rejected
gov.EndBlocker(ctx, suite.GovKeeper)

require.EqualValues(t, 0, suite.GovKeeper.GetActiveProposalsNumber(ctx))
proposal, ok := suite.GovKeeper.GetProposal(ctx, proposal.Id)
require.True(t, ok)
require.Equal(t, v1.StatusFailed, proposal.Status)
Expand Down Expand Up @@ -483,7 +487,7 @@ func TestEndBlockerQuorumCheck(t *testing.T) {
require.NoError(t, err)
require.NotNil(t, res)
// Activate proposal
newDepositMsg := v1.NewMsgDeposit(addrs[1], res.ProposalId, params.MinDeposit)
newDepositMsg := v1.NewMsgDeposit(addrs[1], res.ProposalId, suite.GovKeeper.GetMinDeposit(ctx))
res1, err := govMsgSvr.Deposit(ctx, newDepositMsg)
require.NoError(t, err)
require.NotNil(t, res1)
Expand Down
33 changes: 33 additions & 0 deletions x/gov/client/cli/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ func GetQueryCmd() *cobra.Command {
GetCmdQueryDeposits(),
GetCmdQueryTally(),
GetCmdConstitution(),
GetCmdQueryMinDeposit(),
)

return govQueryCmd
Expand Down Expand Up @@ -676,3 +677,35 @@ func GetCmdConstitution() *cobra.Command {
},
}
}

// GetCmdQueryMinDeposit implements the query min deposit command.
func GetCmdQueryMinDeposit() *cobra.Command {
return &cobra.Command{
Use: "min-deposit",
Args: cobra.ExactArgs(0),
Short: "Query the minimum deposit currently needed for a proposal to enter voting period",
Long: strings.TrimSpace(
fmt.Sprintf(`Query the minimum deposit needed for a proposal to enter voting period.

Example:
$ %s query gov min-deposit
`,
version.AppName,
),
),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientTxContext(cmd)
if err != nil {
return err
}
queryClient := v1.NewQueryClient(clientCtx)

resp, err := queryClient.MinDeposit(cmd.Context(), &v1.QueryMinDepositRequest{})
if err != nil {
return err
}

return clientCtx.PrintProto(resp)
},
}
}
13 changes: 13 additions & 0 deletions x/gov/genesis.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@ func InitGenesis(ctx sdk.Context, ak types.AccountKeeper, bk types.BankKeeper, k
if !balance.IsEqual(totalDeposits) {
panic(fmt.Sprintf("expected module account was %s but we got %s", balance.String(), totalDeposits.String()))
}

if data.LastMinDeposit != nil {
k.SetLastMinDeposit(ctx, data.LastMinDeposit.Value, *data.LastMinDeposit.Time)
} else {
k.SetLastMinDeposit(ctx, data.Params.MinDepositThrottler.FloorValue, ctx.BlockTime())
}
}

// ExportGenesis - output genesis parameters
Expand All @@ -96,12 +102,19 @@ func ExportGenesis(ctx sdk.Context, k *keeper.Keeper) *v1.GenesisState {
proposalsVotes = append(proposalsVotes, votes...)
}

blockTime := ctx.BlockTime()
lastMinDeposit := v1.LastMinDeposit{
Value: k.GetMinDeposit(ctx),
Time: &blockTime,
}

return &v1.GenesisState{
StartingProposalId: startingProposalID,
Deposits: proposalsDeposits,
Votes: proposalsVotes,
Proposals: proposals,
Params: &params,
Constitution: constitution,
LastMinDeposit: &lastMinDeposit,
}
}
35 changes: 32 additions & 3 deletions x/gov/genesis_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ func TestImportExportQueues_ErrorUnconsistentState(t *testing.T) {
suite := createTestSuite(t)
app := suite.App
ctx := app.BaseApp.NewContext(false, tmproto.Header{})
expectedGenState := v1.DefaultGenesisState()
expectedGenState.LastMinDeposit = &v1.LastMinDeposit{
Value: sdk.NewCoins(expectedGenState.Params.MinDepositThrottler.FloorValue...),
Time: &time.Time{},
}
require.Panics(t, func() {
gov.InitGenesis(ctx, suite.AccountKeeper, suite.BankKeeper, suite.GovKeeper, &v1.GenesisState{
Deposits: v1.Deposits{
Expand All @@ -44,20 +49,24 @@ func TestImportExportQueues_ErrorUnconsistentState(t *testing.T) {
})
gov.InitGenesis(ctx, suite.AccountKeeper, suite.BankKeeper, suite.GovKeeper, v1.DefaultGenesisState())
genState := gov.ExportGenesis(ctx, suite.GovKeeper)
require.Equal(t, genState, v1.DefaultGenesisState())
require.Equal(t, genState, expectedGenState)
}

func TestInitGenesis(t *testing.T) {
var (
testAddrs = simtestutil.CreateRandomAccounts(2)
params = &v1.Params{
MinDeposit: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(42))),
MinDepositThrottler: &v1.MinDepositThrottler{
FloorValue: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(42))),
},
}
quorumTimeout = time.Hour * 20
paramsWithQuorumCheckEnabled = &v1.Params{
MinDeposit: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(42))),
QuorumCheckCount: 10,
QuorumTimeout: &quorumTimeout,
MinDepositThrottler: &v1.MinDepositThrottler{
FloorValue: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(42))),
},
}

depositAmount = sdk.Coins{
Expand Down Expand Up @@ -90,6 +99,7 @@ func TestInitGenesis(t *testing.T) {
Options: v1.NewNonSplitVoteOption(v1.OptionNo),
},
}
utcTime = time.Now().UTC()
depositEndTime = time.Now().Add(time.Hour * 8)
votingStartTime = time.Now()
votingEndTime = time.Now().Add(time.Hour * 24)
Expand Down Expand Up @@ -162,6 +172,25 @@ func TestInitGenesis(t *testing.T) {
t.Helper()
p := s.GovKeeper.GetParams(ctx)
assert.Equal(t, *params, p)
lmdCoins, lmdTime := s.GovKeeper.GetLastMinDeposit(ctx)
assert.EqualValues(t, p.MinDepositThrottler.FloorValue, lmdCoins)
assert.Equal(t, ctx.BlockTime(), lmdTime)
},
},
{
name: "ok: genesis with last min deposit",
genesis: v1.GenesisState{
Params: params,
LastMinDeposit: &v1.LastMinDeposit{
Value: sdk.NewCoins(sdk.NewInt64Coin("xxx", 1)),
Time: &utcTime,
},
},
assert: func(t *testing.T, ctx sdk.Context, s suite) {
t.Helper()
lmdCoins, lmdTime := s.GovKeeper.GetLastMinDeposit(ctx)
assert.EqualValues(t, sdk.NewCoins(sdk.NewInt64Coin("xxx", 1)), lmdCoins)
assert.Equal(t, utcTime, lmdTime)
},
},
{
Expand Down
Loading
Loading