From bffcae54a1afee8e93468fbf70f7235894b17465 Mon Sep 17 00:00:00 2001 From: Sunny Aggarwal Date: Wed, 21 Apr 2021 12:59:30 -0400 Subject: [PATCH] Add hooks to governance actions (#9133) * add governance hooks * fix lint * fix lint * CHANGELOG * sh -> gh * improve comments * add test * add more tests * rename two of the hooks Co-authored-by: ahmedaly113 --- CHANGELOG.md | 1 + simapp/app.go | 8 ++- x/gov/abci.go | 6 +++ x/gov/keeper/deposit.go | 3 ++ x/gov/keeper/hooks.go | 44 +++++++++++++++ x/gov/keeper/hooks_test.go | 96 +++++++++++++++++++++++++++++++++ x/gov/keeper/internal_test.go | 8 +++ x/gov/keeper/keeper.go | 14 +++++ x/gov/keeper/proposal.go | 3 ++ x/gov/keeper/vote.go | 3 ++ x/gov/types/expected_keepers.go | 13 +++++ x/gov/types/hooks.go | 42 +++++++++++++++ 12 files changed, 240 insertions(+), 1 deletion(-) create mode 100644 x/gov/keeper/hooks.go create mode 100644 x/gov/keeper/hooks_test.go create mode 100644 x/gov/keeper/internal_test.go create mode 100644 x/gov/types/hooks.go diff --git a/CHANGELOG.md b/CHANGELOG.md index f53879536..5cff1105a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ * (rosetta) [\#8729](https://github.com/cosmos/cosmos-sdk/pull/8729) Data API fully supports balance tracking. Construction API can now construct any message supported by the application. * [\#8754](https://github.com/cosmos/cosmos-sdk/pull/8875) Added support for reverse iteration to pagination. * [#9088](https://github.com/cosmos/cosmos-sdk/pull/9088) Added implementation to ADR-28 Derived Addresses. +* [\#9133](https://github.com/cosmos/cosmos-sdk/pull/9133) Added hooks for governance actions. ### Client Breaking Changes * [\#8363](https://github.com/cosmos/cosmos-sdk/pull/8363) Addresses no longer have a fixed 20-byte length. From the SDK modules' point of view, any 1-255 bytes-long byte array is a valid address. diff --git a/simapp/app.go b/simapp/app.go index e3771e2fc..0e5281a2f 100644 --- a/simapp/app.go +++ b/simapp/app.go @@ -273,11 +273,17 @@ func NewSimApp( AddRoute(paramproposal.RouterKey, params.NewParamChangeProposalHandler(app.ParamsKeeper)). AddRoute(distrtypes.RouterKey, distr.NewCommunityPoolSpendProposalHandler(app.DistrKeeper)). AddRoute(upgradetypes.RouterKey, upgrade.NewSoftwareUpgradeProposalHandler(app.UpgradeKeeper)) - app.GovKeeper = govkeeper.NewKeeper( + govKeeper := govkeeper.NewKeeper( appCodec, keys[govtypes.StoreKey], app.GetSubspace(govtypes.ModuleName), app.AccountKeeper, app.BankKeeper, &stakingKeeper, govRouter, ) + app.GovKeeper = *govKeeper.SetHooks( + govtypes.NewMultiGovHooks( + // register the governance hooks + ), + ) + // create evidence keeper with router evidenceKeeper := evidencekeeper.NewKeeper( appCodec, keys[evidencetypes.StoreKey], &app.StakingKeeper, app.SlashingKeeper, diff --git a/x/gov/abci.go b/x/gov/abci.go index f9815e0fb..04d6cedf5 100644 --- a/x/gov/abci.go +++ b/x/gov/abci.go @@ -21,6 +21,9 @@ func EndBlocker(ctx sdk.Context, keeper keeper.Keeper) { keeper.DeleteProposal(ctx, proposal.ProposalId) keeper.DeleteDeposits(ctx, proposal.ProposalId) + // called when proposal become inactive + keeper.AfterProposalFailedMinDeposit(ctx, proposal.ProposalId) + ctx.EventManager().EmitEvent( sdk.NewEvent( types.EventTypeInactiveProposal, @@ -89,6 +92,9 @@ func EndBlocker(ctx sdk.Context, keeper keeper.Keeper) { keeper.SetProposal(ctx, proposal) keeper.RemoveFromActiveProposalQueue(ctx, proposal.ProposalId, proposal.VotingEndTime) + // when proposal become active + keeper.AfterProposalVotingPeriodEnded(ctx, proposal.ProposalId) + logger.Info( "proposal tallied", "proposal", proposal.ProposalId, diff --git a/x/gov/keeper/deposit.go b/x/gov/keeper/deposit.go index 507ac6793..cac341885 100644 --- a/x/gov/keeper/deposit.go +++ b/x/gov/keeper/deposit.go @@ -150,6 +150,9 @@ func (keeper Keeper) AddDeposit(ctx sdk.Context, proposalID uint64, depositorAdd deposit = types.NewDeposit(proposalID, depositorAddr, depositAmount) } + // called when deposit has been added to a proposal, however the proposal may not be active + keeper.AfterProposalDeposit(ctx, proposalID, depositorAddr) + ctx.EventManager().EmitEvent( sdk.NewEvent( types.EventTypeProposalDeposit, diff --git a/x/gov/keeper/hooks.go b/x/gov/keeper/hooks.go new file mode 100644 index 000000000..a63efdcc7 --- /dev/null +++ b/x/gov/keeper/hooks.go @@ -0,0 +1,44 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/gov/types" +) + +// Implements GovHooks interface +var _ types.GovHooks = Keeper{} + +// AfterProposalSubmission - call hook if registered +func (keeper Keeper) AfterProposalSubmission(ctx sdk.Context, proposalID uint64) { + if keeper.hooks != nil { + keeper.hooks.AfterProposalSubmission(ctx, proposalID) + } +} + +// AfterProposalDeposit - call hook if registered +func (keeper Keeper) AfterProposalDeposit(ctx sdk.Context, proposalID uint64, depositorAddr sdk.AccAddress) { + if keeper.hooks != nil { + keeper.hooks.AfterProposalDeposit(ctx, proposalID, depositorAddr) + } +} + +// AfterProposalVote - call hook if registered +func (keeper Keeper) AfterProposalVote(ctx sdk.Context, proposalID uint64, voterAddr sdk.AccAddress) { + if keeper.hooks != nil { + keeper.hooks.AfterProposalVote(ctx, proposalID, voterAddr) + } +} + +// AfterProposalFailedMinDeposit - call hook if registered +func (keeper Keeper) AfterProposalFailedMinDeposit(ctx sdk.Context, proposalID uint64) { + if keeper.hooks != nil { + keeper.hooks.AfterProposalFailedMinDeposit(ctx, proposalID) + } +} + +// AfterProposalVotingPeriodEnded - call hook if registered +func (keeper Keeper) AfterProposalVotingPeriodEnded(ctx sdk.Context, proposalID uint64) { + if keeper.hooks != nil { + keeper.hooks.AfterProposalVotingPeriodEnded(ctx, proposalID) + } +} diff --git a/x/gov/keeper/hooks_test.go b/x/gov/keeper/hooks_test.go new file mode 100644 index 000000000..f5b9e5170 --- /dev/null +++ b/x/gov/keeper/hooks_test.go @@ -0,0 +1,96 @@ +package keeper_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + tmproto "github.com/tendermint/tendermint/proto/tendermint/types" + + "github.com/cosmos/cosmos-sdk/simapp" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/gov" + "github.com/cosmos/cosmos-sdk/x/gov/keeper" + "github.com/cosmos/cosmos-sdk/x/gov/types" +) + +var _ types.GovHooks = &MockGovHooksReceiver{} + +// GovHooks event hooks for governance proposal object (noalias) +type MockGovHooksReceiver struct { + AfterProposalSubmissionValid bool + AfterProposalDepositValid bool + AfterProposalVoteValid bool + AfterProposalFailedMinDepositValid bool + AfterProposalVotingPeriodEndedValid bool +} + +func (h *MockGovHooksReceiver) AfterProposalSubmission(ctx sdk.Context, proposalID uint64) { + h.AfterProposalSubmissionValid = true +} + +func (h *MockGovHooksReceiver) AfterProposalDeposit(ctx sdk.Context, proposalID uint64, depositorAddr sdk.AccAddress) { + h.AfterProposalDepositValid = true +} + +func (h *MockGovHooksReceiver) AfterProposalVote(ctx sdk.Context, proposalID uint64, voterAddr sdk.AccAddress) { + h.AfterProposalVoteValid = true +} +func (h *MockGovHooksReceiver) AfterProposalFailedMinDeposit(ctx sdk.Context, proposalID uint64) { + h.AfterProposalFailedMinDepositValid = true +} +func (h *MockGovHooksReceiver) AfterProposalVotingPeriodEnded(ctx sdk.Context, proposalID uint64) { + h.AfterProposalVotingPeriodEndedValid = true +} + +func TestHooks(t *testing.T) { + app := simapp.Setup(false) + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + + minDeposit := app.GovKeeper.GetDepositParams(ctx).MinDeposit + addrs := simapp.AddTestAddrs(app, ctx, 1, minDeposit[0].Amount) + + govHooksReceiver := MockGovHooksReceiver{} + + app.GovKeeper = *keeper.UpdateHooks(&app.GovKeeper, + types.NewMultiGovHooks( + &govHooksReceiver, + ), + ) + + require.False(t, govHooksReceiver.AfterProposalSubmissionValid) + require.False(t, govHooksReceiver.AfterProposalDepositValid) + require.False(t, govHooksReceiver.AfterProposalVoteValid) + require.False(t, govHooksReceiver.AfterProposalFailedMinDepositValid) + require.False(t, govHooksReceiver.AfterProposalVotingPeriodEndedValid) + + tp := TestProposal + _, err := app.GovKeeper.SubmitProposal(ctx, tp) + require.NoError(t, err) + require.True(t, govHooksReceiver.AfterProposalSubmissionValid) + + newHeader := ctx.BlockHeader() + newHeader.Time = ctx.BlockHeader().Time.Add(app.GovKeeper.GetDepositParams(ctx).MaxDepositPeriod).Add(time.Duration(1) * time.Second) + ctx = ctx.WithBlockHeader(newHeader) + gov.EndBlocker(ctx, app.GovKeeper) + + require.True(t, govHooksReceiver.AfterProposalFailedMinDepositValid) + + p2, err := app.GovKeeper.SubmitProposal(ctx, tp) + require.NoError(t, err) + + activated, err := app.GovKeeper.AddDeposit(ctx, p2.ProposalId, addrs[0], minDeposit) + require.True(t, activated) + require.NoError(t, err) + require.True(t, govHooksReceiver.AfterProposalDepositValid) + + err = app.GovKeeper.AddVote(ctx, p2.ProposalId, addrs[0], types.NewNonSplitVoteOption(types.OptionYes)) + require.NoError(t, err) + require.True(t, govHooksReceiver.AfterProposalVoteValid) + + newHeader = ctx.BlockHeader() + newHeader.Time = ctx.BlockHeader().Time.Add(app.GovKeeper.GetVotingParams(ctx).VotingPeriod).Add(time.Duration(1) * time.Second) + ctx = ctx.WithBlockHeader(newHeader) + gov.EndBlocker(ctx, app.GovKeeper) + require.True(t, govHooksReceiver.AfterProposalVotingPeriodEndedValid) +} diff --git a/x/gov/keeper/internal_test.go b/x/gov/keeper/internal_test.go new file mode 100644 index 000000000..0c396e714 --- /dev/null +++ b/x/gov/keeper/internal_test.go @@ -0,0 +1,8 @@ +package keeper + +import "github.com/cosmos/cosmos-sdk/x/gov/types" + +func UpdateHooks(k *Keeper, h types.GovHooks) *Keeper { + k.hooks = h + return k +} diff --git a/x/gov/keeper/keeper.go b/x/gov/keeper/keeper.go index 60db99d32..8645a92da 100644 --- a/x/gov/keeper/keeper.go +++ b/x/gov/keeper/keeper.go @@ -23,6 +23,9 @@ type Keeper struct { // The reference to the DelegationSet and ValidatorSet to get information about validators and delegators sk types.StakingKeeper + // GovHooks + hooks types.GovHooks + // The (unexposed) keys used to access the stores from the Context. storeKey sdk.StoreKey @@ -66,6 +69,17 @@ func NewKeeper( } } +// SetHooks sets the hooks for governance +func (keeper *Keeper) SetHooks(gh types.GovHooks) *Keeper { + if keeper.hooks != nil { + panic("cannot set governance hooks twice") + } + + keeper.hooks = gh + + return keeper +} + // Logger returns a module-specific logger. func (keeper Keeper) Logger(ctx sdk.Context) log.Logger { return ctx.Logger().With("module", "x/"+types.ModuleName) diff --git a/x/gov/keeper/proposal.go b/x/gov/keeper/proposal.go index 14a324d78..853f9af35 100644 --- a/x/gov/keeper/proposal.go +++ b/x/gov/keeper/proposal.go @@ -41,6 +41,9 @@ func (keeper Keeper) SubmitProposal(ctx sdk.Context, content types.Content) (typ keeper.InsertInactiveProposalQueue(ctx, proposalID, proposal.DepositEndTime) keeper.SetProposalID(ctx, proposalID+1) + // called right after a proposal is submitted + keeper.AfterProposalSubmission(ctx, proposalID) + ctx.EventManager().EmitEvent( sdk.NewEvent( types.EventTypeSubmitProposal, diff --git a/x/gov/keeper/vote.go b/x/gov/keeper/vote.go index b554826ac..e4000a1a9 100644 --- a/x/gov/keeper/vote.go +++ b/x/gov/keeper/vote.go @@ -27,6 +27,9 @@ func (keeper Keeper) AddVote(ctx sdk.Context, proposalID uint64, voterAddr sdk.A vote := types.NewVote(proposalID, voterAddr, options) keeper.SetVote(ctx, vote) + // called after a vote on a proposal is cast + keeper.AfterProposalVote(ctx, proposalID, voterAddr) + ctx.EventManager().EmitEvent( sdk.NewEvent( types.EventTypeProposalVote, diff --git a/x/gov/types/expected_keepers.go b/x/gov/types/expected_keepers.go index 1ad39abca..a6e521647 100644 --- a/x/gov/types/expected_keepers.go +++ b/x/gov/types/expected_keepers.go @@ -48,3 +48,16 @@ type BankKeeper interface { SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error BurnCoins(ctx sdk.Context, name string, amt sdk.Coins) error } + +// Event Hooks +// These can be utilized to communicate between a governance keeper and another +// keepers. + +// GovHooks event hooks for governance proposal object (noalias) +type GovHooks interface { + AfterProposalSubmission(ctx sdk.Context, proposalID uint64) // Must be called after proposal is submitted + AfterProposalDeposit(ctx sdk.Context, proposalID uint64, depositorAddr sdk.AccAddress) // Must be called after a deposit is made + AfterProposalVote(ctx sdk.Context, proposalID uint64, voterAddr sdk.AccAddress) // Must be called after a vote on a proposal is cast + AfterProposalFailedMinDeposit(ctx sdk.Context, proposalID uint64) // Must be called when proposal fails to reach min deposit + AfterProposalVotingPeriodEnded(ctx sdk.Context, proposalID uint64) // Must be called when proposal's finishes it's voting period +} diff --git a/x/gov/types/hooks.go b/x/gov/types/hooks.go new file mode 100644 index 000000000..e90b19a08 --- /dev/null +++ b/x/gov/types/hooks.go @@ -0,0 +1,42 @@ +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +var _ GovHooks = MultiGovHooks{} + +// combine multiple governance hooks, all hook functions are run in array sequence +type MultiGovHooks []GovHooks + +func NewMultiGovHooks(hooks ...GovHooks) MultiGovHooks { + return hooks +} + +func (h MultiGovHooks) AfterProposalSubmission(ctx sdk.Context, proposalID uint64) { + for i := range h { + h[i].AfterProposalSubmission(ctx, proposalID) + } +} + +func (h MultiGovHooks) AfterProposalDeposit(ctx sdk.Context, proposalID uint64, depositorAddr sdk.AccAddress) { + for i := range h { + h[i].AfterProposalDeposit(ctx, proposalID, depositorAddr) + } +} + +func (h MultiGovHooks) AfterProposalVote(ctx sdk.Context, proposalID uint64, voterAddr sdk.AccAddress) { + for i := range h { + h[i].AfterProposalVote(ctx, proposalID, voterAddr) + } +} +func (h MultiGovHooks) AfterProposalFailedMinDeposit(ctx sdk.Context, proposalID uint64) { + for i := range h { + h[i].AfterProposalFailedMinDeposit(ctx, proposalID) + } +} +func (h MultiGovHooks) AfterProposalVotingPeriodEnded(ctx sdk.Context, proposalID uint64) { + for i := range h { + h[i].AfterProposalVotingPeriodEnded(ctx, proposalID) + } +}