Skip to content

Commit bffcae5

Browse files
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 <[email protected]>
1 parent 603e895 commit bffcae5

12 files changed

+240
-1
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
4343
* (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.
4444
* [\#8754](https://github.com/cosmos/cosmos-sdk/pull/8875) Added support for reverse iteration to pagination.
4545
* [#9088](https://github.com/cosmos/cosmos-sdk/pull/9088) Added implementation to ADR-28 Derived Addresses.
46+
* [\#9133](https://github.com/cosmos/cosmos-sdk/pull/9133) Added hooks for governance actions.
4647

4748
### Client Breaking Changes
4849
* [\#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.

simapp/app.go

+7-1
Original file line numberDiff line numberDiff line change
@@ -273,11 +273,17 @@ func NewSimApp(
273273
AddRoute(paramproposal.RouterKey, params.NewParamChangeProposalHandler(app.ParamsKeeper)).
274274
AddRoute(distrtypes.RouterKey, distr.NewCommunityPoolSpendProposalHandler(app.DistrKeeper)).
275275
AddRoute(upgradetypes.RouterKey, upgrade.NewSoftwareUpgradeProposalHandler(app.UpgradeKeeper))
276-
app.GovKeeper = govkeeper.NewKeeper(
276+
govKeeper := govkeeper.NewKeeper(
277277
appCodec, keys[govtypes.StoreKey], app.GetSubspace(govtypes.ModuleName), app.AccountKeeper, app.BankKeeper,
278278
&stakingKeeper, govRouter,
279279
)
280280

281+
app.GovKeeper = *govKeeper.SetHooks(
282+
govtypes.NewMultiGovHooks(
283+
// register the governance hooks
284+
),
285+
)
286+
281287
// create evidence keeper with router
282288
evidenceKeeper := evidencekeeper.NewKeeper(
283289
appCodec, keys[evidencetypes.StoreKey], &app.StakingKeeper, app.SlashingKeeper,

x/gov/abci.go

+6
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ func EndBlocker(ctx sdk.Context, keeper keeper.Keeper) {
2121
keeper.DeleteProposal(ctx, proposal.ProposalId)
2222
keeper.DeleteDeposits(ctx, proposal.ProposalId)
2323

24+
// called when proposal become inactive
25+
keeper.AfterProposalFailedMinDeposit(ctx, proposal.ProposalId)
26+
2427
ctx.EventManager().EmitEvent(
2528
sdk.NewEvent(
2629
types.EventTypeInactiveProposal,
@@ -89,6 +92,9 @@ func EndBlocker(ctx sdk.Context, keeper keeper.Keeper) {
8992
keeper.SetProposal(ctx, proposal)
9093
keeper.RemoveFromActiveProposalQueue(ctx, proposal.ProposalId, proposal.VotingEndTime)
9194

95+
// when proposal become active
96+
keeper.AfterProposalVotingPeriodEnded(ctx, proposal.ProposalId)
97+
9298
logger.Info(
9399
"proposal tallied",
94100
"proposal", proposal.ProposalId,

x/gov/keeper/deposit.go

+3
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,9 @@ func (keeper Keeper) AddDeposit(ctx sdk.Context, proposalID uint64, depositorAdd
150150
deposit = types.NewDeposit(proposalID, depositorAddr, depositAmount)
151151
}
152152

153+
// called when deposit has been added to a proposal, however the proposal may not be active
154+
keeper.AfterProposalDeposit(ctx, proposalID, depositorAddr)
155+
153156
ctx.EventManager().EmitEvent(
154157
sdk.NewEvent(
155158
types.EventTypeProposalDeposit,

x/gov/keeper/hooks.go

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package keeper
2+
3+
import (
4+
sdk "github.com/cosmos/cosmos-sdk/types"
5+
"github.com/cosmos/cosmos-sdk/x/gov/types"
6+
)
7+
8+
// Implements GovHooks interface
9+
var _ types.GovHooks = Keeper{}
10+
11+
// AfterProposalSubmission - call hook if registered
12+
func (keeper Keeper) AfterProposalSubmission(ctx sdk.Context, proposalID uint64) {
13+
if keeper.hooks != nil {
14+
keeper.hooks.AfterProposalSubmission(ctx, proposalID)
15+
}
16+
}
17+
18+
// AfterProposalDeposit - call hook if registered
19+
func (keeper Keeper) AfterProposalDeposit(ctx sdk.Context, proposalID uint64, depositorAddr sdk.AccAddress) {
20+
if keeper.hooks != nil {
21+
keeper.hooks.AfterProposalDeposit(ctx, proposalID, depositorAddr)
22+
}
23+
}
24+
25+
// AfterProposalVote - call hook if registered
26+
func (keeper Keeper) AfterProposalVote(ctx sdk.Context, proposalID uint64, voterAddr sdk.AccAddress) {
27+
if keeper.hooks != nil {
28+
keeper.hooks.AfterProposalVote(ctx, proposalID, voterAddr)
29+
}
30+
}
31+
32+
// AfterProposalFailedMinDeposit - call hook if registered
33+
func (keeper Keeper) AfterProposalFailedMinDeposit(ctx sdk.Context, proposalID uint64) {
34+
if keeper.hooks != nil {
35+
keeper.hooks.AfterProposalFailedMinDeposit(ctx, proposalID)
36+
}
37+
}
38+
39+
// AfterProposalVotingPeriodEnded - call hook if registered
40+
func (keeper Keeper) AfterProposalVotingPeriodEnded(ctx sdk.Context, proposalID uint64) {
41+
if keeper.hooks != nil {
42+
keeper.hooks.AfterProposalVotingPeriodEnded(ctx, proposalID)
43+
}
44+
}

x/gov/keeper/hooks_test.go

+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package keeper_test
2+
3+
import (
4+
"testing"
5+
"time"
6+
7+
"github.com/stretchr/testify/require"
8+
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
9+
10+
"github.com/cosmos/cosmos-sdk/simapp"
11+
sdk "github.com/cosmos/cosmos-sdk/types"
12+
"github.com/cosmos/cosmos-sdk/x/gov"
13+
"github.com/cosmos/cosmos-sdk/x/gov/keeper"
14+
"github.com/cosmos/cosmos-sdk/x/gov/types"
15+
)
16+
17+
var _ types.GovHooks = &MockGovHooksReceiver{}
18+
19+
// GovHooks event hooks for governance proposal object (noalias)
20+
type MockGovHooksReceiver struct {
21+
AfterProposalSubmissionValid bool
22+
AfterProposalDepositValid bool
23+
AfterProposalVoteValid bool
24+
AfterProposalFailedMinDepositValid bool
25+
AfterProposalVotingPeriodEndedValid bool
26+
}
27+
28+
func (h *MockGovHooksReceiver) AfterProposalSubmission(ctx sdk.Context, proposalID uint64) {
29+
h.AfterProposalSubmissionValid = true
30+
}
31+
32+
func (h *MockGovHooksReceiver) AfterProposalDeposit(ctx sdk.Context, proposalID uint64, depositorAddr sdk.AccAddress) {
33+
h.AfterProposalDepositValid = true
34+
}
35+
36+
func (h *MockGovHooksReceiver) AfterProposalVote(ctx sdk.Context, proposalID uint64, voterAddr sdk.AccAddress) {
37+
h.AfterProposalVoteValid = true
38+
}
39+
func (h *MockGovHooksReceiver) AfterProposalFailedMinDeposit(ctx sdk.Context, proposalID uint64) {
40+
h.AfterProposalFailedMinDepositValid = true
41+
}
42+
func (h *MockGovHooksReceiver) AfterProposalVotingPeriodEnded(ctx sdk.Context, proposalID uint64) {
43+
h.AfterProposalVotingPeriodEndedValid = true
44+
}
45+
46+
func TestHooks(t *testing.T) {
47+
app := simapp.Setup(false)
48+
ctx := app.BaseApp.NewContext(false, tmproto.Header{})
49+
50+
minDeposit := app.GovKeeper.GetDepositParams(ctx).MinDeposit
51+
addrs := simapp.AddTestAddrs(app, ctx, 1, minDeposit[0].Amount)
52+
53+
govHooksReceiver := MockGovHooksReceiver{}
54+
55+
app.GovKeeper = *keeper.UpdateHooks(&app.GovKeeper,
56+
types.NewMultiGovHooks(
57+
&govHooksReceiver,
58+
),
59+
)
60+
61+
require.False(t, govHooksReceiver.AfterProposalSubmissionValid)
62+
require.False(t, govHooksReceiver.AfterProposalDepositValid)
63+
require.False(t, govHooksReceiver.AfterProposalVoteValid)
64+
require.False(t, govHooksReceiver.AfterProposalFailedMinDepositValid)
65+
require.False(t, govHooksReceiver.AfterProposalVotingPeriodEndedValid)
66+
67+
tp := TestProposal
68+
_, err := app.GovKeeper.SubmitProposal(ctx, tp)
69+
require.NoError(t, err)
70+
require.True(t, govHooksReceiver.AfterProposalSubmissionValid)
71+
72+
newHeader := ctx.BlockHeader()
73+
newHeader.Time = ctx.BlockHeader().Time.Add(app.GovKeeper.GetDepositParams(ctx).MaxDepositPeriod).Add(time.Duration(1) * time.Second)
74+
ctx = ctx.WithBlockHeader(newHeader)
75+
gov.EndBlocker(ctx, app.GovKeeper)
76+
77+
require.True(t, govHooksReceiver.AfterProposalFailedMinDepositValid)
78+
79+
p2, err := app.GovKeeper.SubmitProposal(ctx, tp)
80+
require.NoError(t, err)
81+
82+
activated, err := app.GovKeeper.AddDeposit(ctx, p2.ProposalId, addrs[0], minDeposit)
83+
require.True(t, activated)
84+
require.NoError(t, err)
85+
require.True(t, govHooksReceiver.AfterProposalDepositValid)
86+
87+
err = app.GovKeeper.AddVote(ctx, p2.ProposalId, addrs[0], types.NewNonSplitVoteOption(types.OptionYes))
88+
require.NoError(t, err)
89+
require.True(t, govHooksReceiver.AfterProposalVoteValid)
90+
91+
newHeader = ctx.BlockHeader()
92+
newHeader.Time = ctx.BlockHeader().Time.Add(app.GovKeeper.GetVotingParams(ctx).VotingPeriod).Add(time.Duration(1) * time.Second)
93+
ctx = ctx.WithBlockHeader(newHeader)
94+
gov.EndBlocker(ctx, app.GovKeeper)
95+
require.True(t, govHooksReceiver.AfterProposalVotingPeriodEndedValid)
96+
}

x/gov/keeper/internal_test.go

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package keeper
2+
3+
import "github.com/cosmos/cosmos-sdk/x/gov/types"
4+
5+
func UpdateHooks(k *Keeper, h types.GovHooks) *Keeper {
6+
k.hooks = h
7+
return k
8+
}

x/gov/keeper/keeper.go

+14
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ type Keeper struct {
2323
// The reference to the DelegationSet and ValidatorSet to get information about validators and delegators
2424
sk types.StakingKeeper
2525

26+
// GovHooks
27+
hooks types.GovHooks
28+
2629
// The (unexposed) keys used to access the stores from the Context.
2730
storeKey sdk.StoreKey
2831

@@ -66,6 +69,17 @@ func NewKeeper(
6669
}
6770
}
6871

72+
// SetHooks sets the hooks for governance
73+
func (keeper *Keeper) SetHooks(gh types.GovHooks) *Keeper {
74+
if keeper.hooks != nil {
75+
panic("cannot set governance hooks twice")
76+
}
77+
78+
keeper.hooks = gh
79+
80+
return keeper
81+
}
82+
6983
// Logger returns a module-specific logger.
7084
func (keeper Keeper) Logger(ctx sdk.Context) log.Logger {
7185
return ctx.Logger().With("module", "x/"+types.ModuleName)

x/gov/keeper/proposal.go

+3
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ func (keeper Keeper) SubmitProposal(ctx sdk.Context, content types.Content) (typ
4141
keeper.InsertInactiveProposalQueue(ctx, proposalID, proposal.DepositEndTime)
4242
keeper.SetProposalID(ctx, proposalID+1)
4343

44+
// called right after a proposal is submitted
45+
keeper.AfterProposalSubmission(ctx, proposalID)
46+
4447
ctx.EventManager().EmitEvent(
4548
sdk.NewEvent(
4649
types.EventTypeSubmitProposal,

x/gov/keeper/vote.go

+3
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ func (keeper Keeper) AddVote(ctx sdk.Context, proposalID uint64, voterAddr sdk.A
2727
vote := types.NewVote(proposalID, voterAddr, options)
2828
keeper.SetVote(ctx, vote)
2929

30+
// called after a vote on a proposal is cast
31+
keeper.AfterProposalVote(ctx, proposalID, voterAddr)
32+
3033
ctx.EventManager().EmitEvent(
3134
sdk.NewEvent(
3235
types.EventTypeProposalVote,

x/gov/types/expected_keepers.go

+13
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,16 @@ type BankKeeper interface {
4848
SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error
4949
BurnCoins(ctx sdk.Context, name string, amt sdk.Coins) error
5050
}
51+
52+
// Event Hooks
53+
// These can be utilized to communicate between a governance keeper and another
54+
// keepers.
55+
56+
// GovHooks event hooks for governance proposal object (noalias)
57+
type GovHooks interface {
58+
AfterProposalSubmission(ctx sdk.Context, proposalID uint64) // Must be called after proposal is submitted
59+
AfterProposalDeposit(ctx sdk.Context, proposalID uint64, depositorAddr sdk.AccAddress) // Must be called after a deposit is made
60+
AfterProposalVote(ctx sdk.Context, proposalID uint64, voterAddr sdk.AccAddress) // Must be called after a vote on a proposal is cast
61+
AfterProposalFailedMinDeposit(ctx sdk.Context, proposalID uint64) // Must be called when proposal fails to reach min deposit
62+
AfterProposalVotingPeriodEnded(ctx sdk.Context, proposalID uint64) // Must be called when proposal's finishes it's voting period
63+
}

x/gov/types/hooks.go

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package types
2+
3+
import (
4+
sdk "github.com/cosmos/cosmos-sdk/types"
5+
)
6+
7+
var _ GovHooks = MultiGovHooks{}
8+
9+
// combine multiple governance hooks, all hook functions are run in array sequence
10+
type MultiGovHooks []GovHooks
11+
12+
func NewMultiGovHooks(hooks ...GovHooks) MultiGovHooks {
13+
return hooks
14+
}
15+
16+
func (h MultiGovHooks) AfterProposalSubmission(ctx sdk.Context, proposalID uint64) {
17+
for i := range h {
18+
h[i].AfterProposalSubmission(ctx, proposalID)
19+
}
20+
}
21+
22+
func (h MultiGovHooks) AfterProposalDeposit(ctx sdk.Context, proposalID uint64, depositorAddr sdk.AccAddress) {
23+
for i := range h {
24+
h[i].AfterProposalDeposit(ctx, proposalID, depositorAddr)
25+
}
26+
}
27+
28+
func (h MultiGovHooks) AfterProposalVote(ctx sdk.Context, proposalID uint64, voterAddr sdk.AccAddress) {
29+
for i := range h {
30+
h[i].AfterProposalVote(ctx, proposalID, voterAddr)
31+
}
32+
}
33+
func (h MultiGovHooks) AfterProposalFailedMinDeposit(ctx sdk.Context, proposalID uint64) {
34+
for i := range h {
35+
h[i].AfterProposalFailedMinDeposit(ctx, proposalID)
36+
}
37+
}
38+
func (h MultiGovHooks) AfterProposalVotingPeriodEnded(ctx sdk.Context, proposalID uint64) {
39+
for i := range h {
40+
h[i].AfterProposalVotingPeriodEnded(ctx, proposalID)
41+
}
42+
}

0 commit comments

Comments
 (0)