x/committee: committee gov module

Committee Gov Module
This commit is contained in:
Federico Kunze 2020-04-30 09:16:33 -04:00 committed by GitHub
commit e9c16fa752
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 5231 additions and 13 deletions

View File

@ -4,14 +4,6 @@ import (
"io" "io"
"os" "os"
"github.com/kava-labs/kava/x/auction"
"github.com/kava-labs/kava/x/bep3"
"github.com/kava-labs/kava/x/cdp"
"github.com/kava-labs/kava/x/incentive"
"github.com/kava-labs/kava/x/kavadist"
"github.com/kava-labs/kava/x/pricefeed"
validatorvesting "github.com/kava-labs/kava/x/validator-vesting"
abci "github.com/tendermint/tendermint/abci/types" abci "github.com/tendermint/tendermint/abci/types"
"github.com/tendermint/tendermint/libs/log" "github.com/tendermint/tendermint/libs/log"
tmos "github.com/tendermint/tendermint/libs/os" tmos "github.com/tendermint/tendermint/libs/os"
@ -37,6 +29,15 @@ import (
"github.com/cosmos/cosmos-sdk/x/slashing" "github.com/cosmos/cosmos-sdk/x/slashing"
"github.com/cosmos/cosmos-sdk/x/staking" "github.com/cosmos/cosmos-sdk/x/staking"
"github.com/cosmos/cosmos-sdk/x/supply" "github.com/cosmos/cosmos-sdk/x/supply"
"github.com/kava-labs/kava/x/auction"
"github.com/kava-labs/kava/x/bep3"
"github.com/kava-labs/kava/x/cdp"
"github.com/kava-labs/kava/x/committee"
"github.com/kava-labs/kava/x/incentive"
"github.com/kava-labs/kava/x/kavadist"
"github.com/kava-labs/kava/x/pricefeed"
validatorvesting "github.com/kava-labs/kava/x/validator-vesting"
) )
const ( const (
@ -59,7 +60,7 @@ var (
staking.AppModuleBasic{}, staking.AppModuleBasic{},
mint.AppModuleBasic{}, mint.AppModuleBasic{},
distr.AppModuleBasic{}, distr.AppModuleBasic{},
gov.NewAppModuleBasic(paramsclient.ProposalHandler, distr.ProposalHandler), gov.NewAppModuleBasic(paramsclient.ProposalHandler, distr.ProposalHandler, committee.ProposalHandler),
params.AppModuleBasic{}, params.AppModuleBasic{},
crisis.AppModuleBasic{}, crisis.AppModuleBasic{},
slashing.AppModuleBasic{}, slashing.AppModuleBasic{},
@ -68,6 +69,7 @@ var (
auction.AppModuleBasic{}, auction.AppModuleBasic{},
cdp.AppModuleBasic{}, cdp.AppModuleBasic{},
pricefeed.AppModuleBasic{}, pricefeed.AppModuleBasic{},
committee.AppModuleBasic{},
bep3.AppModuleBasic{}, bep3.AppModuleBasic{},
kavadist.AppModuleBasic{}, kavadist.AppModuleBasic{},
incentive.AppModuleBasic{}, incentive.AppModuleBasic{},
@ -121,6 +123,7 @@ type App struct {
auctionKeeper auction.Keeper auctionKeeper auction.Keeper
cdpKeeper cdp.Keeper cdpKeeper cdp.Keeper
pricefeedKeeper pricefeed.Keeper pricefeedKeeper pricefeed.Keeper
committeeKeeper committee.Keeper
bep3Keeper bep3.Keeper bep3Keeper bep3.Keeper
kavadistKeeper kavadist.Keeper kavadistKeeper kavadist.Keeper
incentiveKeeper incentive.Keeper incentiveKeeper incentive.Keeper
@ -148,7 +151,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
supply.StoreKey, mint.StoreKey, distr.StoreKey, slashing.StoreKey, supply.StoreKey, mint.StoreKey, distr.StoreKey, slashing.StoreKey,
gov.StoreKey, params.StoreKey, evidence.StoreKey, validatorvesting.StoreKey, gov.StoreKey, params.StoreKey, evidence.StoreKey, validatorvesting.StoreKey,
auction.StoreKey, cdp.StoreKey, pricefeed.StoreKey, bep3.StoreKey, auction.StoreKey, cdp.StoreKey, pricefeed.StoreKey, bep3.StoreKey,
kavadist.StoreKey, incentive.StoreKey, kavadist.StoreKey, incentive.StoreKey, committee.StoreKey,
) )
tkeys := sdk.NewTransientStoreKeys(params.TStoreKey) tkeys := sdk.NewTransientStoreKeys(params.TStoreKey)
@ -245,11 +248,27 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
evidenceKeeper.SetRouter(evidenceRouter) evidenceKeeper.SetRouter(evidenceRouter)
app.evidenceKeeper = *evidenceKeeper app.evidenceKeeper = *evidenceKeeper
// create committee keeper with router
committeeGovRouter := gov.NewRouter()
committeeGovRouter.
AddRoute(gov.RouterKey, gov.ProposalHandler).
AddRoute(params.RouterKey, params.NewParamChangeProposalHandler(app.paramsKeeper)).
AddRoute(distr.RouterKey, distr.NewCommunityPoolSpendProposalHandler(app.distrKeeper))
// Note: the committee proposal handler is not registered on the committee router. This means committees cannot create or update other committees.
// Adding the committee proposal handler to the router is possible but awkward as the handler depends on the keeper which depends on the handler.
app.committeeKeeper = committee.NewKeeper(
app.cdc,
keys[committee.StoreKey],
committeeGovRouter, // TODO blacklist module addresses?
)
// create gov keeper with router
govRouter := gov.NewRouter() govRouter := gov.NewRouter()
govRouter. govRouter.
AddRoute(gov.RouterKey, gov.ProposalHandler). AddRoute(gov.RouterKey, gov.ProposalHandler).
AddRoute(params.RouterKey, params.NewParamChangeProposalHandler(app.paramsKeeper)). AddRoute(params.RouterKey, params.NewParamChangeProposalHandler(app.paramsKeeper)).
AddRoute(distr.RouterKey, distr.NewCommunityPoolSpendProposalHandler(app.distrKeeper)) AddRoute(distr.RouterKey, distr.NewCommunityPoolSpendProposalHandler(app.distrKeeper)).
AddRoute(committee.RouterKey, committee.NewProposalHandler(app.committeeKeeper))
app.govKeeper = gov.NewKeeper( app.govKeeper = gov.NewKeeper(
app.cdc, app.cdc,
keys[gov.StoreKey], keys[gov.StoreKey],
@ -334,12 +353,15 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
bep3.NewAppModule(app.bep3Keeper, app.accountKeeper, app.supplyKeeper), bep3.NewAppModule(app.bep3Keeper, app.accountKeeper, app.supplyKeeper),
kavadist.NewAppModule(app.kavadistKeeper, app.supplyKeeper), kavadist.NewAppModule(app.kavadistKeeper, app.supplyKeeper),
incentive.NewAppModule(app.incentiveKeeper, app.accountKeeper, app.supplyKeeper), incentive.NewAppModule(app.incentiveKeeper, app.accountKeeper, app.supplyKeeper),
committee.NewAppModule(app.committeeKeeper, app.accountKeeper),
) )
// During begin block slashing happens after distr.BeginBlocker so that // During begin block slashing happens after distr.BeginBlocker so that
// there is nothing left over in the validator fee pool, so as to keep the // there is nothing left over in the validator fee pool, so as to keep the
// CanWithdrawInvariant invariant. // CanWithdrawInvariant invariant.
app.mm.SetOrderBeginBlockers(mint.ModuleName, distr.ModuleName, slashing.ModuleName, validatorvesting.ModuleName, kavadist.ModuleName, cdp.ModuleName, auction.ModuleName, bep3.ModuleName, incentive.ModuleName) // Auction.BeginBlocker will close out expired auctions and pay debt back to cdp.
// So it should be run before cdp.BeginBlocker which cancels out debt with stable and starts more auctions.
app.mm.SetOrderBeginBlockers(mint.ModuleName, distr.ModuleName, slashing.ModuleName, validatorvesting.ModuleName, kavadist.ModuleName, auction.ModuleName, cdp.ModuleName, bep3.ModuleName, incentive.ModuleName, committee.ModuleName)
app.mm.SetOrderEndBlockers(crisis.ModuleName, gov.ModuleName, staking.ModuleName, pricefeed.ModuleName) app.mm.SetOrderEndBlockers(crisis.ModuleName, gov.ModuleName, staking.ModuleName, pricefeed.ModuleName)
@ -348,7 +370,8 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
validatorvesting.ModuleName, distr.ModuleName, validatorvesting.ModuleName, distr.ModuleName,
staking.ModuleName, bank.ModuleName, slashing.ModuleName, staking.ModuleName, bank.ModuleName, slashing.ModuleName,
gov.ModuleName, mint.ModuleName, evidence.ModuleName, gov.ModuleName, mint.ModuleName, evidence.ModuleName,
pricefeed.ModuleName, cdp.ModuleName, auction.ModuleName, bep3.ModuleName, kavadist.ModuleName, incentive.ModuleName, pricefeed.ModuleName, cdp.ModuleName, auction.ModuleName,
bep3.ModuleName, kavadist.ModuleName, incentive.ModuleName, committee.ModuleName,
supply.ModuleName, // calculates the total supply from account - should run after modules that modify accounts in genesis supply.ModuleName, // calculates the total supply from account - should run after modules that modify accounts in genesis
crisis.ModuleName, // runs the invariants at genesis - should run after other modules crisis.ModuleName, // runs the invariants at genesis - should run after other modules
genutil.ModuleName, // genutils must occur after staking so that pools are properly initialized with tokens from genesis accounts. genutil.ModuleName, // genutils must occur after staking so that pools are properly initialized with tokens from genesis accounts.

View File

@ -30,6 +30,7 @@ import (
"github.com/kava-labs/kava/x/auction" "github.com/kava-labs/kava/x/auction"
"github.com/kava-labs/kava/x/bep3" "github.com/kava-labs/kava/x/bep3"
"github.com/kava-labs/kava/x/cdp" "github.com/kava-labs/kava/x/cdp"
"github.com/kava-labs/kava/x/committee"
"github.com/kava-labs/kava/x/incentive" "github.com/kava-labs/kava/x/incentive"
"github.com/kava-labs/kava/x/kavadist" "github.com/kava-labs/kava/x/kavadist"
"github.com/kava-labs/kava/x/pricefeed" "github.com/kava-labs/kava/x/pricefeed"
@ -77,6 +78,7 @@ func (tApp TestApp) GetPriceFeedKeeper() pricefeed.Keeper { return tApp.pricefee
func (tApp TestApp) GetBep3Keeper() bep3.Keeper { return tApp.bep3Keeper } func (tApp TestApp) GetBep3Keeper() bep3.Keeper { return tApp.bep3Keeper }
func (tApp TestApp) GetKavadistKeeper() kavadist.Keeper { return tApp.kavadistKeeper } func (tApp TestApp) GetKavadistKeeper() kavadist.Keeper { return tApp.kavadistKeeper }
func (tApp TestApp) GetIncentiveKeeper() incentive.Keeper { return tApp.incentiveKeeper } func (tApp TestApp) GetIncentiveKeeper() incentive.Keeper { return tApp.incentiveKeeper }
func (tApp TestApp) GetCommitteeKeeper() committee.Keeper { return tApp.committeeKeeper }
// This calls InitChain on the app using the default genesis state, overwitten with any passed in genesis states // This calls InitChain on the app using the default genesis state, overwitten with any passed in genesis states
func (tApp TestApp) InitializeFromGenesisStates(genesisStates ...GenesisState) TestApp { func (tApp TestApp) InitializeFromGenesisStates(genesisStates ...GenesisState) TestApp {

12
x/committee/abci.go Normal file
View File

@ -0,0 +1,12 @@
package committee
import (
sdk "github.com/cosmos/cosmos-sdk/types"
abci "github.com/tendermint/tendermint/abci/types"
)
// BeginBlocker runs at the start of every block.
func BeginBlocker(ctx sdk.Context, _ abci.RequestBeginBlock, k Keeper) {
k.CloseExpiredProposals(ctx)
}

70
x/committee/abci_test.go Normal file
View File

@ -0,0 +1,70 @@
package committee_test
import (
"testing"
"time"
"github.com/stretchr/testify/suite"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/gov"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/committee"
)
type ModuleTestSuite struct {
suite.Suite
keeper committee.Keeper
app app.TestApp
ctx sdk.Context
addresses []sdk.AccAddress
}
func (suite *ModuleTestSuite) SetupTest() {
suite.app = app.NewTestApp()
suite.keeper = suite.app.GetCommitteeKeeper()
suite.ctx = suite.app.NewContext(true, abci.Header{})
_, suite.addresses = app.GeneratePrivKeyAddressPairs(5)
}
func (suite *ModuleTestSuite) TestBeginBlock() {
suite.app.InitializeFromGenesisStates()
normalCom := committee.Committee{
ID: 12,
Members: suite.addresses[:2],
Permissions: []committee.Permission{committee.GodPermission{}},
VoteThreshold: d("0.8"),
ProposalDuration: time.Hour * 24 * 7,
}
suite.keeper.SetCommittee(suite.ctx, normalCom)
pprop1 := gov.NewTextProposal("1A Title", "A description of this proposal.")
id1, err := suite.keeper.SubmitProposal(suite.ctx, normalCom.Members[0], normalCom.ID, pprop1)
suite.NoError(err)
oneHrLaterCtx := suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(time.Hour))
pprop2 := gov.NewTextProposal("2A Title", "A description of this proposal.")
id2, err := suite.keeper.SubmitProposal(oneHrLaterCtx, normalCom.Members[0], normalCom.ID, pprop2)
suite.NoError(err)
// Run BeginBlocker
proposalDurationLaterCtx := suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(normalCom.ProposalDuration))
suite.NotPanics(func() {
committee.BeginBlocker(proposalDurationLaterCtx, abci.RequestBeginBlock{}, suite.keeper)
})
// Check expired proposals are gone
_, found := suite.keeper.GetProposal(suite.ctx, id1)
suite.False(found, "expected expired proposal to be closed")
_, found = suite.keeper.GetProposal(suite.ctx, id2)
suite.True(found, "expected non expired proposal to be not closed")
}
func TestModuleTestSuite(t *testing.T) {
suite.Run(t, new(ModuleTestSuite))
}

101
x/committee/alias.go Normal file
View File

@ -0,0 +1,101 @@
package committee
// DO NOT EDIT - generated by aliasgen tool (github.com/rhuairahrighairidh/aliasgen)
import (
"github.com/kava-labs/kava/x/committee/client"
"github.com/kava-labs/kava/x/committee/keeper"
"github.com/kava-labs/kava/x/committee/types"
)
const (
AttributeKeyCommitteeID = types.AttributeKeyCommitteeID
AttributeKeyProposalCloseStatus = types.AttributeKeyProposalCloseStatus
AttributeKeyProposalID = types.AttributeKeyProposalID
AttributeValueCategory = types.AttributeValueCategory
AttributeValueProposalFailed = types.AttributeValueProposalFailed
AttributeValueProposalPassed = types.AttributeValueProposalPassed
AttributeValueProposalTimeout = types.AttributeValueProposalTimeout
DefaultNextProposalID = types.DefaultNextProposalID
DefaultParamspace = types.DefaultParamspace
EventTypeProposalClose = types.EventTypeProposalClose
EventTypeProposalSubmit = types.EventTypeProposalSubmit
EventTypeProposalVote = types.EventTypeProposalVote
MaxCommitteeDescriptionLength = types.MaxCommitteeDescriptionLength
ModuleName = types.ModuleName
ProposalTypeCommitteeChange = types.ProposalTypeCommitteeChange
ProposalTypeCommitteeDelete = types.ProposalTypeCommitteeDelete
QuerierRoute = types.QuerierRoute
QueryCommittee = types.QueryCommittee
QueryCommittees = types.QueryCommittees
QueryProposal = types.QueryProposal
QueryProposals = types.QueryProposals
QueryTally = types.QueryTally
QueryVote = types.QueryVote
QueryVotes = types.QueryVotes
RouterKey = types.RouterKey
StoreKey = types.StoreKey
TypeMsgSubmitProposal = types.TypeMsgSubmitProposal
TypeMsgVote = types.TypeMsgVote
)
var (
// function aliases
NewKeeper = keeper.NewKeeper
NewQuerier = keeper.NewQuerier
DefaultGenesisState = types.DefaultGenesisState
GetKeyFromID = types.GetKeyFromID
GetVoteKey = types.GetVoteKey
NewCommittee = types.NewCommittee
NewCommitteeChangeProposal = types.NewCommitteeChangeProposal
NewCommitteeDeleteProposal = types.NewCommitteeDeleteProposal
NewGenesisState = types.NewGenesisState
NewMsgSubmitProposal = types.NewMsgSubmitProposal
NewMsgVote = types.NewMsgVote
NewProposal = types.NewProposal
NewQueryCommitteeParams = types.NewQueryCommitteeParams
NewQueryProposalParams = types.NewQueryProposalParams
NewQueryVoteParams = types.NewQueryVoteParams
RegisterCodec = types.RegisterCodec
RegisterPermissionTypeCodec = types.RegisterPermissionTypeCodec
RegisterProposalTypeCodec = types.RegisterProposalTypeCodec
Uint64FromBytes = types.Uint64FromBytes
// variable aliases
ProposalHandler = client.ProposalHandler
CommitteeKeyPrefix = types.CommitteeKeyPrefix
ErrInvalidCommittee = types.ErrInvalidCommittee
ErrInvalidGenesis = types.ErrInvalidGenesis
ErrInvalidPubProposal = types.ErrInvalidPubProposal
ErrNoProposalHandlerExists = types.ErrNoProposalHandlerExists
ErrProposalExpired = types.ErrProposalExpired
ErrUnknownCommittee = types.ErrUnknownCommittee
ErrUnknownProposal = types.ErrUnknownProposal
ErrUnknownVote = types.ErrUnknownVote
ModuleCdc = types.ModuleCdc
NextProposalIDKey = types.NextProposalIDKey
ProposalKeyPrefix = types.ProposalKeyPrefix
VoteKeyPrefix = types.VoteKeyPrefix
)
type (
Keeper = keeper.Keeper
AllowedParam = types.AllowedParam
AllowedParams = types.AllowedParams
Committee = types.Committee
CommitteeChangeProposal = types.CommitteeChangeProposal
CommitteeDeleteProposal = types.CommitteeDeleteProposal
GenesisState = types.GenesisState
GodPermission = types.GodPermission
MsgSubmitProposal = types.MsgSubmitProposal
MsgVote = types.MsgVote
ParamChangePermission = types.ParamChangePermission
Permission = types.Permission
Proposal = types.Proposal
PubProposal = types.PubProposal
QueryCommitteeParams = types.QueryCommitteeParams
QueryProposalParams = types.QueryProposalParams
QueryVoteParams = types.QueryVoteParams
TextPermission = types.TextPermission
Vote = types.Vote
)

View File

@ -0,0 +1,36 @@
package cli_test
import (
"testing"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/stretchr/testify/suite"
"github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/committee/client/cli"
)
type CLITestSuite struct {
suite.Suite
cdc *codec.Codec
}
func (suite *CLITestSuite) SetupTest() {
tApp := app.NewTestApp()
suite.cdc = tApp.Codec()
}
func (suite *CLITestSuite) TestExampleCommitteeChangeProposal() {
suite.NotPanics(func() { cli.MustGetExampleCommitteeChangeProposal(suite.cdc) })
}
func (suite *CLITestSuite) TestExampleCommitteeDeleteProposal() {
suite.NotPanics(func() { cli.MustGetExampleCommitteeDeleteProposal(suite.cdc) })
}
func (suite *CLITestSuite) TestExampleParameterChangeProposal() {
suite.NotPanics(func() { cli.MustGetExampleParameterChangeProposal(suite.cdc) })
}
func TestCLITestSuite(t *testing.T) {
suite.Run(t, new(CLITestSuite))
}

View File

@ -0,0 +1,294 @@
package cli
import (
"fmt"
"strconv"
"github.com/spf13/cobra"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/version"
"github.com/kava-labs/kava/x/committee/client/common"
"github.com/kava-labs/kava/x/committee/types"
)
// GetQueryCmd returns the cli query commands for this module
func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
queryCmd := &cobra.Command{
Use: types.ModuleName,
Short: fmt.Sprintf("Querying commands for the %s module", types.ModuleName),
DisableFlagParsing: true,
SuggestionsMinimumDistance: 2,
RunE: client.ValidateCmd,
}
queryCmd.AddCommand(flags.GetCommands(
// committees
GetCmdQueryCommittee(queryRoute, cdc),
GetCmdQueryCommittees(queryRoute, cdc),
// proposals
GetCmdQueryProposal(queryRoute, cdc),
GetCmdQueryProposals(queryRoute, cdc),
// votes
GetCmdQueryVotes(queryRoute, cdc),
// other
GetCmdQueryProposer(queryRoute, cdc),
GetCmdQueryTally(queryRoute, cdc))...)
return queryCmd
}
// ------------------------------------------
// Committees
// ------------------------------------------
// GetCmdQueryCommittee implements a query committee command.
func GetCmdQueryCommittee(queryRoute string, cdc *codec.Codec) *cobra.Command {
cmd := &cobra.Command{
Use: "committee [committee-id]",
Args: cobra.ExactArgs(1),
Short: "Query details of a single committee",
Example: fmt.Sprintf("%s query %s committee 1", version.ClientName, types.ModuleName),
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
// Prepare params for querier
committeeID, err := strconv.ParseUint(args[0], 10, 64)
if err != nil {
return fmt.Errorf("committee-id %s not a valid uint", args[0])
}
bz, err := cdc.MarshalJSON(types.NewQueryCommitteeParams(committeeID))
if err != nil {
return err
}
// Query
res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryCommittee), bz)
if err != nil {
return err
}
// Decode and print result
committee := types.Committee{}
if err = cdc.UnmarshalJSON(res, &committee); err != nil {
return err
}
return cliCtx.PrintOutput(committee)
},
}
return cmd
}
// GetCmdQueryCommittees implements a query committees command.
func GetCmdQueryCommittees(queryRoute string, cdc *codec.Codec) *cobra.Command {
cmd := &cobra.Command{
Use: "committees",
Args: cobra.NoArgs,
Short: "Query all committees",
Example: fmt.Sprintf("%s query %s committees", version.ClientName, types.ModuleName),
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
// Query
res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryCommittees), nil)
if err != nil {
return err
}
// Decode and print result
committees := []types.Committee{} // using empty (not nil) slice so json output returns "[]"" instead of "null" when there's no data
if err = cdc.UnmarshalJSON(res, &committees); err != nil {
return err
}
return cliCtx.PrintOutput(committees)
},
}
return cmd
}
// ------------------------------------------
// Proposals
// ------------------------------------------
// GetCmdQueryProposal implements the query proposal command.
func GetCmdQueryProposal(queryRoute string, cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "proposal [proposal-id]",
Args: cobra.ExactArgs(1),
Short: "Query details of a single proposal",
Example: fmt.Sprintf("%s query %s proposal 2", version.ClientName, types.ModuleName),
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
// Prepare params for querier
proposalID, err := strconv.ParseUint(args[0], 10, 64)
if err != nil {
return fmt.Errorf("proposal-id %s not a valid uint", args[0])
}
bz, err := cdc.MarshalJSON(types.NewQueryProposalParams(proposalID))
if err != nil {
return err
}
// Query
res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryProposal), bz)
if err != nil {
return err
}
// Decode and print results
var proposal types.Proposal
cdc.MustUnmarshalJSON(res, &proposal)
return cliCtx.PrintOutput(proposal)
},
}
}
// GetCmdQueryProposals implements a query proposals command.
func GetCmdQueryProposals(queryRoute string, cdc *codec.Codec) *cobra.Command {
cmd := &cobra.Command{
Use: "proposals [committee-id]",
Short: "Query all proposals for a committee",
Args: cobra.ExactArgs(1),
Example: fmt.Sprintf("%s query %s proposals 1", version.ClientName, types.ModuleName),
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
// Prepare params for querier
committeeID, err := strconv.ParseUint(args[0], 10, 64)
if err != nil {
return fmt.Errorf("committee-id %s not a valid uint", args[0])
}
bz, err := cdc.MarshalJSON(types.NewQueryCommitteeParams(committeeID))
if err != nil {
return err
}
// Query
res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryProposals), bz)
if err != nil {
return err
}
// Decode and print results
proposals := []types.Proposal{}
err = cdc.UnmarshalJSON(res, &proposals)
if err != nil {
return err
}
return cliCtx.PrintOutput(proposals)
},
}
return cmd
}
// ------------------------------------------
// Votes
// ------------------------------------------
// GetCmdQueryVotes implements the command to query for proposal votes.
func GetCmdQueryVotes(queryRoute string, cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "votes [proposal-id]",
Args: cobra.ExactArgs(1),
Short: "Query votes on a proposal",
Example: fmt.Sprintf("%s query %s votes 2", version.ClientName, types.ModuleName),
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
// Prepare params for querier
proposalID, err := strconv.ParseUint(args[0], 10, 64)
if err != nil {
return fmt.Errorf("proposal-id %s not a valid int", args[0])
}
bz, err := cdc.MarshalJSON(types.NewQueryProposalParams(proposalID))
if err != nil {
return err
}
// Query
res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryVotes), bz)
if err != nil {
return err
}
// Decode and print results
votes := []types.Vote{} // using empty (not nil) slice so json returns [] instead of null when there's no data
err = cdc.UnmarshalJSON(res, &votes)
if err != nil {
return err
}
return cliCtx.PrintOutput(votes)
},
}
}
// ------------------------------------------
// Other
// ------------------------------------------
func GetCmdQueryTally(queryRoute string, cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "tally [proposal-id]",
Args: cobra.ExactArgs(1),
Short: "Get the current tally of votes on a proposal",
Long: "Query the current tally of votes on a proposal to see the progress of the voting.",
Example: fmt.Sprintf("%s query %s tally 2", version.ClientName, types.ModuleName),
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
// Prepare params for querier
proposalID, err := strconv.ParseUint(args[0], 10, 64)
if err != nil {
return fmt.Errorf("proposal-id %s not a valid int", args[0])
}
bz, err := cdc.MarshalJSON(types.NewQueryProposalParams(proposalID))
if err != nil {
return err
}
// Query
res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/tally", queryRoute), bz)
if err != nil {
return err
}
// Decode and print results
var tally bool
if err = cdc.UnmarshalJSON(res, &tally); err != nil {
return err
}
return cliCtx.PrintOutput(tally)
},
}
}
func GetCmdQueryProposer(queryRoute string, cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "proposer [proposal-id]",
Args: cobra.ExactArgs(1),
Short: "Query the proposer of a governance proposal",
Long: "Query which address proposed a proposal with a given ID.",
Example: fmt.Sprintf("%s query %s proposer 2", version.ClientName, types.ModuleName),
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
// validate that the proposalID is a uint
proposalID, err := strconv.ParseUint(args[0], 10, 64)
if err != nil {
return fmt.Errorf("proposal-id %s is not a valid uint", args[0])
}
prop, err := common.QueryProposer(cliCtx, proposalID)
if err != nil {
return err
}
return cliCtx.PrintOutput(prop)
},
}
}

View File

@ -0,0 +1,240 @@
package cli
import (
"bufio"
"fmt"
"io/ioutil"
"strconv"
"time"
"github.com/spf13/cobra"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/version"
"github.com/cosmos/cosmos-sdk/x/auth"
"github.com/cosmos/cosmos-sdk/x/auth/client/utils"
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
"github.com/cosmos/cosmos-sdk/x/params"
"github.com/tendermint/tendermint/crypto"
"github.com/kava-labs/kava/x/committee/types"
)
func GetTxCmd(storeKey string, cdc *codec.Codec) *cobra.Command {
txCmd := &cobra.Command{
Use: types.ModuleName,
Short: "committee governance transactions subcommands",
DisableFlagParsing: true,
SuggestionsMinimumDistance: 2,
RunE: client.ValidateCmd,
}
txCmd.AddCommand(flags.PostCommands(
GetCmdVote(cdc),
GetCmdSubmitProposal(cdc),
)...)
return txCmd
}
// GetCmdSubmitProposal returns the command to submit a proposal to a committee
func GetCmdSubmitProposal(cdc *codec.Codec) *cobra.Command {
cmd := &cobra.Command{
Use: "submit-proposal [committee-id] [proposal-file]",
Short: "Submit a governance proposal to a particular committee",
Long: fmt.Sprintf(`Submit a proposal to a committee so they can vote on it.
The proposal file must be the json encoded forms of the proposal type you want to submit.
For example:
%s
`, MustGetExampleParameterChangeProposal(cdc)),
Args: cobra.ExactArgs(2),
Example: fmt.Sprintf("%s tx %s submit-proposal 1 your-proposal.json", version.ClientName, types.ModuleName),
RunE: func(cmd *cobra.Command, args []string) error {
inBuf := bufio.NewReader(cmd.InOrStdin())
txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc))
cliCtx := context.NewCLIContext().WithCodec(cdc)
// Get proposing address
proposer := cliCtx.GetFromAddress()
// Get committee ID
committeeID, err := strconv.ParseUint(args[0], 10, 64)
if err != nil {
return fmt.Errorf("committee-id %s not a valid int", args[0])
}
// Get the proposal
bz, err := ioutil.ReadFile(args[1])
if err != nil {
return err
}
var pubProposal types.PubProposal
if err := cdc.UnmarshalJSON(bz, &pubProposal); err != nil {
return err
}
if err = pubProposal.ValidateBasic(); err != nil {
return err
}
// Build message and run basic validation
msg := types.NewMsgSubmitProposal(pubProposal, proposer, committeeID)
err = msg.ValidateBasic()
if err != nil {
return err
}
// Sign and broadcast message
return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
},
}
return cmd
}
// GetCmdVote returns the command to vote on a proposal.
func GetCmdVote(cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "vote [proposal-id]",
Args: cobra.ExactArgs(1),
Short: "Vote for an active proposal",
Long: "Submit a yes vote for the proposal with id [proposal-id].",
Example: fmt.Sprintf("%s tx %s vote 2", version.ClientName, types.ModuleName),
RunE: func(cmd *cobra.Command, args []string) error {
inBuf := bufio.NewReader(cmd.InOrStdin())
txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc))
cliCtx := context.NewCLIContext().WithCodec(cdc)
// Get voting address
from := cliCtx.GetFromAddress()
// validate that the proposal id is a uint
proposalID, err := strconv.ParseUint(args[0], 10, 64)
if err != nil {
return fmt.Errorf("proposal-id %s not a valid int, please input a valid proposal-id", args[0])
}
// Build vote message and run basic validation
msg := types.NewMsgVote(from, proposalID)
err = msg.ValidateBasic()
if err != nil {
return err
}
return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
},
}
}
// GetGovCmdSubmitProposal returns a command to submit a proposal to the gov module. It is passed to the gov module for use on its command subtree.
func GetGovCmdSubmitProposal(cdc *codec.Codec) *cobra.Command {
cmd := &cobra.Command{
Use: "committee [proposal-file] [deposit]",
Short: "Submit a governance proposal to change a committee.",
Long: fmt.Sprintf(`Submit a governance proposal to create, alter, or delete a committee.
The proposal file must be the json encoded form of the proposal type you want to submit.
For example, to create or update a committee:
%s
and to delete a committee:
%s
`, MustGetExampleCommitteeChangeProposal(cdc), MustGetExampleCommitteeDeleteProposal(cdc)),
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
inBuf := bufio.NewReader(cmd.InOrStdin())
txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc))
cliCtx := context.NewCLIContext().WithCodec(cdc)
// Get proposing address
proposer := cliCtx.GetFromAddress()
// Get the deposit
deposit, err := sdk.ParseCoins(args[1])
if err != nil {
return err
}
// Get the proposal
bz, err := ioutil.ReadFile(args[0])
if err != nil {
return err
}
var content govtypes.Content
if err := cdc.UnmarshalJSON(bz, &content); err != nil {
return err
}
if err = content.ValidateBasic(); err != nil {
return err
}
// Build message and run basic validation
msg := govtypes.NewMsgSubmitProposal(content, deposit, proposer)
err = msg.ValidateBasic()
if err != nil {
return err
}
// Sign and broadcast message
return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
},
}
return cmd
}
// MustGetExampleCommitteeChangeProposal is a helper function to return an example json proposal
func MustGetExampleCommitteeChangeProposal(cdc *codec.Codec) string {
exampleChangeProposal := types.NewCommitteeChangeProposal(
"A Title",
"A description of this proposal.",
types.NewCommittee(
1,
"The description of this committee.",
[]sdk.AccAddress{sdk.AccAddress(crypto.AddressHash([]byte("exampleAddress")))},
[]types.Permission{
types.ParamChangePermission{
AllowedParams: types.AllowedParams{{Subspace: "cdp", Key: "CircuitBreaker"}},
},
},
sdk.MustNewDecFromStr("0.8"),
time.Hour*24*7,
),
)
exampleChangeProposalBz, err := cdc.MarshalJSONIndent(exampleChangeProposal, "", " ")
if err != nil {
panic(err)
}
return string(exampleChangeProposalBz)
}
// MustGetExampleCommitteeDeleteProposal is a helper function to return an example json proposal
func MustGetExampleCommitteeDeleteProposal(cdc *codec.Codec) string {
exampleDeleteProposal := types.NewCommitteeDeleteProposal(
"A Title",
"A description of this proposal.",
1,
)
exampleDeleteProposalBz, err := cdc.MarshalJSONIndent(exampleDeleteProposal, "", " ")
if err != nil {
panic(err)
}
return string(exampleDeleteProposalBz)
}
// MustGetExampleParameterChangeProposal is a helper function to return an example json proposal
func MustGetExampleParameterChangeProposal(cdc *codec.Codec) string {
exampleParameterChangeProposal := params.NewParameterChangeProposal(
"A Title",
"A description of this proposal.",
[]params.ParamChange{params.NewParamChange("cdp", "SurplusAuctionThreshold", "1000000000")},
)
exampleParameterChangeProposalBz, err := cdc.MarshalJSONIndent(exampleParameterChangeProposal, "", " ")
if err != nil {
panic(err)
}
return string(exampleParameterChangeProposalBz)
}

View File

@ -0,0 +1,60 @@
package common
import (
"fmt"
"github.com/cosmos/cosmos-sdk/client/context"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth/client/utils"
"github.com/kava-labs/kava/x/committee/types"
)
// Note: QueryProposer is copied in from the gov module
const (
defaultPage = 1
defaultLimit = 30 // should be consistent with tendermint/tendermint/rpc/core/pipe.go:19
)
// Proposer contains metadata of a governance proposal used for querying a proposer.
type Proposer struct {
ProposalID uint64 `json:"proposal_id" yaml:"proposal_id"`
Proposer string `json:"proposer" yaml:"proposer"`
}
// NewProposer returns a new Proposer given id and proposer
func NewProposer(proposalID uint64, proposer string) Proposer {
return Proposer{proposalID, proposer}
}
func (p Proposer) String() string {
return fmt.Sprintf("Proposal with ID %d was proposed by %s", p.ProposalID, p.Proposer)
}
// QueryProposer will query for a proposer of a governance proposal by ID.
func QueryProposer(cliCtx context.CLIContext, proposalID uint64) (Proposer, error) {
events := []string{
fmt.Sprintf("%s.%s='%s'", sdk.EventTypeMessage, sdk.AttributeKeyAction, types.TypeMsgSubmitProposal),
fmt.Sprintf("%s.%s='%s'", types.EventTypeProposalSubmit, types.AttributeKeyProposalID, []byte(fmt.Sprintf("%d", proposalID))),
}
// NOTE: SearchTxs is used to facilitate the txs query which does not currently
// support configurable pagination.
searchResult, err := utils.QueryTxsByEvents(cliCtx, events, defaultPage, defaultLimit)
if err != nil {
return Proposer{}, err
}
for _, info := range searchResult.Txs {
for _, msg := range info.Tx.GetMsgs() {
// there should only be a single proposal under the given conditions
if msg.Type() == types.TypeMsgSubmitProposal {
subMsg := msg.(types.MsgSubmitProposal)
return NewProposer(proposalID, subMsg.Proposer.String()), nil
}
}
}
return Proposer{}, fmt.Errorf("failed to find the proposer for proposalID %d", proposalID)
}

View File

@ -0,0 +1,11 @@
package client
import (
govclient "github.com/cosmos/cosmos-sdk/x/gov/client"
"github.com/kava-labs/kava/x/committee/client/cli"
"github.com/kava-labs/kava/x/committee/client/rest"
)
// ProposalHandler is a struct containing handler funcs for submiting CommitteeChange/Delete proposal txs to the gov module through the cli or rest.
var ProposalHandler = govclient.NewProposalHandler(cli.GetGovCmdSubmitProposal, rest.ProposalRESTHandler)

View File

@ -0,0 +1,275 @@
package rest
import (
"errors"
"fmt"
"net/http"
"github.com/gorilla/mux"
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/types/rest"
"github.com/kava-labs/kava/x/committee/client/common"
"github.com/kava-labs/kava/x/committee/types"
)
func registerQueryRoutes(cliCtx context.CLIContext, r *mux.Router) {
r.HandleFunc(fmt.Sprintf("/%s/committees", types.ModuleName), queryCommitteesHandlerFn(cliCtx)).Methods("GET")
r.HandleFunc(fmt.Sprintf("/%s/committees/{%s}", types.ModuleName, RestCommitteeID), queryCommitteeHandlerFn(cliCtx)).Methods("GET")
r.HandleFunc(fmt.Sprintf("/%s/committees/{%s}/proposals", types.ModuleName, RestCommitteeID), queryProposalsHandlerFn(cliCtx)).Methods("GET")
r.HandleFunc(fmt.Sprintf("/%s/proposals/{%s}", types.ModuleName, RestProposalID), queryProposalHandlerFn(cliCtx)).Methods("GET")
r.HandleFunc(fmt.Sprintf("/%s/proposals/{%s}/proposer", types.ModuleName, RestProposalID), queryProposerHandlerFn(cliCtx)).Methods("GET")
r.HandleFunc(fmt.Sprintf("/%s/proposals/{%s}/tally", types.ModuleName, RestProposalID), queryTallyOnProposalHandlerFn(cliCtx)).Methods("GET")
r.HandleFunc(fmt.Sprintf("/%s/proposals/{%s}/votes", types.ModuleName, RestProposalID), queryVotesOnProposalHandlerFn(cliCtx)).Methods("GET")
}
// ------------------------------------------
// Committees
// ------------------------------------------
func queryCommitteesHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Parse the query height
cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r)
if !ok {
return
}
// Query
res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", types.ModuleName, types.QueryCommittees), nil)
if err != nil {
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
// Write response
cliCtx = cliCtx.WithHeight(height)
rest.PostProcessResponse(w, cliCtx, res)
}
}
func queryCommitteeHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Parse the query height
cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r)
if !ok {
return
}
// Prepare params for querier
vars := mux.Vars(r)
if len(vars[RestCommitteeID]) == 0 {
err := errors.New("committeeID required but not specified")
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
committeeID, ok := rest.ParseUint64OrReturnBadRequest(w, vars[RestCommitteeID])
if !ok {
return
}
bz, err := cliCtx.Codec.MarshalJSON(types.NewQueryCommitteeParams(committeeID))
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
// Query
res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", types.ModuleName, types.QueryCommittee), bz)
if err != nil {
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
// Write response
cliCtx = cliCtx.WithHeight(height)
rest.PostProcessResponse(w, cliCtx, res)
}
}
// ------------------------------------------
// Proposals
// ------------------------------------------
func queryProposalsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Parse the query height
cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r)
if !ok {
return
}
// Prepare params for querier
vars := mux.Vars(r)
if len(vars[RestCommitteeID]) == 0 {
err := errors.New("committeeID required but not specified")
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
committeeID, ok := rest.ParseUint64OrReturnBadRequest(w, vars[RestCommitteeID])
if !ok {
return
}
bz, err := cliCtx.Codec.MarshalJSON(types.NewQueryCommitteeParams(committeeID))
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
// Query
res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", types.ModuleName, types.QueryProposals), bz)
if err != nil {
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
// Write response
cliCtx = cliCtx.WithHeight(height)
rest.PostProcessResponse(w, cliCtx, res)
}
}
func queryProposalHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Parse the query height
cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r)
if !ok {
return
}
// Prepare params for querier
vars := mux.Vars(r)
if len(vars[RestProposalID]) == 0 {
err := errors.New("proposalID required but not specified")
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
proposalID, ok := rest.ParseUint64OrReturnBadRequest(w, vars[RestProposalID])
if !ok {
return
}
bz, err := cliCtx.Codec.MarshalJSON(types.NewQueryProposalParams(proposalID))
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
// Query
res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", types.ModuleName, types.QueryProposal), bz)
if err != nil {
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
// Write response
cliCtx = cliCtx.WithHeight(height)
rest.PostProcessResponse(w, cliCtx, res)
}
}
func queryProposerHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Parse the query height
cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r)
if !ok {
return
}
// Prepare params for querier
vars := mux.Vars(r)
proposalID, ok := rest.ParseUint64OrReturnBadRequest(w, vars[RestProposalID])
if !ok {
return
}
// Query
res, err := common.QueryProposer(cliCtx, proposalID)
if err != nil {
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
// Write response
rest.PostProcessResponse(w, cliCtx, res)
}
}
// ------------------------------------------
// Votes
// ------------------------------------------
func queryVotesOnProposalHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Parse the query height
cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r)
if !ok {
return
}
// Prepare params for querier
vars := mux.Vars(r)
if len(vars[RestProposalID]) == 0 {
err := errors.New(fmt.Sprintf("%s required but not specified", RestProposalID))
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
proposalID, ok := rest.ParseUint64OrReturnBadRequest(w, vars[RestProposalID])
if !ok {
return
}
bz, err := cliCtx.Codec.MarshalJSON(types.NewQueryProposalParams(proposalID))
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
// Query
res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", types.ModuleName, types.QueryVotes), bz)
if err != nil {
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
// Write response
cliCtx = cliCtx.WithHeight(height)
rest.PostProcessResponse(w, cliCtx, res)
}
}
func queryTallyOnProposalHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Parse the query height
cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r)
if !ok {
return
}
// Prepare params for querier
vars := mux.Vars(r)
if len(vars[RestProposalID]) == 0 {
err := errors.New(fmt.Sprintf("%s required but not specified", RestProposalID))
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
proposalID, ok := rest.ParseUint64OrReturnBadRequest(w, vars[RestProposalID])
if !ok {
return
}
bz, err := cliCtx.Codec.MarshalJSON(types.NewQueryProposalParams(proposalID))
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
// Query
res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", types.ModuleName, types.QueryTally), bz)
if err != nil {
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
// Write response
cliCtx = cliCtx.WithHeight(height)
rest.PostProcessResponse(w, cliCtx, res)
}
}

View File

@ -0,0 +1,19 @@
package rest
import (
"github.com/gorilla/mux"
"github.com/cosmos/cosmos-sdk/client/context"
)
// REST Variable names
const (
RestProposalID = "proposal-id"
RestCommitteeID = "committee-id"
)
// RegisterRoutes - Central function to define routes that get registered by the main application
func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router) {
registerQueryRoutes(cliCtx, r)
registerTxRoutes(cliCtx, r)
}

View File

@ -0,0 +1,149 @@
package rest
import (
"fmt"
"net/http"
"github.com/gorilla/mux"
"github.com/cosmos/cosmos-sdk/client/context"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/rest"
"github.com/cosmos/cosmos-sdk/x/auth/client/utils"
govrest "github.com/cosmos/cosmos-sdk/x/gov/client/rest"
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
"github.com/kava-labs/kava/x/committee/types"
)
func registerTxRoutes(cliCtx context.CLIContext, r *mux.Router) {
r.HandleFunc(fmt.Sprintf("/%s/committees/{%s}/proposals", types.ModuleName, RestCommitteeID), postProposalHandlerFn(cliCtx)).Methods("POST")
r.HandleFunc(fmt.Sprintf("/%s/proposals/{%s}/votes", types.ModuleName, RestProposalID), postVoteHandlerFn(cliCtx)).Methods("POST")
}
// PostProposalReq defines the properties of a proposal request's body.
type PostProposalReq struct {
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
PubProposal types.PubProposal `json:"pub_proposal" yaml:"pub_proposal"`
Proposer sdk.AccAddress `json:"proposer" yaml:"proposer"`
}
func postProposalHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Parse and validate url params
vars := mux.Vars(r)
if len(vars[RestCommitteeID]) == 0 {
rest.WriteErrorResponse(w, http.StatusBadRequest, fmt.Sprintf("%s required but not specified", RestCommitteeID))
return
}
committeeID, ok := rest.ParseUint64OrReturnBadRequest(w, vars[RestCommitteeID])
if !ok {
return
}
// Parse and validate http request body
var req PostProposalReq
if !rest.ReadRESTReq(w, r, cliCtx.Codec, &req) {
return
}
req.BaseReq = req.BaseReq.Sanitize()
if !req.BaseReq.ValidateBasic(w) {
return
}
if err := req.PubProposal.ValidateBasic(); err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
// Create and return a StdTx
msg := types.NewMsgSubmitProposal(req.PubProposal, req.Proposer, committeeID)
if err := msg.ValidateBasic(); err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
utils.WriteGenerateStdTxResponse(w, cliCtx, req.BaseReq, []sdk.Msg{msg})
}
}
// PostVoteReq defines the properties of a vote request's body.
type PostVoteReq struct {
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
Voter sdk.AccAddress `json:"voter" yaml:"voter"`
}
func postVoteHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Parse and validate url params
vars := mux.Vars(r)
if len(vars[RestProposalID]) == 0 {
rest.WriteErrorResponse(w, http.StatusBadRequest, fmt.Sprintf("%s required but not specified", RestProposalID))
return
}
proposalID, ok := rest.ParseUint64OrReturnBadRequest(w, vars[RestProposalID])
if !ok {
return
}
// Parse and validate http request body
var req PostVoteReq
if !rest.ReadRESTReq(w, r, cliCtx.Codec, &req) {
return
}
req.BaseReq = req.BaseReq.Sanitize()
if !req.BaseReq.ValidateBasic(w) {
return
}
// Create and return a StdTx
msg := types.NewMsgVote(req.Voter, proposalID)
if err := msg.ValidateBasic(); err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
utils.WriteGenerateStdTxResponse(w, cliCtx, req.BaseReq, []sdk.Msg{msg})
}
}
// This is a rest handler for for the gov module, that handles committee change/delete proposals.
type PostGovProposalReq struct {
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
Content govtypes.Content `json:"content" yaml:"content"`
Proposer sdk.AccAddress `json:"proposer" yaml:"proposer"`
Deposit sdk.Coins `json:"deposit" yaml:"deposit"`
}
func ProposalRESTHandler(cliCtx context.CLIContext) govrest.ProposalRESTHandler {
return govrest.ProposalRESTHandler{
SubRoute: "committee",
Handler: postGovProposalHandlerFn(cliCtx),
}
}
func postGovProposalHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Parse and validate http request body
var req PostGovProposalReq
if !rest.ReadRESTReq(w, r, cliCtx.Codec, &req) {
return
}
req.BaseReq = req.BaseReq.Sanitize()
if !req.BaseReq.ValidateBasic(w) {
return
}
if err := req.Content.ValidateBasic(); err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
// Create and return a StdTx
msg := govtypes.NewMsgSubmitProposal(req.Content, req.Deposit, req.Proposer)
if err := msg.ValidateBasic(); err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
utils.WriteGenerateStdTxResponse(w, cliCtx, req.BaseReq, []sdk.Msg{msg})
}
}

46
x/committee/genesis.go Normal file
View File

@ -0,0 +1,46 @@
package committee
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/committee/types"
)
// InitGenesis initializes the store state from a genesis state.
func InitGenesis(ctx sdk.Context, keeper Keeper, gs GenesisState) {
if err := gs.Validate(); err != nil {
panic(fmt.Sprintf("failed to validate %s genesis state: %s", ModuleName, err))
}
keeper.SetNextProposalID(ctx, gs.NextProposalID)
for _, com := range gs.Committees {
keeper.SetCommittee(ctx, com)
}
for _, p := range gs.Proposals {
keeper.SetProposal(ctx, p)
}
for _, v := range gs.Votes {
keeper.SetVote(ctx, v)
}
}
// ExportGenesis returns a GenesisState for a given context and keeper.
func ExportGenesis(ctx sdk.Context, keeper Keeper) GenesisState {
nextID, err := keeper.GetNextProposalID(ctx)
if err != nil {
panic(err)
}
committees := keeper.GetCommittees(ctx)
proposals := keeper.GetProposals(ctx)
votes := keeper.GetVotes(ctx)
return types.NewGenesisState(
nextID,
committees,
proposals,
votes,
)
}

View File

@ -0,0 +1,75 @@
package committee_test
import (
"testing"
"github.com/stretchr/testify/suite"
abci "github.com/tendermint/tendermint/abci/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/committee"
"github.com/kava-labs/kava/x/committee/types"
)
type GenesisTestSuite struct {
suite.Suite
app app.TestApp
ctx sdk.Context
keeper committee.Keeper
}
func (suite *GenesisTestSuite) TestGenesis() {
testCases := []struct {
name string
genState types.GenesisState
expectPass bool
}{
{
name: "normal",
genState: types.DefaultGenesisState(),
expectPass: true,
},
{
name: "invalid",
genState: types.NewGenesisState(
2,
[]types.Committee{},
[]types.Proposal{{ID: 1, CommitteeID: 57}},
[]types.Vote{},
),
expectPass: false,
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
// Setup (note: suite.SetupTest is not run before every suite.Run)
suite.app = app.NewTestApp()
suite.keeper = suite.app.GetCommitteeKeeper()
suite.ctx = suite.app.NewContext(true, abci.Header{})
// Run
var exportedGenState types.GenesisState
run := func() {
committee.InitGenesis(suite.ctx, suite.keeper, tc.genState)
exportedGenState = committee.ExportGenesis(suite.ctx, suite.keeper)
}
if tc.expectPass {
suite.NotPanics(run)
} else {
suite.Panics(run)
}
// Check
if tc.expectPass {
suite.Equal(tc.genState, exportedGenState)
}
})
}
}
func TestGenesisTestSuite(t *testing.T) {
suite.Run(t, new(GenesisTestSuite))
}

95
x/committee/handler.go Normal file
View File

@ -0,0 +1,95 @@
package committee
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/kava-labs/kava/x/committee/keeper"
"github.com/kava-labs/kava/x/committee/types"
)
// NewHandler creates an sdk.Handler for committee messages
func NewHandler(k keeper.Keeper) sdk.Handler {
return func(ctx sdk.Context, msg sdk.Msg) (*sdk.Result, error) {
ctx = ctx.WithEventManager(sdk.NewEventManager())
switch msg := msg.(type) {
case types.MsgSubmitProposal:
return handleMsgSubmitProposal(ctx, k, msg)
case types.MsgVote:
return handleMsgVote(ctx, k, msg)
default:
return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized %s message type: %T", ModuleName, msg)
}
}
}
func handleMsgSubmitProposal(ctx sdk.Context, k keeper.Keeper, msg types.MsgSubmitProposal) (*sdk.Result, error) {
proposalID, err := k.SubmitProposal(ctx, msg.Proposer, msg.CommitteeID, msg.PubProposal)
if err != nil {
return nil, err
}
ctx.EventManager().EmitEvent(
sdk.NewEvent(
sdk.EventTypeMessage,
sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory),
sdk.NewAttribute(sdk.AttributeKeySender, msg.Proposer.String()),
),
)
return &sdk.Result{
Data: GetKeyFromID(proposalID),
Events: ctx.EventManager().Events(),
}, nil
}
func handleMsgVote(ctx sdk.Context, k keeper.Keeper, msg types.MsgVote) (*sdk.Result, error) {
// get the proposal just to add fields to the event
proposal, found := k.GetProposal(ctx, msg.ProposalID)
if !found {
return nil, sdkerrors.Wrapf(ErrUnknownProposal, "%d", msg.ProposalID)
}
err := k.AddVote(ctx, msg.ProposalID, msg.Voter)
if err != nil {
return nil, err
}
ctx.EventManager().EmitEvent(
sdk.NewEvent(
sdk.EventTypeMessage,
sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory),
sdk.NewAttribute(sdk.AttributeKeySender, msg.Voter.String()),
),
)
// Enact a proposal if it has enough votes
passes, err := k.GetProposalResult(ctx, msg.ProposalID)
if err != nil {
return nil, err
}
if !passes {
return &sdk.Result{Events: ctx.EventManager().Events()}, nil
}
err = k.EnactProposal(ctx, msg.ProposalID)
outcome := types.AttributeValueProposalPassed
if err != nil {
outcome = types.AttributeValueProposalFailed
}
k.DeleteProposalAndVotes(ctx, msg.ProposalID)
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeProposalClose,
sdk.NewAttribute(types.AttributeKeyCommitteeID, fmt.Sprintf("%d", proposal.CommitteeID)),
sdk.NewAttribute(types.AttributeKeyProposalID, fmt.Sprintf("%d", proposal.ID)),
sdk.NewAttribute(types.AttributeKeyProposalCloseStatus, outcome),
),
)
return &sdk.Result{Events: ctx.EventManager().Events()}, nil
}

210
x/committee/handler_test.go Normal file
View File

@ -0,0 +1,210 @@
package committee_test
import (
"testing"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/distribution"
"github.com/cosmos/cosmos-sdk/x/params"
"github.com/stretchr/testify/suite"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/kava-labs/kava/app"
cdptypes "github.com/kava-labs/kava/x/cdp/types"
"github.com/kava-labs/kava/x/committee"
"github.com/kava-labs/kava/x/committee/keeper"
"github.com/kava-labs/kava/x/committee/types"
)
// NewDistributionGenesisWithPool creates a default distribution genesis state with some coins in the community pool.
func NewDistributionGenesisWithPool(communityPoolCoins sdk.Coins) app.GenesisState {
gs := distribution.DefaultGenesisState()
gs.FeePool = distribution.FeePool{CommunityPool: sdk.NewDecCoinsFromCoins(communityPoolCoins...)}
return app.GenesisState{distribution.ModuleName: distribution.ModuleCdc.MustMarshalJSON(gs)}
}
type HandlerTestSuite struct {
suite.Suite
app app.TestApp
keeper keeper.Keeper
handler sdk.Handler
ctx sdk.Context
addresses []sdk.AccAddress
communityPoolAmt sdk.Coins
}
func (suite *HandlerTestSuite) SetupTest() {
_, suite.addresses = app.GeneratePrivKeyAddressPairs(5)
suite.app = app.NewTestApp()
suite.keeper = suite.app.GetCommitteeKeeper()
suite.handler = committee.NewHandler(suite.keeper)
firstBlockTime := time.Date(1998, time.January, 1, 1, 0, 0, 0, time.UTC)
testGenesis := types.NewGenesisState(
3,
[]types.Committee{
{
ID: 1,
Description: "This committee is for testing.",
Members: suite.addresses[:3],
Permissions: []types.Permission{types.GodPermission{}},
VoteThreshold: d("0.5"),
ProposalDuration: time.Hour * 24 * 7,
},
},
[]types.Proposal{},
[]types.Vote{},
)
suite.communityPoolAmt = cs(c("ukava", 1000))
suite.app.InitializeFromGenesisStates(
NewCommitteeGenesisState(suite.app.Codec(), testGenesis),
NewDistributionGenesisWithPool(suite.communityPoolAmt),
)
suite.ctx = suite.app.NewContext(true, abci.Header{Height: 1, Time: firstBlockTime})
}
func (suite *HandlerTestSuite) TestSubmitProposalMsg_Valid() {
msg := committee.NewMsgSubmitProposal(
params.NewParameterChangeProposal(
"A Title",
"A description of this proposal.",
[]params.ParamChange{{
Subspace: cdptypes.ModuleName,
Key: string(cdptypes.KeyDebtThreshold),
Value: string(types.ModuleCdc.MustMarshalJSON(i(1000000))),
}},
),
suite.addresses[0],
1,
)
res, err := suite.handler(suite.ctx, msg)
suite.NoError(err)
_, found := suite.keeper.GetProposal(suite.ctx, types.Uint64FromBytes(res.Data))
suite.True(found)
}
func (suite *HandlerTestSuite) TestSubmitProposalMsg_Invalid() {
var committeeID uint64 = 1
msg := types.NewMsgSubmitProposal(
params.NewParameterChangeProposal(
"A Title",
"A description of this proposal.",
[]params.ParamChange{{
Subspace: cdptypes.ModuleName,
Key: "nonsense-key",
Value: "nonsense-value",
}},
),
suite.addresses[0],
committeeID,
)
_, err := suite.handler(suite.ctx, msg)
suite.Error(err)
suite.Empty(
suite.keeper.GetProposalsByCommittee(suite.ctx, committeeID),
"proposal found when none should exist",
)
}
func (suite *HandlerTestSuite) TestSubmitProposalMsg_Unregistered() {
var committeeID uint64 = 1
msg := types.NewMsgSubmitProposal(
UnregisteredPubProposal{},
suite.addresses[0],
committeeID,
)
_, err := suite.handler(suite.ctx, msg)
suite.Error(err)
suite.Empty(
suite.keeper.GetProposalsByCommittee(suite.ctx, committeeID),
"proposal found when none should exist",
)
}
func (suite *HandlerTestSuite) TestMsgAddVote_ProposalPass() {
previousCDPDebtThreshold := suite.app.GetCDPKeeper().GetParams(suite.ctx).DebtAuctionThreshold
newDebtThreshold := previousCDPDebtThreshold.Add(i(1000000))
msg := types.NewMsgSubmitProposal(
params.NewParameterChangeProposal(
"A Title",
"A description of this proposal.",
[]params.ParamChange{{
Subspace: cdptypes.ModuleName,
Key: string(cdptypes.KeyDebtThreshold),
Value: string(types.ModuleCdc.MustMarshalJSON(newDebtThreshold)),
}},
),
suite.addresses[0],
1,
)
res, err := suite.handler(suite.ctx, msg)
suite.NoError(err)
proposalID := types.Uint64FromBytes(res.Data)
_, err = suite.handler(suite.ctx, types.NewMsgVote(suite.addresses[0], proposalID))
suite.NoError(err)
// Add a vote to make the proposal pass
_, err = suite.handler(suite.ctx, types.NewMsgVote(suite.addresses[1], proposalID))
suite.NoError(err)
// Check the param has been updated
suite.Equal(newDebtThreshold, suite.app.GetCDPKeeper().GetParams(suite.ctx).DebtAuctionThreshold)
// Check proposal and votes are gone
_, found := suite.keeper.GetProposal(suite.ctx, proposalID)
suite.False(found)
suite.Empty(
suite.keeper.GetVotesByProposal(suite.ctx, proposalID),
"vote found when there should be none",
)
}
func (suite *HandlerTestSuite) TestMsgAddVote_ProposalFail() {
recipient := suite.addresses[4]
recipientCoins := suite.app.GetBankKeeper().GetCoins(suite.ctx, recipient)
msg := types.NewMsgSubmitProposal(
distribution.NewCommunityPoolSpendProposal(
"A Title",
"A description of this proposal.",
recipient,
cs(c("ukava", 500)),
),
suite.addresses[0],
1,
)
res, err := suite.handler(suite.ctx, msg)
suite.NoError(err)
proposalID := types.Uint64FromBytes(res.Data)
_, err = suite.handler(suite.ctx, types.NewMsgVote(suite.addresses[0], proposalID))
suite.NoError(err)
// invalidate the proposal by emptying community pool
suite.app.GetDistrKeeper().DistributeFromFeePool(suite.ctx, suite.communityPoolAmt, suite.addresses[0])
// Add a vote to make the proposal pass
_, err = suite.handler(suite.ctx, types.NewMsgVote(suite.addresses[1], proposalID))
suite.NoError(err)
// Check the proposal was not enacted
suite.Equal(recipientCoins, suite.app.GetBankKeeper().GetCoins(suite.ctx, recipient))
// Check proposal and votes are gone
_, found := suite.keeper.GetProposal(suite.ctx, proposalID)
suite.False(found)
suite.Empty(
suite.keeper.GetVotesByProposal(suite.ctx, proposalID),
"vote found when there should be none",
)
}
func TestHandlerTestSuite(t *testing.T) {
suite.Run(t, new(HandlerTestSuite))
}

View File

@ -0,0 +1,32 @@
package committee_test
import (
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/committee/types"
)
// Avoid cluttering test cases with long function names
func i(in int64) sdk.Int { return sdk.NewInt(in) }
func d(str string) sdk.Dec { return sdk.MustNewDecFromStr(str) }
func c(denom string, amount int64) sdk.Coin { return sdk.NewInt64Coin(denom, amount) }
func cs(coins ...sdk.Coin) sdk.Coins { return sdk.NewCoins(coins...) }
// NewCommitteeGenesisState marshals a committee genesis state into json for use in initializing test apps.
func NewCommitteeGenesisState(cdc *codec.Codec, gs types.GenesisState) app.GenesisState {
return app.GenesisState{types.ModuleName: cdc.MustMarshalJSON(gs)}
}
var _ types.PubProposal = UnregisteredPubProposal{}
// UnregisteredPubProposal is a pubproposal type that is not registered on the amino codec.
type UnregisteredPubProposal struct{}
func (UnregisteredPubProposal) GetTitle() string { return "unregistered" }
func (UnregisteredPubProposal) GetDescription() string { return "unregistered" }
func (UnregisteredPubProposal) ProposalRoute() string { return "unregistered" }
func (UnregisteredPubProposal) ProposalType() string { return "unregistered" }
func (UnregisteredPubProposal) ValidateBasic() error { return nil }
func (UnregisteredPubProposal) String() string { return "unregistered" }

View File

@ -0,0 +1,34 @@
package keeper_test
import (
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/committee"
"github.com/kava-labs/kava/x/committee/keeper"
"github.com/kava-labs/kava/x/committee/types"
)
// Avoid cluttering test cases with long function names
func i(in int64) sdk.Int { return sdk.NewInt(in) }
func d(str string) sdk.Dec { return sdk.MustNewDecFromStr(str) }
func c(denom string, amount int64) sdk.Coin { return sdk.NewInt64Coin(denom, amount) }
func cs(coins ...sdk.Coin) sdk.Coins { return sdk.NewCoins(coins...) }
// getProposalVoteMap collects up votes into a map indexed by proposalID
func getProposalVoteMap(k keeper.Keeper, ctx sdk.Context) map[uint64]([]types.Vote) {
proposalVoteMap := map[uint64]([]types.Vote){}
k.IterateProposals(ctx, func(p types.Proposal) bool {
proposalVoteMap[p.ID] = k.GetVotesByProposal(ctx, p.ID)
return false
})
return proposalVoteMap
}
// NewCommitteeGenesisState marshals a committee genesis state into json for use in initializing test apps.
func NewCommitteeGenesisState(cdc *codec.Codec, gs committee.GenesisState) app.GenesisState {
return app.GenesisState{committee.ModuleName: cdc.MustMarshalJSON(gs)}
}

View File

@ -0,0 +1,281 @@
package keeper
import (
"time"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/store/prefix"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
"github.com/kava-labs/kava/x/committee/types"
)
type Keeper struct {
cdc *codec.Codec
storeKey sdk.StoreKey
// Proposal router
router govtypes.Router
}
func NewKeeper(cdc *codec.Codec, storeKey sdk.StoreKey, router govtypes.Router) Keeper {
// Logic in the keeper methods assume the set of gov handlers is fixed.
// So the gov router must be sealed so no handlers can be added or removed after the keeper is created.
router.Seal()
return Keeper{
cdc: cdc,
storeKey: storeKey,
router: router,
}
}
// ------------------------------------------
// Committees
// ------------------------------------------
// GetCommittee gets a committee from the store.
func (k Keeper) GetCommittee(ctx sdk.Context, committeeID uint64) (types.Committee, bool) {
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.CommitteeKeyPrefix)
bz := store.Get(types.GetKeyFromID(committeeID))
if bz == nil {
return types.Committee{}, false
}
var committee types.Committee
k.cdc.MustUnmarshalBinaryBare(bz, &committee)
return committee, true
}
// SetCommittee puts a committee into the store.
func (k Keeper) SetCommittee(ctx sdk.Context, committee types.Committee) {
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.CommitteeKeyPrefix)
bz := k.cdc.MustMarshalBinaryBare(committee)
store.Set(types.GetKeyFromID(committee.ID), bz)
}
// DeleteCommittee removes a committee from the store.
func (k Keeper) DeleteCommittee(ctx sdk.Context, committeeID uint64) {
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.CommitteeKeyPrefix)
store.Delete(types.GetKeyFromID(committeeID))
}
// IterateCommittees provides an iterator over all stored committees.
// For each committee, cb will be called. If cb returns true, the iterator will close and stop.
func (k Keeper) IterateCommittees(ctx sdk.Context, cb func(committee types.Committee) (stop bool)) {
iterator := sdk.KVStorePrefixIterator(ctx.KVStore(k.storeKey), types.CommitteeKeyPrefix)
defer iterator.Close()
for ; iterator.Valid(); iterator.Next() {
var committee types.Committee
k.cdc.MustUnmarshalBinaryBare(iterator.Value(), &committee)
if cb(committee) {
break
}
}
}
// GetCommittees returns all stored committees.
func (k Keeper) GetCommittees(ctx sdk.Context) []types.Committee {
results := []types.Committee{}
k.IterateCommittees(ctx, func(com types.Committee) bool {
results = append(results, com)
return false
})
return results
}
// ------------------------------------------
// Proposals
// ------------------------------------------
// SetNextProposalID stores an ID to be used for the next created proposal
func (k Keeper) SetNextProposalID(ctx sdk.Context, id uint64) {
store := ctx.KVStore(k.storeKey)
store.Set(types.NextProposalIDKey, types.GetKeyFromID(id))
}
// GetNextProposalID reads the next available global ID from store
func (k Keeper) GetNextProposalID(ctx sdk.Context) (uint64, error) {
store := ctx.KVStore(k.storeKey)
bz := store.Get(types.NextProposalIDKey)
if bz == nil {
return 0, sdkerrors.Wrap(types.ErrInvalidGenesis, "next proposal ID not set at genesis")
}
return types.Uint64FromBytes(bz), nil
}
// IncrementNextProposalID increments the next proposal ID in the store by 1.
func (k Keeper) IncrementNextProposalID(ctx sdk.Context) error {
id, err := k.GetNextProposalID(ctx)
if err != nil {
return err
}
k.SetNextProposalID(ctx, id+1)
return nil
}
// StoreNewProposal stores a proposal, adding a new ID
func (k Keeper) StoreNewProposal(ctx sdk.Context, pubProposal types.PubProposal, committeeID uint64, deadline time.Time) (uint64, error) {
newProposalID, err := k.GetNextProposalID(ctx)
if err != nil {
return 0, err
}
proposal := types.NewProposal(
pubProposal,
newProposalID,
committeeID,
deadline,
)
k.SetProposal(ctx, proposal)
err = k.IncrementNextProposalID(ctx)
if err != nil {
return 0, err
}
return newProposalID, nil
}
// GetProposal gets a proposal from the store.
func (k Keeper) GetProposal(ctx sdk.Context, proposalID uint64) (types.Proposal, bool) {
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.ProposalKeyPrefix)
bz := store.Get(types.GetKeyFromID(proposalID))
if bz == nil {
return types.Proposal{}, false
}
var proposal types.Proposal
k.cdc.MustUnmarshalBinaryBare(bz, &proposal)
return proposal, true
}
// SetProposal puts a proposal into the store.
func (k Keeper) SetProposal(ctx sdk.Context, proposal types.Proposal) {
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.ProposalKeyPrefix)
bz := k.cdc.MustMarshalBinaryBare(proposal)
store.Set(types.GetKeyFromID(proposal.ID), bz)
}
// DeleteProposal removes a proposal from the store.
func (k Keeper) DeleteProposal(ctx sdk.Context, proposalID uint64) {
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.ProposalKeyPrefix)
store.Delete(types.GetKeyFromID(proposalID))
}
// IterateProposals provides an iterator over all stored proposals.
// For each proposal, cb will be called. If cb returns true, the iterator will close and stop.
func (k Keeper) IterateProposals(ctx sdk.Context, cb func(proposal types.Proposal) (stop bool)) {
iterator := sdk.KVStorePrefixIterator(ctx.KVStore(k.storeKey), types.ProposalKeyPrefix)
defer iterator.Close()
for ; iterator.Valid(); iterator.Next() {
var proposal types.Proposal
k.cdc.MustUnmarshalBinaryBare(iterator.Value(), &proposal)
if cb(proposal) {
break
}
}
}
// GetProposals returns all stored proposals.
func (k Keeper) GetProposals(ctx sdk.Context) []types.Proposal {
results := []types.Proposal{}
k.IterateProposals(ctx, func(prop types.Proposal) bool {
results = append(results, prop)
return false
})
return results
}
// GetProposalsByCommittee returns all proposals for one committee.
func (k Keeper) GetProposalsByCommittee(ctx sdk.Context, committeeID uint64) []types.Proposal {
results := []types.Proposal{}
k.IterateProposals(ctx, func(prop types.Proposal) bool {
if prop.CommitteeID == committeeID {
results = append(results, prop)
}
return false
})
return results
}
// DeleteProposalAndVotes removes a proposal and its associated votes.
func (k Keeper) DeleteProposalAndVotes(ctx sdk.Context, proposalID uint64) {
votes := k.GetVotesByProposal(ctx, proposalID)
k.DeleteProposal(ctx, proposalID)
for _, v := range votes {
k.DeleteVote(ctx, v.ProposalID, v.Voter)
}
}
// ------------------------------------------
// Votes
// ------------------------------------------
// GetVote gets a vote from the store.
func (k Keeper) GetVote(ctx sdk.Context, proposalID uint64, voter sdk.AccAddress) (types.Vote, bool) {
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.VoteKeyPrefix)
bz := store.Get(types.GetVoteKey(proposalID, voter))
if bz == nil {
return types.Vote{}, false
}
var vote types.Vote
k.cdc.MustUnmarshalBinaryBare(bz, &vote)
return vote, true
}
// SetVote puts a vote into the store.
func (k Keeper) SetVote(ctx sdk.Context, vote types.Vote) {
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.VoteKeyPrefix)
bz := k.cdc.MustMarshalBinaryBare(vote)
store.Set(types.GetVoteKey(vote.ProposalID, vote.Voter), bz)
}
// DeleteVote removes a Vote from the store.
func (k Keeper) DeleteVote(ctx sdk.Context, proposalID uint64, voter sdk.AccAddress) {
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.VoteKeyPrefix)
store.Delete(types.GetVoteKey(proposalID, voter))
}
// IterateVotes provides an iterator over all stored votes.
// For each vote, cb will be called. If cb returns true, the iterator will close and stop.
func (k Keeper) IterateVotes(ctx sdk.Context, cb func(vote types.Vote) (stop bool)) {
iterator := sdk.KVStorePrefixIterator(ctx.KVStore(k.storeKey), types.VoteKeyPrefix)
defer iterator.Close()
for ; iterator.Valid(); iterator.Next() {
var vote types.Vote
k.cdc.MustUnmarshalBinaryBare(iterator.Value(), &vote)
if cb(vote) {
break
}
}
}
// GetVotes returns all stored votes.
func (k Keeper) GetVotes(ctx sdk.Context) []types.Vote {
results := []types.Vote{}
k.IterateVotes(ctx, func(vote types.Vote) bool {
results = append(results, vote)
return false
})
return results
}
// GetVotesByProposal returns all votes for one proposal.
func (k Keeper) GetVotesByProposal(ctx sdk.Context, proposalID uint64) []types.Vote {
results := []types.Vote{}
k.IterateVotes(ctx, func(vote types.Vote) bool {
if vote.ProposalID == proposalID {
results = append(results, vote)
}
return false
})
return results
}

View File

@ -0,0 +1,112 @@
package keeper_test
import (
"testing"
"time"
"github.com/stretchr/testify/suite"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/gov"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/committee/keeper"
"github.com/kava-labs/kava/x/committee/types"
)
type KeeperTestSuite struct {
suite.Suite
keeper keeper.Keeper
app app.TestApp
ctx sdk.Context
addresses []sdk.AccAddress
}
func (suite *KeeperTestSuite) SetupTest() {
suite.app = app.NewTestApp()
suite.keeper = suite.app.GetCommitteeKeeper()
suite.ctx = suite.app.NewContext(true, abci.Header{})
_, suite.addresses = app.GeneratePrivKeyAddressPairs(5)
}
func (suite *KeeperTestSuite) TestGetSetDeleteCommittee() {
// setup test
com := types.Committee{
ID: 12,
Description: "This committee is for testing.",
Members: suite.addresses,
Permissions: []types.Permission{types.GodPermission{}},
VoteThreshold: d("0.667"),
ProposalDuration: time.Hour * 24 * 7,
}
// write and read from store
suite.keeper.SetCommittee(suite.ctx, com)
readCommittee, found := suite.keeper.GetCommittee(suite.ctx, com.ID)
// check before and after match
suite.True(found)
suite.Equal(com, readCommittee)
// delete from store
suite.keeper.DeleteCommittee(suite.ctx, com.ID)
// check does not exist
_, found = suite.keeper.GetCommittee(suite.ctx, com.ID)
suite.False(found)
}
func (suite *KeeperTestSuite) TestGetSetDeleteProposal() {
// test setup
prop := types.Proposal{
ID: 12,
CommitteeID: 0,
PubProposal: gov.NewTextProposal("A Title", "A description of this proposal."),
Deadline: time.Date(1998, time.January, 1, 0, 0, 0, 0, time.UTC),
}
// write and read from store
suite.keeper.SetProposal(suite.ctx, prop)
readProposal, found := suite.keeper.GetProposal(suite.ctx, prop.ID)
// check before and after match
suite.True(found)
suite.Equal(prop, readProposal)
// delete from store
suite.keeper.DeleteProposal(suite.ctx, prop.ID)
// check does not exist
_, found = suite.keeper.GetProposal(suite.ctx, prop.ID)
suite.False(found)
}
func (suite *KeeperTestSuite) TestGetSetDeleteVote() {
// test setup
vote := types.Vote{
ProposalID: 12,
Voter: suite.addresses[0],
}
// write and read from store
suite.keeper.SetVote(suite.ctx, vote)
readVote, found := suite.keeper.GetVote(suite.ctx, vote.ProposalID, vote.Voter)
// check before and after match
suite.True(found)
suite.Equal(vote, readVote)
// delete from store
suite.keeper.DeleteVote(suite.ctx, vote.ProposalID, vote.Voter)
// check does not exist
_, found = suite.keeper.GetVote(suite.ctx, vote.ProposalID, vote.Voter)
suite.False(found)
}
func TestKeeperTestSuite(t *testing.T) {
suite.Run(t, new(KeeperTestSuite))
}

View File

@ -0,0 +1,179 @@
package keeper
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/kava-labs/kava/x/committee/types"
)
// SubmitProposal adds a proposal to a committee so that it can be voted on.
func (k Keeper) SubmitProposal(ctx sdk.Context, proposer sdk.AccAddress, committeeID uint64, pubProposal types.PubProposal) (uint64, error) {
// Limit proposals to only be submitted by committee members
com, found := k.GetCommittee(ctx, committeeID)
if !found {
return 0, sdkerrors.Wrapf(types.ErrUnknownCommittee, "%d", committeeID)
}
if !com.HasMember(proposer) {
return 0, sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "proposer not member of committee")
}
// Check committee has permissions to enact proposal.
if !com.HasPermissionsFor(pubProposal) {
return 0, sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "committee does not have permissions to enact proposal")
}
// Check proposal is valid
if err := k.ValidatePubProposal(ctx, pubProposal); err != nil {
return 0, err
}
// Get a new ID and store the proposal
deadline := ctx.BlockTime().Add(com.ProposalDuration)
proposalID, err := k.StoreNewProposal(ctx, pubProposal, committeeID, deadline)
if err != nil {
return 0, err
}
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeProposalSubmit,
sdk.NewAttribute(types.AttributeKeyCommitteeID, fmt.Sprintf("%d", com.ID)),
sdk.NewAttribute(types.AttributeKeyProposalID, fmt.Sprintf("%d", proposalID)),
),
)
return proposalID, nil
}
// AddVote submits a vote on a proposal.
func (k Keeper) AddVote(ctx sdk.Context, proposalID uint64, voter sdk.AccAddress) error {
// Validate
pr, found := k.GetProposal(ctx, proposalID)
if !found {
return sdkerrors.Wrapf(types.ErrUnknownProposal, "%d", proposalID)
}
if pr.HasExpiredBy(ctx.BlockTime()) {
return sdkerrors.Wrapf(types.ErrProposalExpired, "%s ≥ %s", ctx.BlockTime(), pr.Deadline)
}
com, found := k.GetCommittee(ctx, pr.CommitteeID)
if !found {
return sdkerrors.Wrapf(types.ErrUnknownCommittee, "%d", pr.CommitteeID)
}
if !com.HasMember(voter) {
return sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "voter must be a member of committee")
}
// Store vote, overwriting any prior vote
k.SetVote(ctx, types.Vote{ProposalID: proposalID, Voter: voter})
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeProposalVote,
sdk.NewAttribute(types.AttributeKeyCommitteeID, fmt.Sprintf("%d", com.ID)),
sdk.NewAttribute(types.AttributeKeyProposalID, fmt.Sprintf("%d", pr.ID)),
),
)
return nil
}
// GetProposalResult calculates if a proposal currently has enough votes to pass.
func (k Keeper) GetProposalResult(ctx sdk.Context, proposalID uint64) (bool, error) {
pr, found := k.GetProposal(ctx, proposalID)
if !found {
return false, sdkerrors.Wrapf(types.ErrUnknownProposal, "%d", proposalID)
}
com, found := k.GetCommittee(ctx, pr.CommitteeID)
if !found {
return false, sdkerrors.Wrapf(types.ErrUnknownCommittee, "%d", pr.CommitteeID)
}
numVotes := k.TallyVotes(ctx, proposalID)
proposalResult := sdk.NewDec(numVotes).GTE(com.VoteThreshold.MulInt64(int64(len(com.Members))))
return proposalResult, nil
}
// TallyVotes counts all the votes on a proposal
func (k Keeper) TallyVotes(ctx sdk.Context, proposalID uint64) int64 {
votes := k.GetVotesByProposal(ctx, proposalID)
return int64(len(votes))
}
// EnactProposal makes the changes proposed in a proposal.
func (k Keeper) EnactProposal(ctx sdk.Context, proposalID uint64) error {
pr, found := k.GetProposal(ctx, proposalID)
if !found {
return sdkerrors.Wrapf(types.ErrUnknownProposal, "%d", proposalID)
}
if err := k.ValidatePubProposal(ctx, pr.PubProposal); err != nil {
return err
}
handler := k.router.GetRoute(pr.ProposalRoute())
if err := handler(ctx, pr.PubProposal); err != nil {
// the handler should not error as it was checked in ValidatePubProposal
panic(fmt.Sprintf("unexpected handler error: %s", err))
}
return nil
}
// CloseExpiredProposals removes proposals (and associated votes) that have past their deadline.
func (k Keeper) CloseExpiredProposals(ctx sdk.Context) {
k.IterateProposals(ctx, func(proposal types.Proposal) bool {
if !proposal.HasExpiredBy(ctx.BlockTime()) {
return false
}
k.DeleteProposalAndVotes(ctx, proposal.ID)
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeProposalClose,
sdk.NewAttribute(types.AttributeKeyCommitteeID, fmt.Sprintf("%d", proposal.CommitteeID)),
sdk.NewAttribute(types.AttributeKeyProposalID, fmt.Sprintf("%d", proposal.ID)),
sdk.NewAttribute(types.AttributeKeyProposalCloseStatus, types.AttributeValueProposalTimeout),
),
)
return false
})
}
// ValidatePubProposal checks if a pubproposal is valid.
func (k Keeper) ValidatePubProposal(ctx sdk.Context, pubProposal types.PubProposal) (returnErr error) {
if pubProposal == nil {
return sdkerrors.Wrap(types.ErrInvalidPubProposal, "pub proposal cannot be nil")
}
if err := pubProposal.ValidateBasic(); err != nil {
return err
}
if !k.router.HasRoute(pubProposal.ProposalRoute()) {
return sdkerrors.Wrapf(types.ErrNoProposalHandlerExists, "%T", pubProposal)
}
// Run the proposal's changes through the associated handler using a cached version of state to ensure changes are not permanent.
cacheCtx, _ := ctx.CacheContext()
handler := k.router.GetRoute(pubProposal.ProposalRoute())
// Handle an edge case where a param change proposal causes the proposal handler to panic.
// A param change proposal with a registered subspace value but unregistered key value will cause a panic in the param change proposal handler.
// This defer will catch panics and return a normal error: `recover()` gets the panic value, then the enclosing function's return value is swapped for an error.
// reference: https://stackoverflow.com/questions/33167282/how-to-return-a-value-in-a-go-function-that-panics?noredirect=1&lq=1
defer func() {
if r := recover(); r != nil {
returnErr = sdkerrors.Wrapf(types.ErrInvalidPubProposal, "proposal handler panicked: %s", r)
}
}()
if err := handler(cacheCtx, pubProposal); err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,446 @@
package keeper_test
import (
"reflect"
"time"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/gov"
"github.com/cosmos/cosmos-sdk/x/params"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/kava-labs/kava/app"
cdptypes "github.com/kava-labs/kava/x/cdp/types"
"github.com/kava-labs/kava/x/committee"
"github.com/kava-labs/kava/x/committee/types"
)
func (suite *KeeperTestSuite) TestSubmitProposal() {
normalCom := types.Committee{
ID: 12,
Description: "This committee is for testing.",
Members: suite.addresses[:2],
Permissions: []types.Permission{types.GodPermission{}},
VoteThreshold: d("0.667"),
ProposalDuration: time.Hour * 24 * 7,
}
noPermissionsCom := normalCom
noPermissionsCom.Permissions = []types.Permission{}
testcases := []struct {
name string
committee types.Committee
pubProposal types.PubProposal
proposer sdk.AccAddress
committeeID uint64
expectErr bool
}{
{
name: "normal",
committee: normalCom,
pubProposal: gov.NewTextProposal("A Title", "A description of this proposal."),
proposer: normalCom.Members[0],
committeeID: normalCom.ID,
expectErr: false,
},
{
name: "invalid proposal",
committee: normalCom,
pubProposal: nil,
proposer: normalCom.Members[0],
committeeID: normalCom.ID,
expectErr: true,
},
{
name: "missing committee",
// no committee
pubProposal: gov.NewTextProposal("A Title", "A description of this proposal."),
proposer: suite.addresses[0],
committeeID: 0,
expectErr: true,
},
{
name: "not a member",
committee: normalCom,
pubProposal: gov.NewTextProposal("A Title", "A description of this proposal."),
proposer: suite.addresses[4],
committeeID: normalCom.ID,
expectErr: true,
},
{
name: "not enough permissions",
committee: noPermissionsCom,
pubProposal: gov.NewTextProposal("A Title", "A description of this proposal."),
proposer: noPermissionsCom.Members[0],
committeeID: noPermissionsCom.ID,
expectErr: true,
},
}
for _, tc := range testcases {
suite.Run(tc.name, func() {
// Create local testApp because suite doesn't run the SetupTest function for subtests
tApp := app.NewTestApp()
keeper := tApp.GetCommitteeKeeper()
ctx := tApp.NewContext(true, abci.Header{})
tApp.InitializeFromGenesisStates()
// setup committee (if required)
if !(reflect.DeepEqual(tc.committee, types.Committee{})) {
keeper.SetCommittee(ctx, tc.committee)
}
id, err := keeper.SubmitProposal(ctx, tc.proposer, tc.committeeID, tc.pubProposal)
if tc.expectErr {
suite.NotNil(err)
} else {
suite.NoError(err)
pr, found := keeper.GetProposal(ctx, id)
suite.True(found)
suite.Equal(tc.committeeID, pr.CommitteeID)
suite.Equal(ctx.BlockTime().Add(tc.committee.ProposalDuration), pr.Deadline)
}
})
}
}
func (suite *KeeperTestSuite) TestAddVote() {
normalCom := types.Committee{
ID: 12,
Members: suite.addresses[:2],
Permissions: []types.Permission{types.GodPermission{}},
}
firstBlockTime := time.Date(1998, time.January, 1, 1, 0, 0, 0, time.UTC)
testcases := []struct {
name string
proposalID uint64
voter sdk.AccAddress
voteTime time.Time
expectErr bool
}{
{
name: "normal",
proposalID: types.DefaultNextProposalID,
voter: normalCom.Members[0],
expectErr: false,
},
{
name: "nonexistent proposal",
proposalID: 9999999,
voter: normalCom.Members[0],
expectErr: true,
},
{
name: "voter not committee member",
proposalID: types.DefaultNextProposalID,
voter: suite.addresses[4],
expectErr: true,
},
{
name: "proposal expired",
proposalID: types.DefaultNextProposalID,
voter: normalCom.Members[0],
voteTime: firstBlockTime.Add(normalCom.ProposalDuration),
expectErr: true,
},
}
for _, tc := range testcases {
suite.Run(tc.name, func() {
// Create local testApp because suite doesn't run the SetupTest function for subtests
tApp := app.NewTestApp()
keeper := tApp.GetCommitteeKeeper()
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: firstBlockTime})
tApp.InitializeFromGenesisStates()
// setup the committee and proposal
keeper.SetCommittee(ctx, normalCom)
_, err := keeper.SubmitProposal(ctx, normalCom.Members[0], normalCom.ID, gov.NewTextProposal("A Title", "A description of this proposal."))
suite.NoError(err)
ctx = ctx.WithBlockTime(tc.voteTime)
err = keeper.AddVote(ctx, tc.proposalID, tc.voter)
if tc.expectErr {
suite.NotNil(err)
} else {
suite.NoError(err)
_, found := keeper.GetVote(ctx, tc.proposalID, tc.voter)
suite.True(found)
}
})
}
}
func (suite *KeeperTestSuite) TestGetProposalResult() {
normalCom := types.Committee{
ID: 12,
Description: "This committee is for testing.",
Members: suite.addresses[:5],
Permissions: []types.Permission{types.GodPermission{}},
VoteThreshold: d("0.667"),
ProposalDuration: time.Hour * 24 * 7,
}
var defaultID uint64 = 1
firstBlockTime := time.Date(1998, time.January, 1, 1, 0, 0, 0, time.UTC)
testcases := []struct {
name string
committee types.Committee
votes []types.Vote
proposalPasses bool
expectErr bool
}{
{
name: "enough votes",
committee: normalCom,
votes: []types.Vote{
{ProposalID: defaultID, Voter: suite.addresses[0]},
{ProposalID: defaultID, Voter: suite.addresses[1]},
{ProposalID: defaultID, Voter: suite.addresses[2]},
{ProposalID: defaultID, Voter: suite.addresses[3]},
},
proposalPasses: true,
expectErr: false,
},
{
name: "not enough votes",
committee: normalCom,
votes: []types.Vote{
{ProposalID: defaultID, Voter: suite.addresses[0]},
},
proposalPasses: false,
expectErr: false,
},
}
for _, tc := range testcases {
suite.Run(tc.name, func() {
// Create local testApp because suite doesn't run the SetupTest function for subtests
tApp := app.NewTestApp()
keeper := tApp.GetCommitteeKeeper()
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: firstBlockTime})
tApp.InitializeFromGenesisStates(
committeeGenState(
tApp.Codec(),
[]types.Committee{tc.committee},
[]types.Proposal{{
PubProposal: gov.NewTextProposal("A Title", "A description of this proposal."),
ID: defaultID,
CommitteeID: tc.committee.ID,
Deadline: firstBlockTime.Add(time.Hour * 24 * 7),
}},
tc.votes,
),
)
proposalPasses, err := keeper.GetProposalResult(ctx, defaultID)
if tc.expectErr {
suite.NotNil(err)
} else {
suite.NoError(err)
suite.Equal(tc.proposalPasses, proposalPasses)
}
})
}
}
func committeeGenState(cdc *codec.Codec, committees []types.Committee, proposals []types.Proposal, votes []types.Vote) app.GenesisState {
gs := types.NewGenesisState(
uint64(len(proposals)+1),
committees,
proposals,
votes,
)
return app.GenesisState{committee.ModuleName: cdc.MustMarshalJSON(gs)}
}
type UnregisteredPubProposal struct {
gov.TextProposal
}
func (UnregisteredPubProposal) ProposalRoute() string { return "unregistered" }
func (UnregisteredPubProposal) ProposalType() string { return "unregistered" }
var _ types.PubProposal = UnregisteredPubProposal{}
func (suite *KeeperTestSuite) TestValidatePubProposal() {
testcases := []struct {
name string
pubProposal types.PubProposal
expectErr bool
}{
{
name: "valid (text proposal)",
pubProposal: gov.NewTextProposal("A Title", "A description of this proposal."),
expectErr: false,
},
{
name: "valid (param change proposal)",
pubProposal: params.NewParameterChangeProposal(
"Change the debt limit",
"This proposal changes the debt limit of the cdp module.",
[]params.ParamChange{{
Subspace: cdptypes.ModuleName,
Key: string(cdptypes.KeyGlobalDebtLimit),
Value: string(types.ModuleCdc.MustMarshalJSON(cs(c("usdx", 100000000000)))),
}},
),
expectErr: false,
},
{
name: "invalid (missing title)",
pubProposal: gov.TextProposal{Description: "A description of this proposal."},
expectErr: true,
},
{
name: "invalid (unregistered)",
pubProposal: UnregisteredPubProposal{gov.TextProposal{Title: "A Title", Description: "A description of this proposal."}},
expectErr: true,
},
{
name: "invalid (nil)",
pubProposal: nil,
expectErr: true,
},
{
name: "invalid (proposal handler fails)",
pubProposal: params.NewParameterChangeProposal(
"A Title",
"A description of this proposal.",
[]params.ParamChange{{
Subspace: "nonsense-subspace",
Key: "nonsense-key",
Value: "nonsense-value",
}},
),
expectErr: true,
},
{
name: "invalid (proposal handler panics)",
pubProposal: params.NewParameterChangeProposal(
"A Title",
"A description of this proposal.",
[]params.ParamChange{{
Subspace: cdptypes.ModuleName,
Key: "nonsense-key", // a valid Subspace but invalid Key will trigger a panic in the paramchange propsal handler
Value: "nonsense-value",
}},
),
expectErr: true,
},
{
name: "invalid (proposal handler fails - invalid json)",
pubProposal: params.NewParameterChangeProposal(
"A Title",
"A description of this proposal.",
[]params.ParamChange{{
Subspace: cdptypes.ModuleName,
Key: string(cdptypes.KeyGlobalDebtLimit),
Value: `{"denom": "usdx",`,
}},
),
expectErr: true,
},
}
for _, tc := range testcases {
suite.Run(tc.name, func() {
err := suite.keeper.ValidatePubProposal(suite.ctx, tc.pubProposal)
if tc.expectErr {
suite.NotNil(err)
} else {
suite.NoError(err)
}
})
}
}
func (suite *KeeperTestSuite) TestCloseExpiredProposals() {
// Setup test state
firstBlockTime := time.Date(1998, time.January, 1, 1, 0, 0, 0, time.UTC)
testGenesis := types.NewGenesisState(
3,
[]types.Committee{
{
ID: 1,
Description: "This committee is for testing.",
Members: suite.addresses[:3],
Permissions: []types.Permission{types.GodPermission{}},
VoteThreshold: d("0.667"),
ProposalDuration: time.Hour * 24 * 7,
},
{
ID: 2,
Members: suite.addresses[2:],
Permissions: nil,
VoteThreshold: d("0.667"),
ProposalDuration: time.Hour * 24 * 7,
},
},
[]types.Proposal{
{
ID: 1,
CommitteeID: 1,
PubProposal: gov.NewTextProposal("A Title", "A description of this proposal."),
Deadline: firstBlockTime.Add(7 * 24 * time.Hour),
},
{
ID: 2,
CommitteeID: 1,
PubProposal: gov.NewTextProposal("Another Title", "A description of this other proposal."),
Deadline: firstBlockTime.Add(21 * 24 * time.Hour),
},
},
[]types.Vote{
{ProposalID: 1, Voter: suite.addresses[0]},
{ProposalID: 1, Voter: suite.addresses[1]},
{ProposalID: 2, Voter: suite.addresses[2]},
},
)
suite.app.InitializeFromGenesisStates(
NewCommitteeGenesisState(suite.app.Codec(), testGenesis),
)
// close proposals
ctx := suite.app.NewContext(true, abci.Header{Height: 1, Time: firstBlockTime})
suite.keeper.CloseExpiredProposals(ctx)
// check
for _, p := range testGenesis.Proposals {
_, found := suite.keeper.GetProposal(ctx, p.ID)
votes := getProposalVoteMap(suite.keeper, ctx)
if ctx.BlockTime().After(p.Deadline) {
suite.False(found)
suite.Empty(votes[p.ID])
} else {
suite.True(found)
suite.NotEmpty(votes[p.ID])
}
}
// close (later time)
ctx = suite.app.NewContext(true, abci.Header{Height: 1, Time: firstBlockTime.Add(7 * 24 * time.Hour)})
suite.keeper.CloseExpiredProposals(ctx)
// check
for _, p := range testGenesis.Proposals {
_, found := suite.keeper.GetProposal(ctx, p.ID)
votes := getProposalVoteMap(suite.keeper, ctx)
if ctx.BlockTime().Equal(p.Deadline) || ctx.BlockTime().After(p.Deadline) {
suite.False(found)
suite.Empty(votes[p.ID])
} else {
suite.True(found)
suite.NotEmpty(votes[p.ID])
}
}
}

View File

@ -0,0 +1,173 @@
package keeper
import (
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/kava-labs/kava/x/committee/types"
)
// NewQuerier creates a new gov Querier instance
func NewQuerier(keeper Keeper) sdk.Querier {
return func(ctx sdk.Context, path []string, req abci.RequestQuery) ([]byte, error) {
switch path[0] {
case types.QueryCommittees:
return queryCommittees(ctx, path[1:], req, keeper)
case types.QueryCommittee:
return queryCommittee(ctx, path[1:], req, keeper)
case types.QueryProposals:
return queryProposals(ctx, path[1:], req, keeper)
case types.QueryProposal:
return queryProposal(ctx, path[1:], req, keeper)
case types.QueryVotes:
return queryVotes(ctx, path[1:], req, keeper)
case types.QueryVote:
return queryVote(ctx, path[1:], req, keeper)
case types.QueryTally:
return queryTally(ctx, path[1:], req, keeper)
default:
return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unknown %s query endpoint", types.ModuleName)
}
}
}
// ------------------------------------------
// Committees
// ------------------------------------------
func queryCommittees(ctx sdk.Context, path []string, _ abci.RequestQuery, keeper Keeper) ([]byte, error) {
committees := keeper.GetCommittees(ctx)
bz, err := codec.MarshalJSONIndent(keeper.cdc, committees)
if err != nil {
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
}
return bz, nil
}
func queryCommittee(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) ([]byte, error) {
var params types.QueryCommitteeParams
err := keeper.cdc.UnmarshalJSON(req.Data, &params)
if err != nil {
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error())
}
committee, found := keeper.GetCommittee(ctx, params.CommitteeID)
if !found {
return nil, sdkerrors.Wrapf(types.ErrUnknownCommittee, "%d", params.CommitteeID)
}
bz, err := codec.MarshalJSONIndent(keeper.cdc, committee)
if err != nil {
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
}
return bz, nil
}
// ------------------------------------------
// Proposals
// ------------------------------------------
func queryProposals(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) ([]byte, error) {
var params types.QueryCommitteeParams
err := keeper.cdc.UnmarshalJSON(req.Data, &params)
if err != nil {
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error())
}
proposals := keeper.GetProposalsByCommittee(ctx, params.CommitteeID)
bz, err := codec.MarshalJSONIndent(keeper.cdc, proposals)
if err != nil {
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
}
return bz, nil
}
func queryProposal(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) ([]byte, error) {
var params types.QueryProposalParams
err := keeper.cdc.UnmarshalJSON(req.Data, &params)
if err != nil {
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error())
}
proposal, found := keeper.GetProposal(ctx, params.ProposalID)
if !found {
return nil, sdkerrors.Wrapf(types.ErrUnknownProposal, "%d", params.ProposalID)
}
bz, err := codec.MarshalJSONIndent(keeper.cdc, proposal)
if err != nil {
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
}
return bz, nil
}
// ------------------------------------------
// Votes
// ------------------------------------------
func queryVotes(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) ([]byte, error) {
var params types.QueryProposalParams
err := keeper.cdc.UnmarshalJSON(req.Data, &params)
if err != nil {
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error())
}
votes := keeper.GetVotesByProposal(ctx, params.ProposalID)
bz, err := codec.MarshalJSONIndent(keeper.cdc, votes)
if err != nil {
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
}
return bz, nil
}
func queryVote(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) ([]byte, error) {
var params types.QueryVoteParams
err := keeper.cdc.UnmarshalJSON(req.Data, &params)
if err != nil {
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error())
}
vote, found := keeper.GetVote(ctx, params.ProposalID, params.Voter)
if !found {
return nil, sdkerrors.Wrapf(types.ErrUnknownVote, "proposal id: %d, voter: %s", params.ProposalID, params.Voter)
}
bz, err := codec.MarshalJSONIndent(keeper.cdc, vote)
if err != nil {
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
}
return bz, nil
}
// ------------------------------------------
// Tally
// ------------------------------------------
func queryTally(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) ([]byte, error) {
var params types.QueryProposalParams
err := keeper.cdc.UnmarshalJSON(req.Data, &params)
if err != nil {
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error())
}
_, found := keeper.GetProposal(ctx, params.ProposalID)
if !found {
return nil, sdkerrors.Wrapf(types.ErrUnknownProposal, "%d", params.ProposalID)
}
numVotes := keeper.TallyVotes(ctx, params.ProposalID)
bz, err := codec.MarshalJSONIndent(keeper.cdc, numVotes)
if err != nil {
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
}
return bz, nil
}

View File

@ -0,0 +1,242 @@
package keeper_test
import (
"strings"
"testing"
"time"
"github.com/stretchr/testify/suite"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/gov"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/committee/keeper"
"github.com/kava-labs/kava/x/committee/types"
)
const (
custom = "custom"
)
var testTime time.Time = time.Date(1998, time.January, 1, 0, 0, 0, 0, time.UTC)
type QuerierTestSuite struct {
suite.Suite
keeper keeper.Keeper
app app.TestApp
ctx sdk.Context
cdc *codec.Codec
querier sdk.Querier
addresses []sdk.AccAddress
testGenesis types.GenesisState
votes map[uint64]([]types.Vote)
}
func (suite *QuerierTestSuite) SetupTest() {
suite.app = app.NewTestApp()
suite.keeper = suite.app.GetCommitteeKeeper()
suite.ctx = suite.app.NewContext(true, abci.Header{})
suite.cdc = suite.app.Codec()
suite.querier = keeper.NewQuerier(suite.keeper)
_, suite.addresses = app.GeneratePrivKeyAddressPairs(5)
suite.testGenesis = types.NewGenesisState(
3,
[]types.Committee{
{
ID: 1,
Description: "This committee is for testing.",
Members: suite.addresses[:3],
Permissions: []types.Permission{types.GodPermission{}},
VoteThreshold: d("0.667"),
ProposalDuration: time.Hour * 24 * 7,
},
{
ID: 2,
Members: suite.addresses[2:],
Permissions: nil,
VoteThreshold: d("0.667"),
ProposalDuration: time.Hour * 24 * 7,
},
},
[]types.Proposal{
{ID: 1, CommitteeID: 1, PubProposal: gov.NewTextProposal("A Title", "A description of this proposal."), Deadline: testTime.Add(7 * 24 * time.Hour)},
{ID: 2, CommitteeID: 1, PubProposal: gov.NewTextProposal("Another Title", "A description of this other proposal."), Deadline: testTime.Add(21 * 24 * time.Hour)},
},
[]types.Vote{
{ProposalID: 1, Voter: suite.addresses[0]},
{ProposalID: 1, Voter: suite.addresses[1]},
{ProposalID: 2, Voter: suite.addresses[2]},
},
)
suite.app.InitializeFromGenesisStates(
NewCommitteeGenesisState(suite.cdc, suite.testGenesis),
)
suite.votes = getProposalVoteMap(suite.keeper, suite.ctx)
}
func (suite *QuerierTestSuite) TestQueryCommittees() {
ctx := suite.ctx.WithIsCheckTx(false)
// Set up request query
query := abci.RequestQuery{
Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryCommittees}, "/"),
}
// Execute query and check the []byte result
bz, err := suite.querier(ctx, []string{types.QueryCommittees}, query)
suite.NoError(err)
suite.NotNil(bz)
// Unmarshal the bytes
var committees []types.Committee
suite.NoError(suite.cdc.UnmarshalJSON(bz, &committees))
// Check
suite.Equal(suite.testGenesis.Committees, committees)
}
func (suite *QuerierTestSuite) TestQueryCommittee() {
ctx := suite.ctx.WithIsCheckTx(false) // ?
// Set up request query
query := abci.RequestQuery{
Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryCommittee}, "/"),
Data: suite.cdc.MustMarshalJSON(types.NewQueryCommitteeParams(suite.testGenesis.Committees[0].ID)),
}
// Execute query and check the []byte result
bz, err := suite.querier(ctx, []string{types.QueryCommittee}, query)
suite.NoError(err)
suite.NotNil(bz)
// Unmarshal the bytes
var committee types.Committee
suite.NoError(suite.cdc.UnmarshalJSON(bz, &committee))
// Check
suite.Equal(suite.testGenesis.Committees[0], committee)
}
func (suite *QuerierTestSuite) TestQueryProposals() {
ctx := suite.ctx.WithIsCheckTx(false)
// Set up request query
comID := suite.testGenesis.Proposals[0].CommitteeID
query := abci.RequestQuery{
Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryProposals}, "/"),
Data: suite.cdc.MustMarshalJSON(types.NewQueryCommitteeParams(comID)),
}
// Execute query and check the []byte result
bz, err := suite.querier(ctx, []string{types.QueryProposals}, query)
suite.NoError(err)
suite.NotNil(bz)
// Unmarshal the bytes
var proposals []types.Proposal
suite.NoError(suite.cdc.UnmarshalJSON(bz, &proposals))
// Check
expectedProposals := []types.Proposal{}
for _, p := range suite.testGenesis.Proposals {
if p.CommitteeID == comID {
expectedProposals = append(expectedProposals, p)
}
}
suite.Equal(expectedProposals, proposals)
}
func (suite *QuerierTestSuite) TestQueryProposal() {
ctx := suite.ctx.WithIsCheckTx(false) // ?
// Set up request query
query := abci.RequestQuery{
Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryProposal}, "/"),
Data: suite.cdc.MustMarshalJSON(types.NewQueryProposalParams(suite.testGenesis.Proposals[0].ID)),
}
// Execute query and check the []byte result
bz, err := suite.querier(ctx, []string{types.QueryProposal}, query)
suite.NoError(err)
suite.NotNil(bz)
// Unmarshal the bytes
var proposal types.Proposal
suite.NoError(suite.cdc.UnmarshalJSON(bz, &proposal))
// Check
suite.Equal(suite.testGenesis.Proposals[0], proposal)
}
func (suite *QuerierTestSuite) TestQueryVotes() {
ctx := suite.ctx.WithIsCheckTx(false)
// Set up request query
propID := suite.testGenesis.Proposals[0].ID
query := abci.RequestQuery{
Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryVotes}, "/"),
Data: suite.cdc.MustMarshalJSON(types.NewQueryProposalParams(propID)),
}
// Execute query and check the []byte result
bz, err := suite.querier(ctx, []string{types.QueryVotes}, query)
suite.NoError(err)
suite.NotNil(bz)
// Unmarshal the bytes
var votes []types.Vote
suite.NoError(suite.cdc.UnmarshalJSON(bz, &votes))
// Check
suite.Equal(suite.votes[propID], votes)
}
func (suite *QuerierTestSuite) TestQueryVote() {
ctx := suite.ctx.WithIsCheckTx(false) // ?
// Set up request query
propID := suite.testGenesis.Proposals[0].ID
query := abci.RequestQuery{
Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryVote}, "/"),
Data: suite.cdc.MustMarshalJSON(types.NewQueryVoteParams(propID, suite.votes[propID][0].Voter)),
}
// Execute query and check the []byte result
bz, err := suite.querier(ctx, []string{types.QueryVote}, query)
suite.NoError(err)
suite.NotNil(bz)
// Unmarshal the bytes
var vote types.Vote
suite.NoError(suite.cdc.UnmarshalJSON(bz, &vote))
// Check
suite.Equal(suite.votes[propID][0], vote)
}
func (suite *QuerierTestSuite) TestQueryTally() {
ctx := suite.ctx.WithIsCheckTx(false) // ?
// Set up request query
propID := suite.testGenesis.Proposals[0].ID
query := abci.RequestQuery{
Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryTally}, "/"),
Data: suite.cdc.MustMarshalJSON(types.NewQueryProposalParams(propID)),
}
// Execute query and check the []byte result
bz, err := suite.querier(ctx, []string{types.QueryTally}, query)
suite.NoError(err)
suite.NotNil(bz)
// Unmarshal the bytes
var tally int64
suite.NoError(suite.cdc.UnmarshalJSON(bz, &tally))
// Check
suite.Equal(int64(len(suite.votes[propID])), tally)
}
func TestQuerierTestSuite(t *testing.T) {
suite.Run(t, new(QuerierTestSuite))
}

169
x/committee/module.go Normal file
View File

@ -0,0 +1,169 @@
package committee
import (
"encoding/json"
"math/rand"
"github.com/gorilla/mux"
"github.com/spf13/cobra"
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/module"
"github.com/cosmos/cosmos-sdk/x/auth"
sim "github.com/cosmos/cosmos-sdk/x/simulation"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/kava-labs/kava/x/committee/client/cli"
"github.com/kava-labs/kava/x/committee/client/rest"
"github.com/kava-labs/kava/x/committee/simulation"
)
var (
_ module.AppModule = AppModule{}
_ module.AppModuleBasic = AppModuleBasic{}
_ module.AppModuleSimulation = AppModule{}
)
// AppModuleBasic app module basics object
type AppModuleBasic struct{}
// Name gets the module name
func (AppModuleBasic) Name() string {
return ModuleName
}
// RegisterCodec register module codec
func (AppModuleBasic) RegisterCodec(cdc *codec.Codec) {
RegisterCodec(cdc)
}
// DefaultGenesis default genesis state
func (AppModuleBasic) DefaultGenesis() json.RawMessage {
return ModuleCdc.MustMarshalJSON(DefaultGenesisState())
}
// ValidateGenesis module validate genesis
func (AppModuleBasic) ValidateGenesis(bz json.RawMessage) error {
var gs GenesisState
err := ModuleCdc.UnmarshalJSON(bz, &gs)
if err != nil {
return err
}
return gs.Validate()
}
// RegisterRESTRoutes registers the REST routes for the module.
func (AppModuleBasic) RegisterRESTRoutes(ctx context.CLIContext, rtr *mux.Router) {
rest.RegisterRoutes(ctx, rtr)
}
// GetTxCmd returns the root tx command for the module.
func (AppModuleBasic) GetTxCmd(cdc *codec.Codec) *cobra.Command {
return cli.GetTxCmd(StoreKey, cdc)
}
// GetQueryCmd returns the root query command for the module.
func (AppModuleBasic) GetQueryCmd(cdc *codec.Codec) *cobra.Command {
return cli.GetQueryCmd(StoreKey, cdc)
}
//____________________________________________________________________________
// AppModule app module type
type AppModule struct {
AppModuleBasic
keeper Keeper
accountKeeper auth.AccountKeeper
}
// NewAppModule creates a new AppModule object
func NewAppModule(keeper Keeper, accountKeeper auth.AccountKeeper) AppModule {
return AppModule{
AppModuleBasic: AppModuleBasic{},
keeper: keeper,
accountKeeper: accountKeeper,
}
}
// Name module name
func (AppModule) Name() string {
return ModuleName
}
// RegisterInvariants register module invariants
func (AppModule) RegisterInvariants(_ sdk.InvariantRegistry) {}
// Route module message route name
func (AppModule) Route() string {
return RouterKey
}
// NewHandler module handler
func (am AppModule) NewHandler() sdk.Handler {
return NewHandler(am.keeper)
}
// QuerierRoute module querier route name
func (AppModule) QuerierRoute() string {
return QuerierRoute
}
// NewQuerierHandler module querier
func (am AppModule) NewQuerierHandler() sdk.Querier {
return NewQuerier(am.keeper)
}
// InitGenesis module init-genesis
func (am AppModule) InitGenesis(ctx sdk.Context, data json.RawMessage) []abci.ValidatorUpdate {
var genesisState GenesisState
ModuleCdc.MustUnmarshalJSON(data, &genesisState)
InitGenesis(ctx, am.keeper, genesisState)
return []abci.ValidatorUpdate{}
}
// ExportGenesis module export genesis
func (am AppModule) ExportGenesis(ctx sdk.Context) json.RawMessage {
gs := ExportGenesis(ctx, am.keeper)
return ModuleCdc.MustMarshalJSON(gs)
}
// BeginBlock module begin-block
func (am AppModule) BeginBlock(ctx sdk.Context, req abci.RequestBeginBlock) {
BeginBlocker(ctx, req, am.keeper)
}
// EndBlock module end-block
func (am AppModule) EndBlock(_ sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate {
return []abci.ValidatorUpdate{}
}
//____________________________________________________________________________
// GenerateGenesisState creates a randomized GenState for the module
func (AppModuleBasic) GenerateGenesisState(simState *module.SimulationState) {
simulation.RandomizedGenState(simState)
}
// TODO
func (AppModuleBasic) ProposalContents(_ module.SimulationState) []sim.WeightedProposalContent {
return nil
}
// RandomizedParams returns functions that generate params for the module.
func (AppModuleBasic) RandomizedParams(r *rand.Rand) []sim.ParamChange {
return nil
}
// RegisterStoreDecoder registers a decoder for the module's types
func (AppModuleBasic) RegisterStoreDecoder(sdr sdk.StoreDecoderRegistry) {
sdr[StoreKey] = simulation.DecodeStore
}
// WeightedOperations returns the all the auction module operations with their respective weights.
func (am AppModule) WeightedOperations(simState module.SimulationState) []sim.WeightedOperation {
return nil // TODO simulation.WeightedOperations(simState.AppParams, simState.Cdc, am.accountKeeper, am.keeper)
}

View File

@ -0,0 +1,58 @@
package committee
import (
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
)
func NewProposalHandler(k Keeper) govtypes.Handler {
return func(ctx sdk.Context, content govtypes.Content) error {
switch c := content.(type) {
case CommitteeChangeProposal:
return handleCommitteeChangeProposal(ctx, k, c)
case CommitteeDeleteProposal:
return handleCommitteeDeleteProposal(ctx, k, c)
default:
return sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized %s proposal content type: %T", ModuleName, c)
}
}
}
func handleCommitteeChangeProposal(ctx sdk.Context, k Keeper, committeeProposal CommitteeChangeProposal) error {
if err := committeeProposal.ValidateBasic(); err != nil {
return sdkerrors.Wrap(ErrInvalidPubProposal, err.Error())
}
// Remove all committee's ongoing proposals
k.IterateProposals(ctx, func(p Proposal) bool {
if p.CommitteeID != committeeProposal.NewCommittee.ID {
return false
}
k.DeleteProposalAndVotes(ctx, p.ID)
return false
})
// update/create the committee
k.SetCommittee(ctx, committeeProposal.NewCommittee)
return nil
}
func handleCommitteeDeleteProposal(ctx sdk.Context, k Keeper, committeeProposal CommitteeDeleteProposal) error {
if err := committeeProposal.ValidateBasic(); err != nil {
return sdkerrors.Wrap(ErrInvalidPubProposal, err.Error())
}
// Remove all committee's ongoing proposals
k.IterateProposals(ctx, func(p Proposal) bool {
if p.CommitteeID != committeeProposal.CommitteeID {
return false
}
k.DeleteProposalAndVotes(ctx, p.ID)
return false
})
k.DeleteCommittee(ctx, committeeProposal.CommitteeID)
return nil
}

View File

@ -0,0 +1,225 @@
package committee_test
import (
"testing"
"time"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/gov"
"github.com/stretchr/testify/suite"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/committee"
"github.com/kava-labs/kava/x/committee/types"
)
var testTime time.Time = time.Date(1998, time.January, 1, 0, 0, 0, 0, time.UTC)
func NewCommitteeGenState(cdc *codec.Codec, gs committee.GenesisState) app.GenesisState {
return app.GenesisState{committee.ModuleName: cdc.MustMarshalJSON(gs)}
}
type ProposalHandlerTestSuite struct {
suite.Suite
keeper committee.Keeper
app app.TestApp
ctx sdk.Context
addresses []sdk.AccAddress
testGenesis committee.GenesisState
}
func (suite *ProposalHandlerTestSuite) SetupTest() {
_, suite.addresses = app.GeneratePrivKeyAddressPairs(5)
suite.testGenesis = committee.NewGenesisState(
2,
[]committee.Committee{
{
ID: 1,
Description: "This committee is for testing.",
Members: suite.addresses[:3],
Permissions: []types.Permission{types.GodPermission{}},
VoteThreshold: d("0.667"),
ProposalDuration: time.Hour * 24 * 7,
},
{
ID: 2,
Members: suite.addresses[2:],
Permissions: nil,
VoteThreshold: d("0.667"),
ProposalDuration: time.Hour * 24 * 7,
},
},
[]committee.Proposal{
{ID: 1, CommitteeID: 1, PubProposal: gov.NewTextProposal("A Title", "A description of this proposal."), Deadline: testTime.Add(7 * 24 * time.Hour)},
},
[]committee.Vote{
{ProposalID: 1, Voter: suite.addresses[0]},
},
)
}
func (suite *ProposalHandlerTestSuite) TestProposalHandler_ChangeCommittee() {
testCases := []struct {
name string
proposal committee.CommitteeChangeProposal
expectPass bool
}{
{
name: "add new",
proposal: committee.NewCommitteeChangeProposal(
"A Title",
"A proposal description.",
committee.Committee{
ID: 34,
Members: suite.addresses[:1],
VoteThreshold: d("1"),
ProposalDuration: time.Hour * 24,
},
),
expectPass: true,
},
{
name: "update",
proposal: committee.NewCommitteeChangeProposal(
"A Title",
"A proposal description.",
committee.Committee{
ID: suite.testGenesis.Committees[0].ID,
Members: suite.addresses, // add new members
Permissions: suite.testGenesis.Committees[0].Permissions,
VoteThreshold: suite.testGenesis.Committees[0].VoteThreshold,
ProposalDuration: suite.testGenesis.Committees[0].ProposalDuration,
},
),
expectPass: true,
},
{
name: "invalid title",
proposal: committee.NewCommitteeChangeProposal(
"A Title That Is Much Too Long And Really Quite Unreasonable Given That It Is Trying To Fullfill The Roll Of An Acceptable Governance Proposal Title That Should Succinctly Communicate The Goal And Contents Of The Proposed Proposal To All Parties Involved",
"A proposal description.",
suite.testGenesis.Committees[0],
),
expectPass: false,
},
{
name: "invalid committee",
proposal: committee.NewCommitteeChangeProposal(
"A Title",
"A proposal description.",
committee.Committee{
ID: suite.testGenesis.Committees[0].ID,
Members: append(suite.addresses, suite.addresses[0]), // duplicate address
Permissions: suite.testGenesis.Committees[0].Permissions,
VoteThreshold: suite.testGenesis.Committees[0].VoteThreshold,
ProposalDuration: suite.testGenesis.Committees[0].ProposalDuration,
},
),
expectPass: false,
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
// Setup
suite.app = app.NewTestApp()
suite.keeper = suite.app.GetCommitteeKeeper()
suite.app = suite.app.InitializeFromGenesisStates(
NewCommitteeGenState(suite.app.Codec(), suite.testGenesis),
)
suite.ctx = suite.app.NewContext(true, abci.Header{Height: 1, Time: testTime})
handler := committee.NewProposalHandler(suite.keeper)
oldProposals := suite.keeper.GetProposalsByCommittee(suite.ctx, tc.proposal.NewCommittee.ID)
// Run
err := handler(suite.ctx, tc.proposal)
// Check
if tc.expectPass {
suite.NoError(err)
// check committee is accurate
actualCom, found := suite.keeper.GetCommittee(suite.ctx, tc.proposal.NewCommittee.ID)
suite.True(found)
suite.Equal(tc.proposal.NewCommittee, actualCom)
// check proposals and votes for this committee have been removed
suite.Empty(suite.keeper.GetProposalsByCommittee(suite.ctx, tc.proposal.NewCommittee.ID))
for _, p := range oldProposals {
suite.Empty(suite.keeper.GetVotesByProposal(suite.ctx, p.ID))
}
} else {
suite.Error(err)
suite.Equal(suite.testGenesis, committee.ExportGenesis(suite.ctx, suite.keeper))
}
})
}
}
func (suite *ProposalHandlerTestSuite) TestProposalHandler_DeleteCommittee() {
testCases := []struct {
name string
proposal committee.CommitteeDeleteProposal
expectPass bool
}{
{
name: "normal",
proposal: committee.NewCommitteeDeleteProposal(
"A Title",
"A proposal description.",
suite.testGenesis.Committees[0].ID,
),
expectPass: true,
},
{
name: "invalid title",
proposal: committee.NewCommitteeDeleteProposal(
"A Title That Is Much Too Long And Really Quite Unreasonable Given That It Is Trying To Fullfill The Roll Of An Acceptable Governance Proposal Title That Should Succinctly Communicate The Goal And Contents Of The Proposed Proposal To All Parties Involved",
"A proposal description.",
suite.testGenesis.Committees[1].ID,
),
expectPass: false,
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
// Setup
suite.app = app.NewTestApp()
suite.keeper = suite.app.GetCommitteeKeeper()
suite.app = suite.app.InitializeFromGenesisStates(
NewCommitteeGenState(suite.app.Codec(), suite.testGenesis),
)
suite.ctx = suite.app.NewContext(true, abci.Header{Height: 1, Time: testTime})
handler := committee.NewProposalHandler(suite.keeper)
oldProposals := suite.keeper.GetProposalsByCommittee(suite.ctx, tc.proposal.CommitteeID)
// Run
err := handler(suite.ctx, tc.proposal)
// Check
if tc.expectPass {
suite.NoError(err)
// check committee has been removed
_, found := suite.keeper.GetCommittee(suite.ctx, tc.proposal.CommitteeID)
suite.False(found)
// check proposals and votes for this committee have been removed
suite.Empty(suite.keeper.GetProposalsByCommittee(suite.ctx, tc.proposal.CommitteeID))
for _, p := range oldProposals {
suite.Empty(suite.keeper.GetVotesByProposal(suite.ctx, p.ID))
}
} else {
suite.Error(err)
suite.Equal(suite.testGenesis, committee.ExportGenesis(suite.ctx, suite.keeper))
}
})
}
}
func TestProposalHandlerTestSuite(t *testing.T) {
suite.Run(t, new(ProposalHandlerTestSuite))
}

View File

@ -0,0 +1,12 @@
package simulation
import (
"github.com/cosmos/cosmos-sdk/codec"
"github.com/tendermint/tendermint/libs/kv"
)
// DecodeStore unmarshals the KVPair's Value to the corresponding module type
func DecodeStore(cdc *codec.Codec, kvA, kvB kv.Pair) string {
// TODO implement this
return ""
}

View File

@ -0,0 +1,22 @@
package simulation
import (
"fmt"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/types/module"
"github.com/kava-labs/kava/x/committee/types"
)
// RandomizedGenState generates a random GenesisState for the module
func RandomizedGenState(simState *module.SimulationState) {
// TODO implement this fully
// - randomly generating the genesis params
// - overwriting with genesis provided to simulation
genesisState := types.DefaultGenesisState()
fmt.Printf("Selected randomly generated %s parameters:\n%s\n", types.ModuleName, codec.MustMarshalJSONIndent(simState.Cdc, genesisState))
simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(genesisState)
}

View File

@ -0,0 +1,14 @@
package simulation
import (
"math/rand"
"github.com/cosmos/cosmos-sdk/x/simulation"
)
// ParamChanges defines the parameters that can be modified by param change proposals
// on the simulation
func ParamChanges(r *rand.Rand) []simulation.ParamChange {
// TODO implement this
return []simulation.ParamChange{}
}

View File

@ -0,0 +1,26 @@
# `committee`
## Table of Contents
## Overview
The `x/committee` module is an additional governance module to `cosmos-sdk/x/gov`.
It allows groups of accounts to vote on and enact proposals without a full chain governance vote. Certain proposal types can then be decided on quickly in emergency situations, or low risk parameter updates can be delegated to a smaller group of individuals.
Committees work with "proposals", using the same type from the `gov` module so they are compatible with all existing proposal types such as param changes, or community pool spend, or text proposals.
Committees have members and permissions.
Members vote on proposals, with just simple one vote per member, no deposits or slashing. More sophisticated voting could be added.
Permissions scope the allowed set of proposals a committee can enact. For example:
- allow the committee to only change the cdp `CircuitBreaker` param.
- allow the committee to change auction bid increments, but only within the range [0, 0.1]
- allow the committee to only disable cdp msg types, but not staking or gov
A permission acts as a filter for incoming gov proposals, rejecting them if they do not pass. A permission can be any type with a method `Allows(p Proposal) bool`. They reject all proposals that they don't explicitly allow.
This allows permissions to be parameterized to allow fine grained control specified at runtime. For example a generic parameter permission type can allow a committee to only change a particular param, or only change params within a certain percentage.

View File

@ -0,0 +1,56 @@
package types
import (
"github.com/cosmos/cosmos-sdk/codec"
distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types"
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
paramstypes "github.com/cosmos/cosmos-sdk/x/params/types"
)
// ModuleCdc is a generic codec to be used throughout module
var ModuleCdc *codec.Codec
func init() {
cdc := codec.New()
RegisterCodec(cdc)
ModuleCdc = cdc
// ModuleCdc is not sealed so that other modules can register their own pubproposal and/or permission types.
// Register external module pubproposal types. Ideally these would be registered within the modules' types pkg init function.
// However registration happens here as a work-around.
RegisterProposalTypeCodec(distrtypes.CommunityPoolSpendProposal{}, "cosmos-sdk/CommunityPoolSpendProposal")
RegisterProposalTypeCodec(paramstypes.ParameterChangeProposal{}, "cosmos-sdk/ParameterChangeProposal")
RegisterProposalTypeCodec(govtypes.TextProposal{}, "cosmos-sdk/TextProposal")
}
// RegisterCodec registers the necessary types for the module
func RegisterCodec(cdc *codec.Codec) {
// Proposals
cdc.RegisterInterface((*PubProposal)(nil), nil)
cdc.RegisterConcrete(CommitteeChangeProposal{}, "kava/CommitteeChangeProposal", nil)
cdc.RegisterConcrete(CommitteeDeleteProposal{}, "kava/CommitteeDeleteProposal", nil)
// Permissions
cdc.RegisterInterface((*Permission)(nil), nil)
cdc.RegisterConcrete(GodPermission{}, "kava/GodPermission", nil)
cdc.RegisterConcrete(ParamChangePermission{}, "kava/ParamChangePermission", nil)
cdc.RegisterConcrete(TextPermission{}, "kava/TextPermission", nil)
// Msgs
cdc.RegisterConcrete(MsgSubmitProposal{}, "kava/MsgSubmitProposal", nil)
cdc.RegisterConcrete(MsgVote{}, "kava/MsgVote", nil)
}
// RegisterPermissionTypeCodec allows external modules to register their own permission types on
// the internal ModuleCdc. This allows the MsgSubmitProposal to be correctly Amino encoded and
// decoded (when the msg contains a CommitteeChangeProposal).
func RegisterPermissionTypeCodec(o interface{}, name string) {
ModuleCdc.RegisterConcrete(o, name, nil)
}
// RegisterProposalTypeCodec allows external modules to register their own pubproposal types on the
// internal ModuleCdc. This allows the MsgSubmitProposal to be correctly Amino encoded and decoded.
func RegisterProposalTypeCodec(o interface{}, name string) {
ModuleCdc.RegisterConcrete(o, name, nil)
}

View File

@ -0,0 +1,140 @@
package types
import (
"fmt"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
"gopkg.in/yaml.v2"
)
const MaxCommitteeDescriptionLength int = 512
// ------------------------------------------
// Committees
// ------------------------------------------
// A Committee is a collection of addresses that are allowed to vote and enact any governance proposal that passes their permissions.
type Committee struct {
ID uint64 `json:"id" yaml:"id"`
Description string `json:"description" yaml:"description"`
Members []sdk.AccAddress `json:"members" yaml:"members"`
Permissions []Permission `json:"permissions" yaml:"permissions"`
VoteThreshold sdk.Dec `json:"vote_threshold" yaml:"vote_threshold"` // Smallest percentage of members that must vote for a proposal to pass.
ProposalDuration time.Duration `json:"proposal_duration" yaml:"proposal_duration"` // The length of time a proposal remains active for. Proposals will close earlier if they get enough votes.
}
func NewCommittee(id uint64, description string, members []sdk.AccAddress, permissions []Permission, threshold sdk.Dec, duration time.Duration) Committee {
return Committee{
ID: id,
Description: description,
Members: members,
Permissions: permissions,
VoteThreshold: threshold,
ProposalDuration: duration,
}
}
func (c Committee) HasMember(addr sdk.AccAddress) bool {
for _, m := range c.Members {
if m.Equals(addr) {
return true
}
}
return false
}
// HasPermissionsFor returns whether the committee is authorized to enact a proposal.
// As long as one permission allows the proposal then it goes through. Its the OR of all permissions.
func (c Committee) HasPermissionsFor(proposal PubProposal) bool {
for _, p := range c.Permissions {
if p.Allows(proposal) {
return true
}
}
return false
}
func (c Committee) Validate() error {
addressMap := make(map[string]bool, len(c.Members))
for _, m := range c.Members {
// check there are no duplicate members
if _, ok := addressMap[m.String()]; ok {
return fmt.Errorf("duplicate member found in committee, %s", m)
}
// check for valid addresses
if m.Empty() {
return fmt.Errorf("committee %d invalid: found empty member address", c.ID)
}
addressMap[m.String()] = true
}
if len(c.Members) == 0 {
return fmt.Errorf("committee %d invalid: cannot have zero members", c.ID)
}
if len(c.Description) > MaxCommitteeDescriptionLength {
return fmt.Errorf("invalid description")
}
// threshold must be in the range (0,1]
if c.VoteThreshold.IsNil() || c.VoteThreshold.LTE(sdk.ZeroDec()) || c.VoteThreshold.GT(sdk.NewDec(1)) {
return fmt.Errorf("invalid threshold")
}
if c.ProposalDuration < 0 {
return fmt.Errorf("invalid proposal duration")
}
return nil
}
// ------------------------------------------
// Proposals
// ------------------------------------------
// PubProposal is the interface that all proposals must fulfill to be submitted to a committee.
// Proposal types can be created external to this module. For example a ParamChangeProposal, or CommunityPoolSpendProposal.
// It is pinned to the equivalent type in the gov module to create compatability between proposal types.
type PubProposal govtypes.Content
// Proposal is an internal record of a governance proposal submitted to a committee.
type Proposal struct {
PubProposal `json:"pub_proposal" yaml:"pub_proposal"`
ID uint64 `json:"id" yaml:"id"`
CommitteeID uint64 `json:"committee_id" yaml:"committee_id"`
Deadline time.Time `json:"deadline" yaml:"deadline"`
}
func NewProposal(pubProposal PubProposal, id uint64, committeeID uint64, deadline time.Time) Proposal {
return Proposal{
PubProposal: pubProposal,
ID: id,
CommitteeID: committeeID,
Deadline: deadline,
}
}
// HasExpiredBy calculates if the proposal will have expired by a certain time.
// All votes must be cast before deadline, those cast at time == deadline are not valid
func (p Proposal) HasExpiredBy(time time.Time) bool {
return !time.Before(p.Deadline)
}
// String implements the fmt.Stringer interface, and importantly overrides the String methods inherited from the embedded PubProposal type.
func (p Proposal) String() string {
bz, _ := yaml.Marshal(p)
return string(bz)
}
// ------------------------------------------
// Votes
// ------------------------------------------
type Vote struct {
ProposalID uint64 `json:"proposal_id" yaml:"proposal_id"`
Voter sdk.AccAddress `json:"voter" yaml:"voter"`
}

View File

@ -0,0 +1,189 @@
package types
import (
"testing"
"time"
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
paramstypes "github.com/cosmos/cosmos-sdk/x/params/types"
"github.com/stretchr/testify/suite"
)
var _ PubProposal = UnregisteredPubProposal{}
type UnregisteredPubProposal struct {
govtypes.TextProposal
}
func (UnregisteredPubProposal) ProposalRoute() string { return "unregistered" }
func (UnregisteredPubProposal) ProposalType() string { return "unregistered" }
type TypesTestSuite struct {
suite.Suite
}
func (suite *TypesTestSuite) TestCommittee_HasPermissionsFor() {
testcases := []struct {
name string
permissions []Permission
pubProposal PubProposal
expectHasPermissions bool
}{
{
name: "normal (single permission)",
permissions: []Permission{ParamChangePermission{
AllowedParams: AllowedParams{
{
Subspace: "cdp",
Key: "DebtThreshold",
},
}}},
pubProposal: paramstypes.NewParameterChangeProposal(
"A Title",
"A description of this proposal.",
[]paramstypes.ParamChange{
{
Subspace: "cdp",
Key: "DebtThreshold",
Value: `{"denom": "usdx", "amount": "1000000"}`,
},
},
),
expectHasPermissions: true,
},
{
name: "normal (multiple permissions)",
permissions: []Permission{
ParamChangePermission{
AllowedParams: AllowedParams{
{
Subspace: "cdp",
Key: "DebtThreshold",
},
}},
TextPermission{},
},
pubProposal: govtypes.NewTextProposal("A Proposal Title", "A description of this proposal"),
expectHasPermissions: true,
},
{
name: "overruling permission",
permissions: []Permission{
ParamChangePermission{
AllowedParams: AllowedParams{
{
Subspace: "cdp",
Key: "DebtThreshold",
},
}},
GodPermission{},
},
pubProposal: paramstypes.NewParameterChangeProposal(
"A Title",
"A description of this proposal.",
[]paramstypes.ParamChange{
{
Subspace: "cdp",
Key: "CollateralParams",
Value: `[]`,
},
},
),
expectHasPermissions: true,
},
{
name: "no permissions",
permissions: nil,
pubProposal: paramstypes.NewParameterChangeProposal(
"A Title",
"A description of this proposal.",
[]paramstypes.ParamChange{
{
Subspace: "cdp",
Key: "CollateralParams",
Value: `[]`,
},
},
),
expectHasPermissions: false,
},
{
name: "split permissions",
// These permissions looks like they allow the param change proposal, however a proposal must pass a single permission independently of others.
permissions: []Permission{
ParamChangePermission{
AllowedParams: AllowedParams{
{
Subspace: "cdp",
Key: "DebtThreshold",
},
}},
ParamChangePermission{
AllowedParams: AllowedParams{
{
Subspace: "cdp",
Key: "DebtParams",
},
}},
},
pubProposal: paramstypes.NewParameterChangeProposal(
"A Title",
"A description of this proposal.",
[]paramstypes.ParamChange{
{
Subspace: "cdp",
Key: "DebtThreshold",
Value: `{"denom": "usdx", "amount": "1000000"}`,
},
{
Subspace: "cdp",
Key: "DebtParams",
Value: `[]`,
},
},
),
expectHasPermissions: false,
},
{
name: "unregistered proposal",
permissions: []Permission{
ParamChangePermission{
AllowedParams: AllowedParams{
{
Subspace: "cdp",
Key: "DebtThreshold",
},
}},
},
pubProposal: UnregisteredPubProposal{govtypes.TextProposal{"A Title", "A description."}},
expectHasPermissions: false,
},
}
for _, tc := range testcases {
suite.Run(tc.name, func() {
com := NewCommittee(
12,
"a description of this committee",
nil,
tc.permissions,
d("0.5"),
24*time.Hour,
)
suite.Equal(
tc.expectHasPermissions,
com.HasPermissionsFor(tc.pubProposal),
)
})
}
}
func TestTypesTestSuite(t *testing.T) {
suite.Run(t, new(TypesTestSuite))
}

View File

@ -0,0 +1,16 @@
package types
import (
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)
var (
ErrUnknownCommittee = sdkerrors.Register(ModuleName, 2, "committee not found")
ErrInvalidCommittee = sdkerrors.Register(ModuleName, 3, "invalid committee")
ErrUnknownProposal = sdkerrors.Register(ModuleName, 4, "proposal not found")
ErrProposalExpired = sdkerrors.Register(ModuleName, 5, "proposal expired")
ErrInvalidPubProposal = sdkerrors.Register(ModuleName, 6, "invalid pubproposal")
ErrUnknownVote = sdkerrors.Register(ModuleName, 7, "vote not found")
ErrInvalidGenesis = sdkerrors.Register(ModuleName, 8, "invalid genesis")
ErrNoProposalHandlerExists = sdkerrors.Register(ModuleName, 9, "pubproposal has no corresponding handler")
)

View File

@ -0,0 +1,16 @@
package types
// Module event types
const (
EventTypeProposalSubmit = "proposal_submit"
EventTypeProposalClose = "proposal_close"
EventTypeProposalVote = "proposal_vote"
AttributeValueCategory = "committee"
AttributeKeyCommitteeID = "committee_id"
AttributeKeyProposalID = "proposal_id"
AttributeKeyProposalCloseStatus = "status"
AttributeValueProposalPassed = "proposal_passed"
AttributeValueProposalTimeout = "proposal_timeout"
AttributeValueProposalFailed = "proposal_failed"
)

View File

@ -0,0 +1,105 @@
package types
import (
"bytes"
"fmt"
)
// DefaultNextProposalID is the starting poiint for proposal IDs.
const DefaultNextProposalID uint64 = 1
// GenesisState is state that must be provided at chain genesis.
type GenesisState struct {
NextProposalID uint64 `json:"next_proposal_id" yaml:"next_proposal_id"`
Committees []Committee `json:"committees" yaml:"committees"`
Proposals []Proposal `json:"proposals" yaml:"proposals"`
Votes []Vote `json:"votes" yaml:"votes"`
}
// NewGenesisState returns a new genesis state object for the module.
func NewGenesisState(nextProposalID uint64, committees []Committee, proposals []Proposal, votes []Vote) GenesisState {
return GenesisState{
NextProposalID: nextProposalID,
Committees: committees,
Proposals: proposals,
Votes: votes,
}
}
// DefaultGenesisState returns the default genesis state for the module.
func DefaultGenesisState() GenesisState {
return NewGenesisState(
DefaultNextProposalID,
[]Committee{},
[]Proposal{},
[]Vote{},
)
}
// Equal checks whether two gov GenesisState structs are equivalent
func (data GenesisState) Equal(data2 GenesisState) bool {
b1 := ModuleCdc.MustMarshalBinaryBare(data)
b2 := ModuleCdc.MustMarshalBinaryBare(data2)
return bytes.Equal(b1, b2)
}
// IsEmpty returns true if a GenesisState is empty
func (data GenesisState) IsEmpty() bool {
return data.Equal(GenesisState{})
}
// Validate performs basic validation of genesis data.
func (gs GenesisState) Validate() error {
// validate committees
committeeMap := make(map[uint64]bool, len(gs.Committees))
for _, com := range gs.Committees {
// check there are no duplicate IDs
if _, ok := committeeMap[com.ID]; ok {
return fmt.Errorf("duplicate committee ID found in genesis state; id: %d", com.ID)
}
committeeMap[com.ID] = true
// validate committee
if err := com.Validate(); err != nil {
return err
}
}
// validate proposals - pp.Val, no duplicate IDs, no ids >= nextID, committee needs to exist
proposalMap := make(map[uint64]bool, len(gs.Proposals))
for _, p := range gs.Proposals {
// check there are no duplicate IDs
if _, ok := proposalMap[p.ID]; ok {
return fmt.Errorf("duplicate proposal ID found in genesis state; id: %d", p.ID)
}
proposalMap[p.ID] = true
// validate next proposal ID
if p.ID >= gs.NextProposalID {
return fmt.Errorf("NextProposalID is not greater than all proposal IDs; id: %d", p.ID)
}
// check committee exists
if !committeeMap[p.CommitteeID] {
return fmt.Errorf("proposal refers to non existant committee; proposal: %+v", p)
}
// validate pubProposal
if err := p.PubProposal.ValidateBasic(); err != nil {
return fmt.Errorf("proposal %d invalid: %w", p.ID, err)
}
}
// validate votes
for _, v := range gs.Votes {
// check proposal exists
if !proposalMap[v.ProposalID] {
return fmt.Errorf("vote refers to non existant proposal; vote: %+v", v)
}
// validate address
if v.Voter.Empty() {
return fmt.Errorf("found empty voter address; vote: %+v", v)
}
}
return nil
}

View File

@ -0,0 +1,169 @@
package types
import (
"testing"
"time"
"github.com/stretchr/testify/require"
sdk "github.com/cosmos/cosmos-sdk/types"
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
"github.com/tendermint/tendermint/crypto"
)
func d(s string) sdk.Dec { return sdk.MustNewDecFromStr(s) }
func TestGenesisState_Validate(t *testing.T) {
testTime := time.Date(1998, time.January, 1, 0, 0, 0, 0, time.UTC)
addresses := []sdk.AccAddress{
sdk.AccAddress(crypto.AddressHash([]byte("KavaTest1"))),
sdk.AccAddress(crypto.AddressHash([]byte("KavaTest2"))),
sdk.AccAddress(crypto.AddressHash([]byte("KavaTest3"))),
sdk.AccAddress(crypto.AddressHash([]byte("KavaTest4"))),
sdk.AccAddress(crypto.AddressHash([]byte("KavaTest5"))),
}
testGenesis := GenesisState{
NextProposalID: 2,
Committees: []Committee{
{
ID: 1,
Description: "This committee is for testing.",
Members: addresses[:3],
Permissions: []Permission{GodPermission{}},
VoteThreshold: d("0.667"),
ProposalDuration: time.Hour * 24 * 7,
},
{
ID: 2,
Description: "This committee is also for testing.",
Members: addresses[2:],
Permissions: nil,
VoteThreshold: d("0.8"),
ProposalDuration: time.Hour * 24 * 21,
},
},
Proposals: []Proposal{
{ID: 1, CommitteeID: 1, PubProposal: govtypes.NewTextProposal("A Title", "A description of this proposal."), Deadline: testTime.Add(7 * 24 * time.Hour)},
},
Votes: []Vote{
{ProposalID: 1, Voter: addresses[0]},
{ProposalID: 1, Voter: addresses[1]},
},
}
testCases := []struct {
name string
genState GenesisState
expectPass bool
}{
{
name: "default",
genState: DefaultGenesisState(),
expectPass: true,
},
{
name: "normal",
genState: testGenesis,
expectPass: true,
},
{
name: "duplicate committee IDs",
genState: GenesisState{
NextProposalID: testGenesis.NextProposalID,
Committees: append(testGenesis.Committees, testGenesis.Committees[0]),
Proposals: testGenesis.Proposals,
Votes: testGenesis.Votes,
},
expectPass: false,
},
{
name: "invalid committee",
genState: GenesisState{
NextProposalID: testGenesis.NextProposalID,
Committees: append(testGenesis.Committees, Committee{}),
Proposals: testGenesis.Proposals,
Votes: testGenesis.Votes,
},
expectPass: false,
},
{
name: "duplicate proposal IDs",
genState: GenesisState{
NextProposalID: testGenesis.NextProposalID,
Committees: testGenesis.Committees,
Proposals: append(testGenesis.Proposals, testGenesis.Proposals[0]),
Votes: testGenesis.Votes,
},
expectPass: false,
},
{
name: "invalid NextProposalID",
genState: GenesisState{
NextProposalID: 0,
Committees: testGenesis.Committees,
Proposals: testGenesis.Proposals,
Votes: testGenesis.Votes,
},
expectPass: false,
},
{
name: "proposal without committee",
genState: GenesisState{
NextProposalID: testGenesis.NextProposalID + 1,
Committees: testGenesis.Committees,
Proposals: append(
testGenesis.Proposals,
Proposal{
ID: testGenesis.NextProposalID,
PubProposal: govtypes.NewTextProposal("A Title", "A description of this proposal."),
CommitteeID: 247, // doesn't exist
}),
Votes: testGenesis.Votes,
},
expectPass: false,
},
{
name: "invalid proposal",
genState: GenesisState{
NextProposalID: testGenesis.NextProposalID,
Committees: testGenesis.Committees,
Proposals: append(testGenesis.Proposals, Proposal{}),
Votes: testGenesis.Votes,
},
expectPass: false,
},
{
name: "vote without proposal",
genState: GenesisState{
NextProposalID: testGenesis.NextProposalID,
Committees: testGenesis.Committees,
Proposals: nil,
Votes: testGenesis.Votes,
},
expectPass: false,
},
{
name: "invalid vote",
genState: GenesisState{
NextProposalID: testGenesis.NextProposalID,
Committees: testGenesis.Committees,
Proposals: testGenesis.Proposals,
Votes: append(testGenesis.Votes, Vote{}),
},
expectPass: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := tc.genState.Validate()
if tc.expectPass {
require.NoError(t, err)
} else {
require.Error(t, err)
}
})
}
}

54
x/committee/types/keys.go Normal file
View File

@ -0,0 +1,54 @@
package types
import (
"encoding/binary"
sdk "github.com/cosmos/cosmos-sdk/types"
)
const (
// ModuleName The name that will be used throughout the module
ModuleName = "committee"
// StoreKey Top level store key where all module items will be stored
StoreKey = ModuleName
// RouterKey Top level router key
RouterKey = ModuleName
// QuerierRoute Top level query string
QuerierRoute = ModuleName
// DefaultParamspace default name for parameter store
DefaultParamspace = ModuleName
)
// Key prefixes
var (
CommitteeKeyPrefix = []byte{0x00} // prefix for keys that store committees
ProposalKeyPrefix = []byte{0x01} // prefix for keys that store proposals
VoteKeyPrefix = []byte{0x02} // prefix for keys that store votes
NextProposalIDKey = []byte{0x03} // key for the next proposal id
)
// GetKeyFromID returns the bytes to use as a key for a uint64 id
func GetKeyFromID(id uint64) []byte {
return uint64ToBytes(id)
}
func GetVoteKey(proposalID uint64, voter sdk.AccAddress) []byte {
return append(GetKeyFromID(proposalID), voter.Bytes()...)
}
// Uint64ToBytes converts a uint64 into fixed length bytes for use in store keys.
func uint64ToBytes(id uint64) []byte {
bz := make([]byte, 8)
binary.BigEndian.PutUint64(bz, uint64(id))
return bz
}
// Uint64FromBytes converts some fixed length bytes back into a uint64.
func Uint64FromBytes(bz []byte) uint64 {
return binary.BigEndian.Uint64(bz)
}

94
x/committee/types/msg.go Normal file
View File

@ -0,0 +1,94 @@
package types
import (
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)
const (
TypeMsgSubmitProposal = "commmittee_submit_proposal" // 'committee' prefix appended to avoid potential conflicts with gov msg types
TypeMsgVote = "committee_vote"
)
var _, _ sdk.Msg = MsgSubmitProposal{}, MsgVote{}
// MsgSubmitProposal is used by committee members to create a new proposal that they can vote on.
type MsgSubmitProposal struct {
PubProposal PubProposal `json:"pub_proposal" yaml:"pub_proposal"`
Proposer sdk.AccAddress `json:"proposer" yaml:"proposer"`
CommitteeID uint64 `json:"committee_id" yaml:"committee_id"`
}
// NewMsgSubmitProposal creates a new MsgSubmitProposal instance
func NewMsgSubmitProposal(pubProposal PubProposal, proposer sdk.AccAddress, committeeID uint64) MsgSubmitProposal {
return MsgSubmitProposal{
PubProposal: pubProposal,
Proposer: proposer,
CommitteeID: committeeID,
}
}
// Route return the message type used for routing the message.
func (msg MsgSubmitProposal) Route() string { return RouterKey }
// Type returns a human-readable string for the message, intended for utilization within events.
func (msg MsgSubmitProposal) Type() string { return TypeMsgSubmitProposal }
// ValidateBasic does a simple validation check that doesn't require access to any other information.
func (msg MsgSubmitProposal) ValidateBasic() error {
if msg.PubProposal == nil {
return sdkerrors.Wrap(ErrInvalidPubProposal, "pub proposal cannot be nil")
}
if msg.Proposer.Empty() {
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "proposer address cannot be empty")
}
return msg.PubProposal.ValidateBasic()
}
// GetSignBytes gets the canonical byte representation of the Msg.
func (msg MsgSubmitProposal) GetSignBytes() []byte {
bz := ModuleCdc.MustMarshalJSON(msg)
return sdk.MustSortJSON(bz)
}
// GetSigners returns the addresses of signers that must sign.
func (msg MsgSubmitProposal) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{msg.Proposer}
}
// MsgVote is submitted by committee members to vote on proposals.
type MsgVote struct {
ProposalID uint64 `json:"proposal_id" yaml:"proposal_id"`
Voter sdk.AccAddress `json:"voter" yaml:"voter"`
}
// NewMsgVote creates a message to cast a vote on an active proposal
func NewMsgVote(voter sdk.AccAddress, proposalID uint64) MsgVote {
return MsgVote{proposalID, voter}
}
// Route return the message type used for routing the message.
func (msg MsgVote) Route() string { return RouterKey }
// Type returns a human-readable string for the message, intended for utilization within events.
func (msg MsgVote) Type() string { return TypeMsgVote }
// ValidateBasic does a simple validation check that doesn't require access to any other information.
func (msg MsgVote) ValidateBasic() error {
if msg.Voter.Empty() {
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "voter address cannot be empty")
}
return nil
}
// GetSignBytes gets the canonical byte representation of the Msg.
func (msg MsgVote) GetSignBytes() []byte {
bz := ModuleCdc.MustMarshalJSON(msg)
return sdk.MustSortJSON(bz)
}
// GetSigners returns the addresses of signers that must sign.
func (msg MsgVote) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{msg.Voter}
}

View File

@ -0,0 +1,81 @@
package types
import (
"testing"
"github.com/stretchr/testify/require"
sdk "github.com/cosmos/cosmos-sdk/types"
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
)
func TestMsgSubmitProposal_ValidateBasic(t *testing.T) {
addr := sdk.AccAddress([]byte("someName"))
tests := []struct {
name string
msg MsgSubmitProposal
expectPass bool
}{
{
name: "normal",
msg: MsgSubmitProposal{govtypes.NewTextProposal("A Title", "A proposal description."), addr, 3},
expectPass: true,
},
{
name: "empty address",
msg: MsgSubmitProposal{govtypes.NewTextProposal("A Title", "A proposal description."), nil, 3},
expectPass: false,
},
{
name: "invalid proposal",
msg: MsgSubmitProposal{govtypes.TextProposal{}, addr, 3},
expectPass: false,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
err := tc.msg.ValidateBasic()
if tc.expectPass {
require.NoError(t, err)
} else {
require.Error(t, err)
}
})
}
}
func TestMsgVote_ValidateBasic(t *testing.T) {
addr := sdk.AccAddress([]byte("someName"))
tests := []struct {
name string
msg MsgVote
expectPass bool
}{
{
name: "normal",
msg: MsgVote{5, addr},
expectPass: true,
},
{
name: "empty address",
msg: MsgVote{5, nil},
expectPass: false,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
err := tc.msg.ValidateBasic()
if tc.expectPass {
require.NoError(t, err)
} else {
require.Error(t, err)
}
})
}
}

View File

@ -0,0 +1,113 @@
package types
import (
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
paramstypes "github.com/cosmos/cosmos-sdk/x/params/types"
)
func init() {
// CommitteeChange/Delete proposals are registered on gov's ModuleCdc (see proposal.go).
// But since these proposals contain Permissions, these types also need registering:
govtypes.ModuleCdc.RegisterInterface((*Permission)(nil), nil)
govtypes.RegisterProposalTypeCodec(GodPermission{}, "kava/GodPermission")
govtypes.RegisterProposalTypeCodec(ParamChangePermission{}, "kava/ParamChangePermission")
govtypes.RegisterProposalTypeCodec(TextPermission{}, "kava/TextPermission")
}
// Permission is anything with a method that validates whether a proposal is allowed by it or not.
type Permission interface {
Allows(PubProposal) bool
}
// ------------------------------------------
// GodPermission
// ------------------------------------------
// GodPermission allows any governance proposal. It is used mainly for testing.
type GodPermission struct{}
var _ Permission = GodPermission{}
func (GodPermission) Allows(PubProposal) bool { return true }
func (GodPermission) MarshalYAML() (interface{}, error) {
valueToMarshal := struct {
Type string `yaml:"type"`
}{
Type: "god_permission",
}
return valueToMarshal, nil
}
// ------------------------------------------
// ParamChangePermission
// ------------------------------------------
// ParamChangeProposal only allows changes to certain params
type ParamChangePermission struct {
AllowedParams AllowedParams `json:"allowed_params" yaml:"allowed_params"`
}
var _ Permission = ParamChangePermission{}
func (perm ParamChangePermission) Allows(p PubProposal) bool {
proposal, ok := p.(paramstypes.ParameterChangeProposal)
if !ok {
return false
}
for _, change := range proposal.Changes {
if !perm.AllowedParams.Contains(change) {
return false
}
}
return true
}
func (perm ParamChangePermission) MarshalYAML() (interface{}, error) {
valueToMarshal := struct {
Type string `yaml:"type"`
AllowedParams AllowedParams `yaml:"allowed_params"`
}{
Type: "param_change_permission",
AllowedParams: perm.AllowedParams,
}
return valueToMarshal, nil
}
type AllowedParam struct {
Subspace string `json:"subspace" yaml:"subspace"`
Key string `json:"key" yaml:"key"`
}
type AllowedParams []AllowedParam
func (allowed AllowedParams) Contains(paramChange paramstypes.ParamChange) bool {
for _, p := range allowed {
if paramChange.Subspace == p.Subspace && paramChange.Key == p.Key {
return true
}
}
return false
}
// ------------------------------------------
// TextPermission
// ------------------------------------------
// TextPermission allows any text governance proposal.
type TextPermission struct{}
var _ Permission = TextPermission{}
func (TextPermission) Allows(p PubProposal) bool {
_, ok := p.(govtypes.TextProposal)
return ok
}
func (TextPermission) MarshalYAML() (interface{}, error) {
valueToMarshal := struct {
Type string `yaml:"type"`
}{
Type: "text_permission",
}
return valueToMarshal, nil
}

View File

@ -0,0 +1,286 @@
package types
import (
"testing"
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
paramstypes "github.com/cosmos/cosmos-sdk/x/params/types"
"github.com/stretchr/testify/suite"
)
type PermissionsTestSuite struct {
suite.Suite
exampleAllowedParams AllowedParams
}
func (suite *PermissionsTestSuite) SetupTest() {
suite.exampleAllowedParams = AllowedParams{
{
Subspace: "cdp",
Key: "DebtThreshold",
},
{
Subspace: "cdp",
Key: "SurplusThreshold",
},
{
Subspace: "cdp",
Key: "CollateralParams",
},
{
Subspace: "auction",
Key: "BidDuration",
},
}
}
func (suite *PermissionsTestSuite) TestParamChangePermission_Allows() {
testcases := []struct {
name string
allowedParams AllowedParams
pubProposal PubProposal
expectAllowed bool
}{
{
name: "normal (single param)",
allowedParams: suite.exampleAllowedParams,
pubProposal: paramstypes.NewParameterChangeProposal(
"A Title",
"A description for this proposal.",
[]paramstypes.ParamChange{
{
Subspace: "cdp",
Key: "DebtThreshold",
Value: `{"denom": "usdx", "amount": "1000000"}`,
},
},
),
expectAllowed: true,
},
{
name: "normal (multiple params)",
allowedParams: suite.exampleAllowedParams,
pubProposal: paramstypes.NewParameterChangeProposal(
"A Title",
"A description for this proposal.",
[]paramstypes.ParamChange{
{
Subspace: "cdp",
Key: "DebtThreshold",
Value: `{"denom": "usdx", "amount": "1000000"}`,
},
{
Subspace: "cdp",
Key: "CollateralParams",
Value: `[]`,
},
},
),
expectAllowed: true,
},
{
name: "not allowed (not in list)",
allowedParams: suite.exampleAllowedParams,
pubProposal: paramstypes.NewParameterChangeProposal(
"A Title",
"A description for this proposal.",
[]paramstypes.ParamChange{
{
Subspace: "cdp",
Key: "GlobalDebtLimit",
Value: `{"denom": "usdx", "amount": "1000000000"}`,
},
},
),
expectAllowed: false,
},
{
name: "not allowed (nil allowed params)",
allowedParams: nil,
pubProposal: paramstypes.NewParameterChangeProposal(
"A Title",
"A description for this proposal.",
[]paramstypes.ParamChange{
{
Subspace: "cdp",
Key: "DebtThreshold",
Value: `[{"denom": "usdx", "amount": "1000000"}]`,
},
},
),
expectAllowed: false,
},
{
name: "not allowed (mismatched pubproposal type)",
allowedParams: suite.exampleAllowedParams,
pubProposal: govtypes.NewTextProposal("A Title", "A description of this proposal."),
expectAllowed: false,
},
{
name: "not allowed (nil pubproposal)",
allowedParams: suite.exampleAllowedParams,
pubProposal: nil,
expectAllowed: false,
},
}
for _, tc := range testcases {
suite.Run(tc.name, func() {
permission := ParamChangePermission{
AllowedParams: tc.allowedParams,
}
suite.Equal(
tc.expectAllowed,
permission.Allows(tc.pubProposal),
)
})
}
}
func (suite *PermissionsTestSuite) TestAllowedParams_Contains() {
testcases := []struct {
name string
allowedParams AllowedParams
testParam paramstypes.ParamChange
expectContained bool
}{
{
name: "normal",
allowedParams: suite.exampleAllowedParams,
testParam: paramstypes.ParamChange{
Subspace: "cdp",
Key: "DebtThreshold",
Value: `{"denom": "usdx", "amount": "1000000"}`,
},
expectContained: true,
},
{
name: "missing subspace",
allowedParams: suite.exampleAllowedParams,
testParam: paramstypes.ParamChange{
Subspace: "",
Key: "DebtThreshold",
Value: `{"denom": "usdx", "amount": "1000000"}`,
},
expectContained: false,
},
{
name: "missing key",
allowedParams: suite.exampleAllowedParams,
testParam: paramstypes.ParamChange{
Subspace: "cdp",
Key: "",
Value: `{"denom": "usdx", "amount": "1000000"}`,
},
expectContained: false,
},
{
name: "empty list",
allowedParams: AllowedParams{},
testParam: paramstypes.ParamChange{
Subspace: "cdp",
Key: "DebtThreshold",
Value: `{"denom": "usdx", "amount": "1000000"}`,
},
expectContained: false,
},
{
name: "nil list",
allowedParams: nil,
testParam: paramstypes.ParamChange{
Subspace: "cdp",
Key: "DebtThreshold",
Value: `{"denom": "usdx", "amount": "1000000"}`,
},
expectContained: false,
},
{
name: "no param change",
allowedParams: suite.exampleAllowedParams,
testParam: paramstypes.ParamChange{},
expectContained: false,
},
{
name: "empty list and no param change",
allowedParams: AllowedParams{},
testParam: paramstypes.ParamChange{},
expectContained: false,
},
}
for _, tc := range testcases {
suite.Run(tc.name, func() {
suite.Require().Equal(
tc.expectContained,
tc.allowedParams.Contains(tc.testParam),
)
})
}
}
func (suite *PermissionsTestSuite) TestTextPermission_Allows() {
testcases := []struct {
name string
pubProposal PubProposal
expectAllowed bool
}{
{
name: "normal",
pubProposal: govtypes.NewTextProposal(
"A Title",
"A description for this proposal.",
),
expectAllowed: true,
},
{
name: "not allowed (wrong pubproposal type)",
pubProposal: paramstypes.NewParameterChangeProposal(
"A Title",
"A description for this proposal.",
[]paramstypes.ParamChange{
{
Subspace: "cdp",
Key: "DebtThreshold",
Value: `{"denom": "usdx", "amount": "1000000"}`,
},
{
Subspace: "cdp",
Key: "CollateralParams",
Value: `[]`,
},
},
),
expectAllowed: false,
},
{
name: "not allowed (nil pubproposal)",
pubProposal: nil,
expectAllowed: false,
},
}
for _, tc := range testcases {
suite.Run(tc.name, func() {
permission := TextPermission{}
suite.Equal(
tc.expectAllowed,
permission.Allows(tc.pubProposal),
)
})
}
}
func TestPermissionsTestSuite(t *testing.T) {
suite.Run(t, new(PermissionsTestSuite))
}

View File

@ -0,0 +1,108 @@
package types
import (
"gopkg.in/yaml.v2"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
)
const (
ProposalTypeCommitteeChange = "CommitteeChange"
ProposalTypeCommitteeDelete = "CommitteeDelete"
)
// ensure proposal types fulfill the PubProposal interface and the gov Content interface.
var _, _ govtypes.Content = CommitteeChangeProposal{}, CommitteeDeleteProposal{}
var _, _ PubProposal = CommitteeChangeProposal{}, CommitteeDeleteProposal{}
func init() {
// Gov proposals need to be registered on gov's ModuleCdc so MsgSubmitProposal can be encoded.
govtypes.RegisterProposalType(ProposalTypeCommitteeChange)
govtypes.RegisterProposalTypeCodec(CommitteeChangeProposal{}, "kava/CommitteeChangeProposal")
govtypes.RegisterProposalType(ProposalTypeCommitteeDelete)
govtypes.RegisterProposalTypeCodec(CommitteeDeleteProposal{}, "kava/CommitteeDeleteProposal")
}
// CommitteeChangeProposal is a gov proposal for creating a new committee or modifying an existing one.
type CommitteeChangeProposal struct {
Title string `json:"title" yaml:"title"`
Description string `json:"description" yaml:"description"`
NewCommittee Committee `json:"new_committee" yaml:"new_committee"`
}
func NewCommitteeChangeProposal(title string, description string, newCommittee Committee) CommitteeChangeProposal {
return CommitteeChangeProposal{
Title: title,
Description: description,
NewCommittee: newCommittee,
}
}
// GetTitle returns the title of the proposal.
func (ccp CommitteeChangeProposal) GetTitle() string { return ccp.Title }
// GetDescription returns the description of the proposal.
func (ccp CommitteeChangeProposal) GetDescription() string { return ccp.Description }
// ProposalRoute returns the routing key of the proposal.
func (ccp CommitteeChangeProposal) ProposalRoute() string { return RouterKey }
// ProposalType returns the type of the proposal.
func (ccp CommitteeChangeProposal) ProposalType() string { return ProposalTypeCommitteeChange }
// ValidateBasic runs basic stateless validity checks
func (ccp CommitteeChangeProposal) ValidateBasic() error {
if err := govtypes.ValidateAbstract(ccp); err != nil {
return err
}
if err := ccp.NewCommittee.Validate(); err != nil {
return sdkerrors.Wrap(ErrInvalidCommittee, err.Error())
}
return nil
}
// String implements the Stringer interface.
func (ccp CommitteeChangeProposal) String() string {
bz, _ := yaml.Marshal(ccp)
return string(bz)
}
// CommitteeDeleteProposal is a gov proposal for removing a committee.
type CommitteeDeleteProposal struct {
Title string `json:"title" yaml:"title"`
Description string `json:"description" yaml:"description"`
CommitteeID uint64 `json:"committee_id" yaml:"committee_id"`
}
func NewCommitteeDeleteProposal(title string, description string, committeeID uint64) CommitteeDeleteProposal {
return CommitteeDeleteProposal{
Title: title,
Description: description,
CommitteeID: committeeID,
}
}
// GetTitle returns the title of the proposal.
func (cdp CommitteeDeleteProposal) GetTitle() string { return cdp.Title }
// GetDescription returns the description of the proposal.
func (cdp CommitteeDeleteProposal) GetDescription() string { return cdp.Description }
// ProposalRoute returns the routing key of the proposal.
func (cdp CommitteeDeleteProposal) ProposalRoute() string { return RouterKey }
// ProposalType returns the type of the proposal.
func (cdp CommitteeDeleteProposal) ProposalType() string { return ProposalTypeCommitteeDelete }
// ValidateBasic runs basic stateless validity checks
func (cdp CommitteeDeleteProposal) ValidateBasic() error {
return govtypes.ValidateAbstract(cdp)
}
// String implements the Stringer interface.
func (cdp CommitteeDeleteProposal) String() string {
bz, _ := yaml.Marshal(cdp)
return string(bz)
}

View File

@ -0,0 +1,48 @@
package types
import (
sdk "github.com/cosmos/cosmos-sdk/types"
)
// Query endpoints supported by the Querier
const (
QueryCommittees = "committees"
QueryCommittee = "committee"
QueryProposals = "proposals"
QueryProposal = "proposal"
QueryVotes = "votes"
QueryVote = "vote"
QueryTally = "tally"
)
type QueryCommitteeParams struct {
CommitteeID uint64 `json:"committee_id" yaml:"committee_id"`
}
func NewQueryCommitteeParams(committeeID uint64) QueryCommitteeParams {
return QueryCommitteeParams{
CommitteeID: committeeID,
}
}
type QueryProposalParams struct {
ProposalID uint64 `json:"proposal_id" yaml:"proposal_id"`
}
func NewQueryProposalParams(proposalID uint64) QueryProposalParams {
return QueryProposalParams{
ProposalID: proposalID,
}
}
type QueryVoteParams struct {
ProposalID uint64 `json:"proposal_id" yaml:"proposal_id"`
Voter sdk.AccAddress `json:"voter" yaml:"voter"`
}
func NewQueryVoteParams(proposalID uint64, voter sdk.AccAddress) QueryVoteParams {
return QueryVoteParams{
ProposalID: proposalID,
Voter: voter,
}
}