diff --git a/app/app.go b/app/app.go index 25ba15a3c..c840e9fa8 100644 --- a/app/app.go +++ b/app/app.go @@ -16,7 +16,7 @@ import ( "github.com/cosmos/cosmos-sdk/wire" "github.com/cosmos/cosmos-sdk/x/auth" "github.com/cosmos/cosmos-sdk/x/bank" - "github.com/cosmos/cosmos-sdk/x/gov" + "github.com/irisnet/irishub/modules/gov" "github.com/cosmos/cosmos-sdk/x/ibc" "github.com/cosmos/cosmos-sdk/x/slashing" "github.com/cosmos/cosmos-sdk/x/stake" diff --git a/cmd/iriscli/main.go b/cmd/iriscli/main.go index abac65687..74fe4302d 100644 --- a/cmd/iriscli/main.go +++ b/cmd/iriscli/main.go @@ -11,7 +11,7 @@ import ( "github.com/cosmos/cosmos-sdk/client/tx" authcmd "github.com/cosmos/cosmos-sdk/x/auth/client/cli" bankcmd "github.com/cosmos/cosmos-sdk/x/bank/client/cli" - govcmd "github.com/cosmos/cosmos-sdk/x/gov/client/cli" + govcmd "github.com/irisnet/irishub/modules/gov/client/cli" ibccmd "github.com/cosmos/cosmos-sdk/x/ibc/client/cli" slashingcmd "github.com/cosmos/cosmos-sdk/x/slashing/client/cli" stakecmd "github.com/cosmos/cosmos-sdk/x/stake/client/cli" diff --git a/modules/gov/client/cli/tx.go b/modules/gov/client/cli/tx.go new file mode 100644 index 000000000..d732e97e9 --- /dev/null +++ b/modules/gov/client/cli/tx.go @@ -0,0 +1,247 @@ +package cli + +import ( + "fmt" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + + "github.com/cosmos/cosmos-sdk/client/context" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/wire" + authcmd "github.com/cosmos/cosmos-sdk/x/auth/client/cli" + "github.com/irisnet/irishub/modules/gov" + "github.com/pkg/errors" +) + +const ( + flagProposalID = "proposalID" + flagTitle = "title" + flagDescription = "description" + flagProposalType = "type" + flagDeposit = "deposit" + flagProposer = "proposer" + flagDepositer = "depositer" + flagVoter = "voter" + flagOption = "option" +) + +// submit a proposal tx +func GetCmdSubmitProposal(cdc *wire.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "submit-proposal", + Short: "Submit a proposal along with an initial deposit", + RunE: func(cmd *cobra.Command, args []string) error { + title := viper.GetString(flagTitle) + description := viper.GetString(flagDescription) + strProposalType := viper.GetString(flagProposalType) + initialDeposit := viper.GetString(flagDeposit) + + // get the from address from the name flag + from, err := sdk.AccAddressFromBech32(viper.GetString(flagProposer)) + if err != nil { + return err + } + + amount, err := sdk.ParseCoins(initialDeposit) + if err != nil { + return err + } + + proposalType, err := gov.ProposalTypeFromString(strProposalType) + if err != nil { + return err + } + + // create the message + msg := gov.NewMsgSubmitProposal(title, description, proposalType, from, amount) + + err = msg.ValidateBasic() + if err != nil { + return err + } + + // build and sign the transaction, then broadcast to Tendermint + ctx := context.NewCoreContextFromViper().WithDecoder(authcmd.GetAccountDecoder(cdc)) + // proposalID must be returned, and it is a part of response + ctx.PrintResponse = true + + err = ctx.EnsureSignBuildBroadcast(ctx.FromAddressName, []sdk.Msg{msg}, cdc) + if err != nil { + return err + } + return nil + }, + } + + cmd.Flags().String(flagTitle, "", "title of proposal") + cmd.Flags().String(flagDescription, "", "description of proposal") + cmd.Flags().String(flagProposalType, "", "proposalType of proposal") + cmd.Flags().String(flagDeposit, "", "deposit of proposal") + cmd.Flags().String(flagProposer, "", "proposer of proposal") + + return cmd +} + +// set a new Deposit transaction +func GetCmdDeposit(cdc *wire.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "deposit", + Short: "deposit tokens for activing proposal", + RunE: func(cmd *cobra.Command, args []string) error { + // get the from address from the name flag + depositer, err := sdk.AccAddressFromBech32(viper.GetString(flagDepositer)) + if err != nil { + return err + } + + proposalID := viper.GetInt64(flagProposalID) + + amount, err := sdk.ParseCoins(viper.GetString(flagDeposit)) + if err != nil { + return err + } + + // create the message + msg := gov.NewMsgDeposit(depositer, proposalID, amount) + + err = msg.ValidateBasic() + if err != nil { + return err + } + + // build and sign the transaction, then broadcast to Tendermint + ctx := context.NewCoreContextFromViper().WithDecoder(authcmd.GetAccountDecoder(cdc)) + + err = ctx.EnsureSignBuildBroadcast(ctx.FromAddressName, []sdk.Msg{msg}, cdc) + if err != nil { + return err + } + return nil + }, + } + + cmd.Flags().String(flagProposalID, "", "proposalID of proposal depositing on") + cmd.Flags().String(flagDepositer, "", "depositer of deposit") + cmd.Flags().String(flagDeposit, "", "amount of deposit") + + return cmd +} + +// set a new Vote transaction +func GetCmdVote(cdc *wire.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "vote", + Short: "vote for an active proposal, options: Yes/No/NoWithVeto/Abstain", + RunE: func(cmd *cobra.Command, args []string) error { + + bechVoter := viper.GetString(flagVoter) + voter, err := sdk.AccAddressFromBech32(bechVoter) + if err != nil { + return err + } + + proposalID := viper.GetInt64(flagProposalID) + + option := viper.GetString(flagOption) + + byteVoteOption, err := gov.VoteOptionFromString(option) + if err != nil { + return err + } + + // create the message + msg := gov.NewMsgVote(voter, proposalID, byteVoteOption) + + err = msg.ValidateBasic() + if err != nil { + return err + } + + fmt.Printf("Vote[Voter:%s,ProposalID:%d,Option:%s]", bechVoter, msg.ProposalID, msg.Option) + + // build and sign the transaction, then broadcast to Tendermint + ctx := context.NewCoreContextFromViper().WithDecoder(authcmd.GetAccountDecoder(cdc)) + + err = ctx.EnsureSignBuildBroadcast(ctx.FromAddressName, []sdk.Msg{msg}, cdc) + if err != nil { + return err + } + return nil + }, + } + + cmd.Flags().String(flagProposalID, "", "proposalID of proposal voting on") + cmd.Flags().String(flagVoter, "", "bech32 voter address") + cmd.Flags().String(flagOption, "", "vote option {Yes, No, NoWithVeto, Abstain}") + + return cmd +} + +// Command to Get a Proposal Information +func GetCmdQueryProposal(storeName string, cdc *wire.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "query-proposal", + Short: "query proposal details", + RunE: func(cmd *cobra.Command, args []string) error { + proposalID := viper.GetInt64(flagProposalID) + + ctx := context.NewCoreContextFromViper() + + res, err := ctx.QueryStore(gov.KeyProposal(proposalID), storeName) + if len(res) == 0 || err != nil { + return errors.Errorf("proposalID [%d] is not existed", proposalID) + } + + var proposal gov.Proposal + cdc.MustUnmarshalBinary(res, &proposal) + output, err := wire.MarshalJSONIndent(cdc, proposal) + if err != nil { + return err + } + fmt.Println(string(output)) + return nil + }, + } + + cmd.Flags().String(flagProposalID, "", "proposalID of proposal being queried") + + return cmd +} + +// Command to Get a Proposal Information +func GetCmdQueryVote(storeName string, cdc *wire.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "query-vote", + Short: "query vote", + RunE: func(cmd *cobra.Command, args []string) error { + proposalID := viper.GetInt64(flagProposalID) + + voterAddr, err := sdk.AccAddressFromBech32(viper.GetString(flagVoter)) + if err != nil { + return err + } + + ctx := context.NewCoreContextFromViper() + + res, err := ctx.QueryStore(gov.KeyVote(proposalID, voterAddr), storeName) + if len(res) == 0 || err != nil { + return errors.Errorf("proposalID [%d] does not exist", proposalID) + } + + var vote gov.Vote + cdc.MustUnmarshalBinary(res, &vote) + output, err := wire.MarshalJSONIndent(cdc, vote) + if err != nil { + return err + } + fmt.Println(string(output)) + return nil + }, + } + + cmd.Flags().String(flagProposalID, "", "proposalID of proposal voting on") + cmd.Flags().String(flagVoter, "", "bech32 voter address") + + return cmd +} diff --git a/modules/gov/depositsvotes.go b/modules/gov/depositsvotes.go new file mode 100644 index 000000000..19ed97f69 --- /dev/null +++ b/modules/gov/depositsvotes.go @@ -0,0 +1,120 @@ +package gov + +import ( + "encoding/json" + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/pkg/errors" +) + +// Vote +type Vote struct { + Voter sdk.AccAddress `json:"voter"` // address of the voter + ProposalID int64 `json:"proposal_id"` // proposalID of the proposal + Option VoteOption `json:"option"` // option from OptionSet chosen by the voter +} + +// Deposit +type Deposit struct { + Depositer sdk.AccAddress `json:"depositer"` // Address of the depositer + ProposalID int64 `json:"proposal_id"` // proposalID of the proposal + Amount sdk.Coins `json:"amount"` // Deposit amount +} + +// Type that represents VoteOption as a byte +type VoteOption byte + +//nolint +const ( + OptionEmpty VoteOption = 0x00 + OptionYes VoteOption = 0x01 + OptionAbstain VoteOption = 0x02 + OptionNo VoteOption = 0x03 + OptionNoWithVeto VoteOption = 0x04 +) + +// String to proposalType byte. Returns ff if invalid. +func VoteOptionFromString(str string) (VoteOption, error) { + switch str { + case "Yes": + return OptionYes, nil + case "Abstain": + return OptionAbstain, nil + case "No": + return OptionNo, nil + case "NoWithVeto": + return OptionNoWithVeto, nil + default: + return VoteOption(0xff), errors.Errorf("'%s' is not a valid vote option", str) + } +} + +// Is defined VoteOption +func validVoteOption(option VoteOption) bool { + if option == OptionYes || + option == OptionAbstain || + option == OptionNo || + option == OptionNoWithVeto { + return true + } + return false +} + +// Marshal needed for protobuf compatibility +func (vo VoteOption) Marshal() ([]byte, error) { + return []byte{byte(vo)}, nil +} + +// Unmarshal needed for protobuf compatibility +func (vo *VoteOption) Unmarshal(data []byte) error { + *vo = VoteOption(data[0]) + return nil +} + +// Marshals to JSON using string +func (vo VoteOption) MarshalJSON() ([]byte, error) { + return json.Marshal(vo.String()) +} + +// Unmarshals from JSON assuming Bech32 encoding +func (vo *VoteOption) UnmarshalJSON(data []byte) error { + var s string + err := json.Unmarshal(data, &s) + if err != nil { + return nil + } + + bz2, err := VoteOptionFromString(s) + if err != nil { + return err + } + *vo = bz2 + return nil +} + +// Turns VoteOption byte to String +func (vo VoteOption) String() string { + switch vo { + case OptionYes: + return "Yes" + case OptionAbstain: + return "Abstain" + case OptionNo: + return "No" + case OptionNoWithVeto: + return "NoWithVeto" + default: + return "" + } +} + +// For Printf / Sprintf, returns bech32 when using %s +func (vo VoteOption) Format(s fmt.State, verb rune) { + switch verb { + case 's': + s.Write([]byte(fmt.Sprintf("%s", vo.String()))) + default: + s.Write([]byte(fmt.Sprintf("%v", byte(vo)))) + } +} diff --git a/modules/gov/errors.go b/modules/gov/errors.go new file mode 100644 index 000000000..0825e00f8 --- /dev/null +++ b/modules/gov/errors.go @@ -0,0 +1,67 @@ +//nolint +package gov + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +const ( + DefaultCodespace sdk.CodespaceType = 5 + + CodeUnknownProposal sdk.CodeType = 1 + CodeInactiveProposal sdk.CodeType = 2 + CodeAlreadyActiveProposal sdk.CodeType = 3 + CodeAlreadyFinishedProposal sdk.CodeType = 4 + CodeAddressNotStaked sdk.CodeType = 5 + CodeInvalidTitle sdk.CodeType = 6 + CodeInvalidDescription sdk.CodeType = 7 + CodeInvalidProposalType sdk.CodeType = 8 + CodeInvalidVote sdk.CodeType = 9 + CodeInvalidGenesis sdk.CodeType = 10 + CodeInvalidProposalStatus sdk.CodeType = 11 +) + +//---------------------------------------- +// Error constructors + +func ErrUnknownProposal(codespace sdk.CodespaceType, proposalID int64) sdk.Error { + return sdk.NewError(codespace, CodeUnknownProposal, fmt.Sprintf("Unknown proposal - %d", proposalID)) +} + +func ErrInactiveProposal(codespace sdk.CodespaceType, proposalID int64) sdk.Error { + return sdk.NewError(codespace, CodeInactiveProposal, fmt.Sprintf("Inactive proposal - %d", proposalID)) +} + +func ErrAlreadyActiveProposal(codespace sdk.CodespaceType, proposalID int64) sdk.Error { + return sdk.NewError(codespace, CodeAlreadyActiveProposal, fmt.Sprintf("Proposal %d has been already active", proposalID)) +} + +func ErrAlreadyFinishedProposal(codespace sdk.CodespaceType, proposalID int64) sdk.Error { + return sdk.NewError(codespace, CodeAlreadyFinishedProposal, fmt.Sprintf("Proposal %d has already passed its voting period", proposalID)) +} + +func ErrAddressNotStaked(codespace sdk.CodespaceType, address sdk.AccAddress) sdk.Error { + return sdk.NewError(codespace, CodeAddressNotStaked, fmt.Sprintf("Address %s is not staked and is thus ineligible to vote", address)) +} + +func ErrInvalidTitle(codespace sdk.CodespaceType, title string) sdk.Error { + return sdk.NewError(codespace, CodeInvalidTitle, fmt.Sprintf("Proposal Title '%s' is not valid", title)) +} + +func ErrInvalidDescription(codespace sdk.CodespaceType, description string) sdk.Error { + return sdk.NewError(codespace, CodeInvalidDescription, fmt.Sprintf("Proposal Desciption '%s' is not valid", description)) +} + +func ErrInvalidProposalType(codespace sdk.CodespaceType, proposalType ProposalKind) sdk.Error { + return sdk.NewError(codespace, CodeInvalidProposalType, fmt.Sprintf("Proposal Type '%s' is not valid", proposalType)) +} + +func ErrInvalidVote(codespace sdk.CodespaceType, voteOption VoteOption) sdk.Error { + return sdk.NewError(codespace, CodeInvalidVote, fmt.Sprintf("'%v' is not a valid voting option", voteOption)) +} + +func ErrInvalidGenesis(codespace sdk.CodespaceType, msg string) sdk.Error { + return sdk.NewError(codespace, CodeInvalidVote, msg) +} diff --git a/modules/gov/genesis.go b/modules/gov/genesis.go new file mode 100644 index 000000000..40218ca86 --- /dev/null +++ b/modules/gov/genesis.go @@ -0,0 +1,41 @@ +package gov + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// GenesisState - all staking state that must be provided at genesis +type GenesisState struct { + StartingProposalID int64 `json:"starting_proposalID"` +} + +func NewGenesisState(startingProposalID int64) GenesisState { + return GenesisState{ + StartingProposalID: startingProposalID, + } +} + +// get raw genesis raw message for testing +func DefaultGenesisState() GenesisState { + return GenesisState{ + StartingProposalID: 1, + } +} + +// InitGenesis - store genesis parameters +func InitGenesis(ctx sdk.Context, k Keeper, data GenesisState) { + err := k.setInitialProposalID(ctx, data.StartingProposalID) + if err != nil { + // TODO: Handle this with #870 + panic(err) + } +} + +// WriteGenesis - output genesis parameters +func WriteGenesis(ctx sdk.Context, k Keeper) GenesisState { + initalProposalID, _ := k.getNewProposalID(ctx) + + return GenesisState{ + initalProposalID, + } +} diff --git a/modules/gov/handler.go b/modules/gov/handler.go new file mode 100644 index 000000000..636454571 --- /dev/null +++ b/modules/gov/handler.go @@ -0,0 +1,162 @@ +package gov + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// Handle all "gov" type messages. +func NewHandler(keeper Keeper) sdk.Handler { + return func(ctx sdk.Context, msg sdk.Msg) sdk.Result { + switch msg := msg.(type) { + case MsgDeposit: + return handleMsgDeposit(ctx, keeper, msg) + case MsgSubmitProposal: + return handleMsgSubmitProposal(ctx, keeper, msg) + case MsgVote: + return handleMsgVote(ctx, keeper, msg) + default: + errMsg := "Unrecognized gov msg type" + return sdk.ErrUnknownRequest(errMsg).Result() + } + } +} + +func handleMsgSubmitProposal(ctx sdk.Context, keeper Keeper, msg MsgSubmitProposal) sdk.Result { + + proposal := keeper.NewTextProposal(ctx, msg.Title, msg.Description, msg.ProposalType) + + err, votingStarted := keeper.AddDeposit(ctx, proposal.GetProposalID(), msg.Proposer, msg.InitialDeposit) + if err != nil { + return err.Result() + } + + proposalIDBytes := keeper.cdc.MustMarshalBinaryBare(proposal.GetProposalID()) + + tags := sdk.NewTags( + "action", []byte("submitProposal"), + "proposer", []byte(msg.Proposer.String()), + "proposalId", proposalIDBytes, + ) + + if votingStarted { + tags.AppendTag("votingPeriodStart", proposalIDBytes) + } + + return sdk.Result{ + Data: proposalIDBytes, + Tags: tags, + } +} + +func handleMsgDeposit(ctx sdk.Context, keeper Keeper, msg MsgDeposit) sdk.Result { + + err, votingStarted := keeper.AddDeposit(ctx, msg.ProposalID, msg.Depositer, msg.Amount) + if err != nil { + return err.Result() + } + + proposalIDBytes := keeper.cdc.MustMarshalBinaryBare(msg.ProposalID) + + // TODO: Add tag for if voting period started + tags := sdk.NewTags( + "action", []byte("deposit"), + "depositer", []byte(msg.Depositer.String()), + "proposalId", proposalIDBytes, + ) + + if votingStarted { + tags.AppendTag("votingPeriodStart", proposalIDBytes) + } + + return sdk.Result{ + Tags: tags, + } +} + +func handleMsgVote(ctx sdk.Context, keeper Keeper, msg MsgVote) sdk.Result { + + err := keeper.AddVote(ctx, msg.ProposalID, msg.Voter, msg.Option) + if err != nil { + return err.Result() + } + + proposalIDBytes := keeper.cdc.MustMarshalBinaryBare(msg.ProposalID) + + tags := sdk.NewTags( + "action", []byte("vote"), + "voter", []byte(msg.Voter.String()), + "proposalId", proposalIDBytes, + ) + return sdk.Result{ + Tags: tags, + } +} + +// Called every block, process inflation, update validator set +func EndBlocker(ctx sdk.Context, keeper Keeper) (tags sdk.Tags, nonVotingVals []sdk.AccAddress) { + + tags = sdk.NewTags() + + // Delete proposals that haven't met minDeposit + for shouldPopInactiveProposalQueue(ctx, keeper) { + inactiveProposal := keeper.InactiveProposalQueuePop(ctx) + if inactiveProposal.GetStatus() == StatusDepositPeriod { + proposalIDBytes := keeper.cdc.MustMarshalBinaryBare(inactiveProposal.GetProposalID()) + keeper.DeleteProposal(ctx, inactiveProposal) + tags.AppendTag("action", []byte("proposalDropped")) + tags.AppendTag("proposalId", proposalIDBytes) + } + } + + var passes bool + + // Check if earliest Active Proposal ended voting period yet + for shouldPopActiveProposalQueue(ctx, keeper) { + activeProposal := keeper.ActiveProposalQueuePop(ctx) + + if ctx.BlockHeight() >= activeProposal.GetVotingStartBlock()+keeper.GetVotingProcedure().VotingPeriod { + passes, nonVotingVals = tally(ctx, keeper, activeProposal) + proposalIDBytes := keeper.cdc.MustMarshalBinaryBare(activeProposal.GetProposalID()) + if passes { + keeper.RefundDeposits(ctx, activeProposal.GetProposalID()) + activeProposal.SetStatus(StatusPassed) + tags.AppendTag("action", []byte("proposalPassed")) + tags.AppendTag("proposalId", proposalIDBytes) + } else { + keeper.DeleteDeposits(ctx, activeProposal.GetProposalID()) + activeProposal.SetStatus(StatusRejected) + tags.AppendTag("action", []byte("proposalRejected")) + tags.AppendTag("proposalId", proposalIDBytes) + } + + keeper.SetProposal(ctx, activeProposal) + } + } + + return tags, nonVotingVals +} +func shouldPopInactiveProposalQueue(ctx sdk.Context, keeper Keeper) bool { + depositProcedure := keeper.GetDepositProcedure() + peekProposal := keeper.InactiveProposalQueuePeek(ctx) + + if peekProposal == nil { + return false + } else if peekProposal.GetStatus() != StatusDepositPeriod { + return true + } else if ctx.BlockHeight() >= peekProposal.GetSubmitBlock()+depositProcedure.MaxDepositPeriod { + return true + } + return false +} + +func shouldPopActiveProposalQueue(ctx sdk.Context, keeper Keeper) bool { + votingProcedure := keeper.GetVotingProcedure() + peekProposal := keeper.ActiveProposalQueuePeek(ctx) + + if peekProposal == nil { + return false + } else if ctx.BlockHeight() >= peekProposal.GetVotingStartBlock()+votingProcedure.VotingPeriod { + return true + } + return false +} diff --git a/modules/gov/keeper.go b/modules/gov/keeper.go new file mode 100644 index 000000000..572c5388e --- /dev/null +++ b/modules/gov/keeper.go @@ -0,0 +1,413 @@ +package gov + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + wire "github.com/cosmos/cosmos-sdk/wire" + "github.com/cosmos/cosmos-sdk/x/bank" +) + +// Governance Keeper +type Keeper struct { + // The reference to the CoinKeeper to modify balances + ck bank.Keeper + + // The ValidatorSet to get information about validators + vs sdk.ValidatorSet + + // The reference to the DelegationSet to get information about delegators + ds sdk.DelegationSet + + // The (unexposed) keys used to access the stores from the Context. + storeKey sdk.StoreKey + + // The wire codec for binary encoding/decoding. + cdc *wire.Codec + + // Reserved codespace + codespace sdk.CodespaceType +} + +// NewGovernanceMapper returns a mapper that uses go-wire to (binary) encode and decode gov types. +func NewKeeper(cdc *wire.Codec, key sdk.StoreKey, ck bank.Keeper, ds sdk.DelegationSet, codespace sdk.CodespaceType) Keeper { + return Keeper{ + storeKey: key, + ck: ck, + ds: ds, + vs: ds.GetValidatorSet(), + cdc: cdc, + codespace: codespace, + } +} + +// Returns the go-wire codec. +func (keeper Keeper) WireCodec() *wire.Codec { + return keeper.cdc +} + +// ===================================================== +// Proposals + +// Creates a NewProposal +func (keeper Keeper) NewTextProposal(ctx sdk.Context, title string, description string, proposalType ProposalKind) Proposal { + proposalID, err := keeper.getNewProposalID(ctx) + if err != nil { + return nil + } + var proposal Proposal = &TextProposal{ + ProposalID: proposalID, + Title: title, + Description: description, + ProposalType: proposalType, + Status: StatusDepositPeriod, + TotalDeposit: sdk.Coins{}, + SubmitBlock: ctx.BlockHeight(), + VotingStartBlock: -1, // TODO: Make Time + } + keeper.SetProposal(ctx, proposal) + keeper.InactiveProposalQueuePush(ctx, proposal) + return proposal +} + +// Get Proposal from store by ProposalID +func (keeper Keeper) GetProposal(ctx sdk.Context, proposalID int64) Proposal { + store := ctx.KVStore(keeper.storeKey) + bz := store.Get(KeyProposal(proposalID)) + if bz == nil { + return nil + } + + var proposal Proposal + keeper.cdc.MustUnmarshalBinary(bz, &proposal) + + return proposal +} + +// Implements sdk.AccountMapper. +func (keeper Keeper) SetProposal(ctx sdk.Context, proposal Proposal) { + store := ctx.KVStore(keeper.storeKey) + bz := keeper.cdc.MustMarshalBinary(proposal) + store.Set(KeyProposal(proposal.GetProposalID()), bz) +} + +// Implements sdk.AccountMapper. +func (keeper Keeper) DeleteProposal(ctx sdk.Context, proposal Proposal) { + store := ctx.KVStore(keeper.storeKey) + store.Delete(KeyProposal(proposal.GetProposalID())) +} + +func (keeper Keeper) setInitialProposalID(ctx sdk.Context, proposalID int64) sdk.Error { + store := ctx.KVStore(keeper.storeKey) + bz := store.Get(KeyNextProposalID) + if bz != nil { + return ErrInvalidGenesis(keeper.codespace, "Initial ProposalID already set") + } + bz = keeper.cdc.MustMarshalBinary(proposalID) + store.Set(KeyNextProposalID, bz) + return nil +} + +func (keeper Keeper) getNewProposalID(ctx sdk.Context) (proposalID int64, err sdk.Error) { + store := ctx.KVStore(keeper.storeKey) + bz := store.Get(KeyNextProposalID) + if bz == nil { + return -1, ErrInvalidGenesis(keeper.codespace, "InitialProposalID never set") + } + keeper.cdc.MustUnmarshalBinary(bz, &proposalID) + bz = keeper.cdc.MustMarshalBinary(proposalID + 1) + store.Set(KeyNextProposalID, bz) + return proposalID, nil +} + +func (keeper Keeper) activateVotingPeriod(ctx sdk.Context, proposal Proposal) { + proposal.SetVotingStartBlock(ctx.BlockHeight()) + proposal.SetStatus(StatusVotingPeriod) + keeper.SetProposal(ctx, proposal) + keeper.ActiveProposalQueuePush(ctx, proposal) +} + +// ===================================================== +// Procedures + +var ( + defaultMinDeposit int64 = 10 + defaultMaxDepositPeriod int64 = 10000 + defaultVotingPeriod int64 = 10000 +) + +// Gets procedure from store. TODO: move to global param store and allow for updating of this +func (keeper Keeper) GetDepositProcedure() DepositProcedure { + return DepositProcedure{ + MinDeposit: sdk.Coins{sdk.NewCoin("steak", defaultMinDeposit)}, + MaxDepositPeriod: defaultMaxDepositPeriod, + } +} + +// Gets procedure from store. TODO: move to global param store and allow for updating of this +func (keeper Keeper) GetVotingProcedure() VotingProcedure { + return VotingProcedure{ + VotingPeriod: defaultVotingPeriod, + } +} + +// Gets procedure from store. TODO: move to global param store and allow for updating of this +func (keeper Keeper) GetTallyingProcedure() TallyingProcedure { + return TallyingProcedure{ + Threshold: sdk.NewRat(1, 2), + Veto: sdk.NewRat(1, 3), + GovernancePenalty: sdk.NewRat(1, 100), + } +} + +// ===================================================== +// Votes + +// Adds a vote on a specific proposal +func (keeper Keeper) AddVote(ctx sdk.Context, proposalID int64, voterAddr sdk.AccAddress, option VoteOption) sdk.Error { + proposal := keeper.GetProposal(ctx, proposalID) + if proposal == nil { + return ErrUnknownProposal(keeper.codespace, proposalID) + } + if proposal.GetStatus() != StatusVotingPeriod { + return ErrInactiveProposal(keeper.codespace, proposalID) + } + + if !validVoteOption(option) { + return ErrInvalidVote(keeper.codespace, option) + } + + vote := Vote{ + ProposalID: proposalID, + Voter: voterAddr, + Option: option, + } + keeper.setVote(ctx, proposalID, voterAddr, vote) + + return nil +} + +// Gets the vote of a specific voter on a specific proposal +func (keeper Keeper) GetVote(ctx sdk.Context, proposalID int64, voterAddr sdk.AccAddress) (Vote, bool) { + store := ctx.KVStore(keeper.storeKey) + bz := store.Get(KeyVote(proposalID, voterAddr)) + if bz == nil { + return Vote{}, false + } + var vote Vote + keeper.cdc.MustUnmarshalBinary(bz, &vote) + return vote, true +} + +func (keeper Keeper) setVote(ctx sdk.Context, proposalID int64, voterAddr sdk.AccAddress, vote Vote) { + store := ctx.KVStore(keeper.storeKey) + bz := keeper.cdc.MustMarshalBinary(vote) + store.Set(KeyVote(proposalID, voterAddr), bz) +} + +// Gets all the votes on a specific proposal +func (keeper Keeper) GetVotes(ctx sdk.Context, proposalID int64) sdk.Iterator { + store := ctx.KVStore(keeper.storeKey) + return sdk.KVStorePrefixIterator(store, KeyVotesSubspace(proposalID)) +} + +func (keeper Keeper) deleteVote(ctx sdk.Context, proposalID int64, voterAddr sdk.AccAddress) { + store := ctx.KVStore(keeper.storeKey) + store.Delete(KeyVote(proposalID, voterAddr)) +} + +// ===================================================== +// Deposits + +// Gets the deposit of a specific depositer on a specific proposal +func (keeper Keeper) GetDeposit(ctx sdk.Context, proposalID int64, depositerAddr sdk.AccAddress) (Deposit, bool) { + store := ctx.KVStore(keeper.storeKey) + bz := store.Get(KeyDeposit(proposalID, depositerAddr)) + if bz == nil { + return Deposit{}, false + } + var deposit Deposit + keeper.cdc.MustUnmarshalBinary(bz, &deposit) + return deposit, true +} + +func (keeper Keeper) setDeposit(ctx sdk.Context, proposalID int64, depositerAddr sdk.AccAddress, deposit Deposit) { + store := ctx.KVStore(keeper.storeKey) + bz := keeper.cdc.MustMarshalBinary(deposit) + store.Set(KeyDeposit(proposalID, depositerAddr), bz) +} + +// Adds or updates a deposit of a specific depositer on a specific proposal +// Activates voting period when appropriate +func (keeper Keeper) AddDeposit(ctx sdk.Context, proposalID int64, depositerAddr sdk.AccAddress, depositAmount sdk.Coins) (sdk.Error, bool) { + // Checks to see if proposal exists + proposal := keeper.GetProposal(ctx, proposalID) + if proposal == nil { + return ErrUnknownProposal(keeper.codespace, proposalID), false + } + + // Check if proposal is still depositable + if (proposal.GetStatus() != StatusDepositPeriod) && (proposal.GetStatus() != StatusVotingPeriod) { + return ErrAlreadyFinishedProposal(keeper.codespace, proposalID), false + } + + // Subtract coins from depositer's account + _, _, err := keeper.ck.SubtractCoins(ctx, depositerAddr, depositAmount) + if err != nil { + return err, false + } + + // Update Proposal + proposal.SetTotalDeposit(proposal.GetTotalDeposit().Plus(depositAmount)) + keeper.SetProposal(ctx, proposal) + + // Check if deposit tipped proposal into voting period + // Active voting period if so + activatedVotingPeriod := false + if proposal.GetStatus() == StatusDepositPeriod && proposal.GetTotalDeposit().IsGTE(keeper.GetDepositProcedure().MinDeposit) { + keeper.activateVotingPeriod(ctx, proposal) + activatedVotingPeriod = true + } + + // Add or update deposit object + currDeposit, found := keeper.GetDeposit(ctx, proposalID, depositerAddr) + if !found { + newDeposit := Deposit{depositerAddr, proposalID, depositAmount} + keeper.setDeposit(ctx, proposalID, depositerAddr, newDeposit) + } else { + currDeposit.Amount = currDeposit.Amount.Plus(depositAmount) + keeper.setDeposit(ctx, proposalID, depositerAddr, currDeposit) + } + + return nil, activatedVotingPeriod +} + +// Gets all the deposits on a specific proposal +func (keeper Keeper) GetDeposits(ctx sdk.Context, proposalID int64) sdk.Iterator { + store := ctx.KVStore(keeper.storeKey) + return sdk.KVStorePrefixIterator(store, KeyDepositsSubspace(proposalID)) +} + +// Returns and deletes all the deposits on a specific proposal +func (keeper Keeper) RefundDeposits(ctx sdk.Context, proposalID int64) { + store := ctx.KVStore(keeper.storeKey) + depositsIterator := keeper.GetDeposits(ctx, proposalID) + + for ; depositsIterator.Valid(); depositsIterator.Next() { + deposit := &Deposit{} + keeper.cdc.MustUnmarshalBinary(depositsIterator.Value(), deposit) + + _, _, err := keeper.ck.AddCoins(ctx, deposit.Depositer, deposit.Amount) + if err != nil { + panic("should not happen") + } + + store.Delete(depositsIterator.Key()) + } + + depositsIterator.Close() +} + +// Deletes all the deposits on a specific proposal without refunding them +func (keeper Keeper) DeleteDeposits(ctx sdk.Context, proposalID int64) { + store := ctx.KVStore(keeper.storeKey) + depositsIterator := keeper.GetDeposits(ctx, proposalID) + + for ; depositsIterator.Valid(); depositsIterator.Next() { + store.Delete(depositsIterator.Key()) + } + + depositsIterator.Close() +} + +// ===================================================== +// ProposalQueues + +func (keeper Keeper) getActiveProposalQueue(ctx sdk.Context) ProposalQueue { + store := ctx.KVStore(keeper.storeKey) + bz := store.Get(KeyActiveProposalQueue) + if bz == nil { + return nil + } + + var proposalQueue ProposalQueue + keeper.cdc.MustUnmarshalBinary(bz, &proposalQueue) + + return proposalQueue +} + +func (keeper Keeper) setActiveProposalQueue(ctx sdk.Context, proposalQueue ProposalQueue) { + store := ctx.KVStore(keeper.storeKey) + bz := keeper.cdc.MustMarshalBinary(proposalQueue) + store.Set(KeyActiveProposalQueue, bz) +} + +// Return the Proposal at the front of the ProposalQueue +func (keeper Keeper) ActiveProposalQueuePeek(ctx sdk.Context) Proposal { + proposalQueue := keeper.getActiveProposalQueue(ctx) + if len(proposalQueue) == 0 { + return nil + } + return keeper.GetProposal(ctx, proposalQueue[0]) +} + +// Remove and return a Proposal from the front of the ProposalQueue +func (keeper Keeper) ActiveProposalQueuePop(ctx sdk.Context) Proposal { + proposalQueue := keeper.getActiveProposalQueue(ctx) + if len(proposalQueue) == 0 { + return nil + } + frontElement, proposalQueue := proposalQueue[0], proposalQueue[1:] + keeper.setActiveProposalQueue(ctx, proposalQueue) + return keeper.GetProposal(ctx, frontElement) +} + +// Add a proposalID to the back of the ProposalQueue +func (keeper Keeper) ActiveProposalQueuePush(ctx sdk.Context, proposal Proposal) { + proposalQueue := append(keeper.getActiveProposalQueue(ctx), proposal.GetProposalID()) + keeper.setActiveProposalQueue(ctx, proposalQueue) +} + +func (keeper Keeper) getInactiveProposalQueue(ctx sdk.Context) ProposalQueue { + store := ctx.KVStore(keeper.storeKey) + bz := store.Get(KeyInactiveProposalQueue) + if bz == nil { + return nil + } + + var proposalQueue ProposalQueue + + keeper.cdc.MustUnmarshalBinary(bz, &proposalQueue) + + return proposalQueue +} + +func (keeper Keeper) setInactiveProposalQueue(ctx sdk.Context, proposalQueue ProposalQueue) { + store := ctx.KVStore(keeper.storeKey) + bz := keeper.cdc.MustMarshalBinary(proposalQueue) + store.Set(KeyInactiveProposalQueue, bz) +} + +// Return the Proposal at the front of the ProposalQueue +func (keeper Keeper) InactiveProposalQueuePeek(ctx sdk.Context) Proposal { + proposalQueue := keeper.getInactiveProposalQueue(ctx) + if len(proposalQueue) == 0 { + return nil + } + return keeper.GetProposal(ctx, proposalQueue[0]) +} + +// Remove and return a Proposal from the front of the ProposalQueue +func (keeper Keeper) InactiveProposalQueuePop(ctx sdk.Context) Proposal { + proposalQueue := keeper.getInactiveProposalQueue(ctx) + if len(proposalQueue) == 0 { + return nil + } + frontElement, proposalQueue := proposalQueue[0], proposalQueue[1:] + keeper.setInactiveProposalQueue(ctx, proposalQueue) + return keeper.GetProposal(ctx, frontElement) +} + +// Add a proposalID to the back of the ProposalQueue +func (keeper Keeper) InactiveProposalQueuePush(ctx sdk.Context, proposal Proposal) { + proposalQueue := append(keeper.getInactiveProposalQueue(ctx), proposal.GetProposalID()) + keeper.setInactiveProposalQueue(ctx, proposalQueue) +} diff --git a/modules/gov/keeper_keys.go b/modules/gov/keeper_keys.go new file mode 100644 index 000000000..7b1bf43f2 --- /dev/null +++ b/modules/gov/keeper_keys.go @@ -0,0 +1,41 @@ +package gov + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// TODO remove some of these prefixes once have working multistore + +// Key for getting a the next available proposalID from the store +var ( + KeyNextProposalID = []byte("newProposalID") + KeyActiveProposalQueue = []byte("activeProposalQueue") + KeyInactiveProposalQueue = []byte("inactiveProposalQueue") +) + +// Key for getting a specific proposal from the store +func KeyProposal(proposalID int64) []byte { + return []byte(fmt.Sprintf("proposals:%d", proposalID)) +} + +// Key for getting a specific deposit from the store +func KeyDeposit(proposalID int64, depositerAddr sdk.AccAddress) []byte { + return []byte(fmt.Sprintf("deposits:%d:%d", proposalID, depositerAddr)) +} + +// Key for getting a specific vote from the store +func KeyVote(proposalID int64, voterAddr sdk.AccAddress) []byte { + return []byte(fmt.Sprintf("votes:%d:%d", proposalID, voterAddr)) +} + +// Key for getting all deposits on a proposal from the store +func KeyDepositsSubspace(proposalID int64) []byte { + return []byte(fmt.Sprintf("deposits:%d:", proposalID)) +} + +// Key for getting all votes on a proposal from the store +func KeyVotesSubspace(proposalID int64) []byte { + return []byte(fmt.Sprintf("votes:%d:", proposalID)) +} diff --git a/modules/gov/msgs.go b/modules/gov/msgs.go new file mode 100644 index 000000000..5d85f689e --- /dev/null +++ b/modules/gov/msgs.go @@ -0,0 +1,194 @@ +package gov + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// name to idetify transaction types +const MsgType = "gov" + +//----------------------------------------------------------- +// MsgSubmitProposal +type MsgSubmitProposal struct { + Title string // Title of the proposal + Description string // Description of the proposal + ProposalType ProposalKind // Type of proposal. Initial set {PlainTextProposal, SoftwareUpgradeProposal} + Proposer sdk.AccAddress // Address of the proposer + InitialDeposit sdk.Coins // Initial deposit paid by sender. Must be strictly positive. +} + +func NewMsgSubmitProposal(title string, description string, proposalType ProposalKind, proposer sdk.AccAddress, initialDeposit sdk.Coins) MsgSubmitProposal { + return MsgSubmitProposal{ + Title: title, + Description: description, + ProposalType: proposalType, + Proposer: proposer, + InitialDeposit: initialDeposit, + } +} + +// Implements Msg. +func (msg MsgSubmitProposal) Type() string { return MsgType } + +// Implements Msg. +func (msg MsgSubmitProposal) ValidateBasic() sdk.Error { + if len(msg.Title) == 0 { + return ErrInvalidTitle(DefaultCodespace, msg.Title) // TODO: Proper Error + } + if len(msg.Description) == 0 { + return ErrInvalidDescription(DefaultCodespace, msg.Description) // TODO: Proper Error + } + if !validProposalType(msg.ProposalType) { + return ErrInvalidProposalType(DefaultCodespace, msg.ProposalType) + } + if len(msg.Proposer) == 0 { + return sdk.ErrInvalidAddress(msg.Proposer.String()) + } + if !msg.InitialDeposit.IsValid() { + return sdk.ErrInvalidCoins(msg.InitialDeposit.String()) + } + if !msg.InitialDeposit.IsNotNegative() { + return sdk.ErrInvalidCoins(msg.InitialDeposit.String()) + } + return nil +} + +func (msg MsgSubmitProposal) String() string { + return fmt.Sprintf("MsgSubmitProposal{%s, %s, %s, %v}", msg.Title, msg.Description, msg.ProposalType, msg.InitialDeposit) +} + +// Implements Msg. +func (msg MsgSubmitProposal) Get(key interface{}) (value interface{}) { + return nil +} + +// Implements Msg. +func (msg MsgSubmitProposal) GetSignBytes() []byte { + b, err := msgCdc.MarshalJSON(msg) + if err != nil { + panic(err) + } + return sdk.MustSortJSON(b) +} + +// Implements Msg. +func (msg MsgSubmitProposal) GetSigners() []sdk.AccAddress { + return []sdk.AccAddress{msg.Proposer} +} + +//----------------------------------------------------------- +// MsgDeposit +type MsgDeposit struct { + ProposalID int64 `json:"proposalID"` // ID of the proposal + Depositer sdk.AccAddress `json:"depositer"` // Address of the depositer + Amount sdk.Coins `json:"amount"` // Coins to add to the proposal's deposit +} + +func NewMsgDeposit(depositer sdk.AccAddress, proposalID int64, amount sdk.Coins) MsgDeposit { + return MsgDeposit{ + ProposalID: proposalID, + Depositer: depositer, + Amount: amount, + } +} + +// Implements Msg. +func (msg MsgDeposit) Type() string { return MsgType } + +// Implements Msg. +func (msg MsgDeposit) ValidateBasic() sdk.Error { + if len(msg.Depositer) == 0 { + return sdk.ErrInvalidAddress(msg.Depositer.String()) + } + if !msg.Amount.IsValid() { + return sdk.ErrInvalidCoins(msg.Amount.String()) + } + if !msg.Amount.IsNotNegative() { + return sdk.ErrInvalidCoins(msg.Amount.String()) + } + if msg.ProposalID < 0 { + return ErrUnknownProposal(DefaultCodespace, msg.ProposalID) + } + return nil +} + +func (msg MsgDeposit) String() string { + return fmt.Sprintf("MsgDeposit{%s=>%v: %v}", msg.Depositer, msg.ProposalID, msg.Amount) +} + +// Implements Msg. +func (msg MsgDeposit) Get(key interface{}) (value interface{}) { + return nil +} + +// Implements Msg. +func (msg MsgDeposit) GetSignBytes() []byte { + b, err := msgCdc.MarshalJSON(msg) + if err != nil { + panic(err) + } + return sdk.MustSortJSON(b) +} + +// Implements Msg. +func (msg MsgDeposit) GetSigners() []sdk.AccAddress { + return []sdk.AccAddress{msg.Depositer} +} + +//----------------------------------------------------------- +// MsgVote +type MsgVote struct { + ProposalID int64 // proposalID of the proposal + Voter sdk.AccAddress // address of the voter + Option VoteOption // option from OptionSet chosen by the voter +} + +func NewMsgVote(voter sdk.AccAddress, proposalID int64, option VoteOption) MsgVote { + return MsgVote{ + ProposalID: proposalID, + Voter: voter, + Option: option, + } +} + +// Implements Msg. +func (msg MsgVote) Type() string { return MsgType } + +// Implements Msg. +func (msg MsgVote) ValidateBasic() sdk.Error { + if len(msg.Voter.Bytes()) == 0 { + return sdk.ErrInvalidAddress(msg.Voter.String()) + } + if msg.ProposalID < 0 { + return ErrUnknownProposal(DefaultCodespace, msg.ProposalID) + } + if !validVoteOption(msg.Option) { + return ErrInvalidVote(DefaultCodespace, msg.Option) + } + return nil +} + +func (msg MsgVote) String() string { + return fmt.Sprintf("MsgVote{%v - %s}", msg.ProposalID, msg.Option) +} + +// Implements Msg. +func (msg MsgVote) Get(key interface{}) (value interface{}) { + return nil +} + +// Implements Msg. +func (msg MsgVote) GetSignBytes() []byte { + b, err := msgCdc.MarshalJSON(msg) + if err != nil { + panic(err) + } + return sdk.MustSortJSON(b) +} + +// Implements Msg. +func (msg MsgVote) GetSigners() []sdk.AccAddress { + return []sdk.AccAddress{msg.Voter} +} diff --git a/modules/gov/procedures.go b/modules/gov/procedures.go new file mode 100644 index 000000000..f46c2149f --- /dev/null +++ b/modules/gov/procedures.go @@ -0,0 +1,23 @@ +package gov + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// Procedure around Deposits for governance +type DepositProcedure struct { + MinDeposit sdk.Coins `json:"min_deposit"` // Minimum deposit for a proposal to enter voting period. + MaxDepositPeriod int64 `json:"max_deposit_period"` // Maximum period for Atom holders to deposit on a proposal. Initial value: 2 months +} + +// Procedure around Tallying votes in governance +type TallyingProcedure struct { + Threshold sdk.Rat `json:"threshold"` // Minimum propotion of Yes votes for proposal to pass. Initial value: 0.5 + Veto sdk.Rat `json:"veto"` // Minimum value of Veto votes to Total votes ratio for proposal to be vetoed. Initial value: 1/3 + GovernancePenalty sdk.Rat `json:"governance_penalty"` // Penalty if validator does not vote +} + +// Procedure around Voting in governance +type VotingProcedure struct { + VotingPeriod int64 `json:"voting_period"` // Length of the voting period. +} diff --git a/modules/gov/proposals.go b/modules/gov/proposals.go new file mode 100644 index 000000000..bb6b0aed4 --- /dev/null +++ b/modules/gov/proposals.go @@ -0,0 +1,288 @@ +package gov + +import ( + "encoding/json" + "fmt" + + "github.com/pkg/errors" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +//----------------------------------------------------------- +// Proposal interface +type Proposal interface { + GetProposalID() int64 + SetProposalID(int64) + + GetTitle() string + SetTitle(string) + + GetDescription() string + SetDescription(string) + + GetProposalType() ProposalKind + SetProposalType(ProposalKind) + + GetStatus() ProposalStatus + SetStatus(ProposalStatus) + + GetSubmitBlock() int64 + SetSubmitBlock(int64) + + GetTotalDeposit() sdk.Coins + SetTotalDeposit(sdk.Coins) + + GetVotingStartBlock() int64 + SetVotingStartBlock(int64) +} + +// checks if two proposals are equal +func ProposalEqual(proposalA Proposal, proposalB Proposal) bool { + if proposalA.GetProposalID() != proposalB.GetProposalID() || + proposalA.GetTitle() != proposalB.GetTitle() || + proposalA.GetDescription() != proposalB.GetDescription() || + proposalA.GetProposalType() != proposalB.GetProposalType() || + proposalA.GetStatus() != proposalB.GetStatus() || + proposalA.GetSubmitBlock() != proposalB.GetSubmitBlock() || + !(proposalA.GetTotalDeposit().IsEqual(proposalB.GetTotalDeposit())) || + proposalA.GetVotingStartBlock() != proposalB.GetVotingStartBlock() { + return false + } + return true +} + +//----------------------------------------------------------- +// Text Proposals +type TextProposal struct { + ProposalID int64 `json:"proposal_id"` // ID of the proposal + Title string `json:"title"` // Title of the proposal + Description string `json:"description"` // Description of the proposal + ProposalType ProposalKind `json:"proposal_type"` // Type of proposal. Initial set {PlainTextProposal, SoftwareUpgradeProposal} + + Status ProposalStatus `json:"proposal_status"` // Status of the Proposal {Pending, Active, Passed, Rejected} + + SubmitBlock int64 `json:"submit_block"` // Height of the block where TxGovSubmitProposal was included + TotalDeposit sdk.Coins `json:"total_deposit"` // Current deposit on this proposal. Initial value is set at InitialDeposit + + VotingStartBlock int64 `json:"voting_start_block"` // Height of the block where MinDeposit was reached. -1 if MinDeposit is not reached +} + +// Implements Proposal Interface +var _ Proposal = (*TextProposal)(nil) + +// nolint +func (tp TextProposal) GetProposalID() int64 { return tp.ProposalID } +func (tp *TextProposal) SetProposalID(proposalID int64) { tp.ProposalID = proposalID } +func (tp TextProposal) GetTitle() string { return tp.Title } +func (tp *TextProposal) SetTitle(title string) { tp.Title = title } +func (tp TextProposal) GetDescription() string { return tp.Description } +func (tp *TextProposal) SetDescription(description string) { tp.Description = description } +func (tp TextProposal) GetProposalType() ProposalKind { return tp.ProposalType } +func (tp *TextProposal) SetProposalType(proposalType ProposalKind) { tp.ProposalType = proposalType } +func (tp TextProposal) GetStatus() ProposalStatus { return tp.Status } +func (tp *TextProposal) SetStatus(status ProposalStatus) { tp.Status = status } +func (tp TextProposal) GetSubmitBlock() int64 { return tp.SubmitBlock } +func (tp *TextProposal) SetSubmitBlock(submitBlock int64) { tp.SubmitBlock = submitBlock } +func (tp TextProposal) GetTotalDeposit() sdk.Coins { return tp.TotalDeposit } +func (tp *TextProposal) SetTotalDeposit(totalDeposit sdk.Coins) { tp.TotalDeposit = totalDeposit } +func (tp TextProposal) GetVotingStartBlock() int64 { return tp.VotingStartBlock } +func (tp *TextProposal) SetVotingStartBlock(votingStartBlock int64) { + tp.VotingStartBlock = votingStartBlock +} + +//----------------------------------------------------------- +// ProposalQueue +type ProposalQueue []int64 + +//----------------------------------------------------------- +// ProposalKind + +// Type that represents Proposal Type as a byte +type ProposalKind byte + +//nolint +const ( + ProposalTypeText ProposalKind = 0x01 + ProposalTypeParameterChange ProposalKind = 0x02 + ProposalTypeSoftwareUpgrade ProposalKind = 0x03 +) + +// String to proposalType byte. Returns ff if invalid. +func ProposalTypeFromString(str string) (ProposalKind, error) { + switch str { + case "Text": + return ProposalTypeText, nil + case "ParameterChange": + return ProposalTypeParameterChange, nil + case "SoftwareUpgrade": + return ProposalTypeSoftwareUpgrade, nil + default: + return ProposalKind(0xff), errors.Errorf("'%s' is not a valid proposal type", str) + } +} + +// is defined ProposalType? +func validProposalType(pt ProposalKind) bool { + if pt == ProposalTypeText || + pt == ProposalTypeParameterChange || + pt == ProposalTypeSoftwareUpgrade { + return true + } + return false +} + +// Marshal needed for protobuf compatibility +func (pt ProposalKind) Marshal() ([]byte, error) { + return []byte{byte(pt)}, nil +} + +// Unmarshal needed for protobuf compatibility +func (pt *ProposalKind) Unmarshal(data []byte) error { + *pt = ProposalKind(data[0]) + return nil +} + +// Marshals to JSON using string +func (pt ProposalKind) MarshalJSON() ([]byte, error) { + return json.Marshal(pt.String()) +} + +// Unmarshals from JSON assuming Bech32 encoding +func (pt *ProposalKind) UnmarshalJSON(data []byte) error { + var s string + err := json.Unmarshal(data, &s) + if err != nil { + return nil + } + + bz2, err := ProposalTypeFromString(s) + if err != nil { + return err + } + *pt = bz2 + return nil +} + +// Turns VoteOption byte to String +func (pt ProposalKind) String() string { + switch pt { + case 0x00: + return "Text" + case 0x01: + return "ParameterChange" + case 0x02: + return "SoftwareUpgrade" + default: + return "" + } +} + +// For Printf / Sprintf, returns bech32 when using %s +func (pt ProposalKind) Format(s fmt.State, verb rune) { + switch verb { + case 's': + s.Write([]byte(fmt.Sprintf("%s", pt.String()))) + default: + s.Write([]byte(fmt.Sprintf("%v", byte(pt)))) + } +} + +//----------------------------------------------------------- +// ProposalStatus + +// Type that represents Proposal Status as a byte +type ProposalStatus byte + +//nolint +const ( + StatusDepositPeriod ProposalStatus = 0x01 + StatusVotingPeriod ProposalStatus = 0x02 + StatusPassed ProposalStatus = 0x03 + StatusRejected ProposalStatus = 0x04 +) + +// ProposalStatusToString turns a string into a ProposalStatus +func ProposalStatusFromString(str string) (ProposalStatus, error) { + switch str { + case "DepositPeriod": + return StatusDepositPeriod, nil + case "VotingPeriod": + return StatusVotingPeriod, nil + case "Passed": + return StatusPassed, nil + case "Rejected": + return StatusRejected, nil + default: + return ProposalStatus(0xff), errors.Errorf("'%s' is not a valid proposal status", str) + } +} + +// is defined ProposalType? +func validProposalStatus(status ProposalStatus) bool { + if status == StatusDepositPeriod || + status == StatusVotingPeriod || + status == StatusPassed || + status == StatusRejected { + return true + } + return false +} + +// Marshal needed for protobuf compatibility +func (status ProposalStatus) Marshal() ([]byte, error) { + return []byte{byte(status)}, nil +} + +// Unmarshal needed for protobuf compatibility +func (status *ProposalStatus) Unmarshal(data []byte) error { + *status = ProposalStatus(data[0]) + return nil +} + +// Marshals to JSON using string +func (status ProposalStatus) MarshalJSON() ([]byte, error) { + return json.Marshal(status.String()) +} + +// Unmarshals from JSON assuming Bech32 encoding +func (status *ProposalStatus) UnmarshalJSON(data []byte) error { + var s string + err := json.Unmarshal(data, &s) + if err != nil { + return nil + } + + bz2, err := ProposalStatusFromString(s) + if err != nil { + return err + } + *status = bz2 + return nil +} + +// Turns VoteStatus byte to String +func (status ProposalStatus) String() string { + switch status { + case StatusDepositPeriod: + return "DepositPeriod" + case StatusVotingPeriod: + return "VotingPeriod" + case StatusPassed: + return "Passed" + case StatusRejected: + return "Rejected" + default: + return "" + } +} + +// For Printf / Sprintf, returns bech32 when using %s +func (status ProposalStatus) Format(s fmt.State, verb rune) { + switch verb { + case 's': + s.Write([]byte(fmt.Sprintf("%s", status.String()))) + default: + s.Write([]byte(fmt.Sprintf("%v", byte(status)))) + } +} diff --git a/modules/gov/tally.go b/modules/gov/tally.go new file mode 100644 index 000000000..38462e72d --- /dev/null +++ b/modules/gov/tally.go @@ -0,0 +1,100 @@ +package gov + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// validatorGovInfo used for tallying +type validatorGovInfo struct { + Address sdk.AccAddress // sdk.AccAddress of the validator owner + Power sdk.Rat // Power of a Validator + DelegatorShares sdk.Rat // Total outstanding delegator shares + Minus sdk.Rat // Minus of validator, used to compute validator's voting power + Vote VoteOption // Vote of the validator +} + +func tally(ctx sdk.Context, keeper Keeper, proposal Proposal) (passes bool, nonVoting []sdk.AccAddress) { + results := make(map[VoteOption]sdk.Rat) + results[OptionYes] = sdk.ZeroRat() + results[OptionAbstain] = sdk.ZeroRat() + results[OptionNo] = sdk.ZeroRat() + results[OptionNoWithVeto] = sdk.ZeroRat() + + totalVotingPower := sdk.ZeroRat() + currValidators := make(map[string]validatorGovInfo) + + keeper.vs.IterateValidatorsBonded(ctx, func(index int64, validator sdk.Validator) (stop bool) { + currValidators[validator.GetOwner().String()] = validatorGovInfo{ + Address: validator.GetOwner(), + Power: validator.GetPower(), + DelegatorShares: validator.GetDelegatorShares(), + Minus: sdk.ZeroRat(), + Vote: OptionEmpty, + } + return false + }) + + // iterate over all the votes + votesIterator := keeper.GetVotes(ctx, proposal.GetProposalID()) + for ; votesIterator.Valid(); votesIterator.Next() { + vote := &Vote{} + keeper.cdc.MustUnmarshalBinary(votesIterator.Value(), vote) + + // if validator, just record it in the map + // if delegator tally voting power + if val, ok := currValidators[vote.Voter.String()]; ok { + val.Vote = vote.Option + currValidators[vote.Voter.String()] = val + } else { + + keeper.ds.IterateDelegations(ctx, vote.Voter, func(index int64, delegation sdk.Delegation) (stop bool) { + val := currValidators[delegation.GetValidator().String()] + val.Minus = val.Minus.Add(delegation.GetBondShares()) + currValidators[delegation.GetValidator().String()] = val + + delegatorShare := delegation.GetBondShares().Quo(val.DelegatorShares) + votingPower := val.Power.Mul(delegatorShare) + + results[vote.Option] = results[vote.Option].Add(votingPower) + totalVotingPower = totalVotingPower.Add(votingPower) + + return false + }) + } + + keeper.deleteVote(ctx, vote.ProposalID, vote.Voter) + } + votesIterator.Close() + + // Iterate over the validators again to tally their voting power and see who didn't vote + nonVoting = []sdk.AccAddress{} + for _, val := range currValidators { + if val.Vote == OptionEmpty { + nonVoting = append(nonVoting, val.Address) + continue + } + sharesAfterMinus := val.DelegatorShares.Sub(val.Minus) + percentAfterMinus := sharesAfterMinus.Quo(val.DelegatorShares) + votingPower := val.Power.Mul(percentAfterMinus) + + results[val.Vote] = results[val.Vote].Add(votingPower) + totalVotingPower = totalVotingPower.Add(votingPower) + } + + tallyingProcedure := keeper.GetTallyingProcedure() + + // If no one votes, proposal fails + if totalVotingPower.Sub(results[OptionAbstain]).Equal(sdk.ZeroRat()) { + return false, nonVoting + } + // If more than 1/3 of voters veto, proposal fails + if results[OptionNoWithVeto].Quo(totalVotingPower).GT(tallyingProcedure.Veto) { + return false, nonVoting + } + // If more than 1/2 of non-abstaining voters vote Yes, proposal passes + if results[OptionYes].Quo(totalVotingPower.Sub(results[OptionAbstain])).GT(tallyingProcedure.Threshold) { + return true, nonVoting + } + // If more than 1/2 of non-abstaining voters vote No, proposal fails + return false, nonVoting +} diff --git a/modules/gov/test_common.go b/modules/gov/test_common.go new file mode 100644 index 000000000..5567e4697 --- /dev/null +++ b/modules/gov/test_common.go @@ -0,0 +1,114 @@ +package gov + +import ( + "bytes" + "log" + "sort" + "testing" + + "github.com/stretchr/testify/require" + + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/crypto" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/bank" + "github.com/cosmos/cosmos-sdk/x/mock" + "github.com/cosmos/cosmos-sdk/x/stake" +) + +// initialize the mock application for this module +func getMockApp(t *testing.T, numGenAccs int) (*mock.App, Keeper, stake.Keeper, []sdk.AccAddress, []crypto.PubKey, []crypto.PrivKey) { + mapp := mock.NewApp() + + stake.RegisterWire(mapp.Cdc) + RegisterWire(mapp.Cdc) + + keyStake := sdk.NewKVStoreKey("stake") + keyGov := sdk.NewKVStoreKey("gov") + + ck := bank.NewKeeper(mapp.AccountMapper) + sk := stake.NewKeeper(mapp.Cdc, keyStake, ck, mapp.RegisterCodespace(stake.DefaultCodespace)) + keeper := NewKeeper(mapp.Cdc, keyGov, ck, sk, DefaultCodespace) + mapp.Router().AddRoute("gov", NewHandler(keeper)) + + require.NoError(t, mapp.CompleteSetup([]*sdk.KVStoreKey{keyStake, keyGov})) + + mapp.SetEndBlocker(getEndBlocker(keeper)) + mapp.SetInitChainer(getInitChainer(mapp, keeper, sk)) + + genAccs, addrs, pubKeys, privKeys := mock.CreateGenAccounts(numGenAccs, sdk.Coins{sdk.NewCoin("steak", 42)}) + mock.SetGenesis(mapp, genAccs) + + return mapp, keeper, sk, addrs, pubKeys, privKeys +} + +// gov and stake endblocker +func getEndBlocker(keeper Keeper) sdk.EndBlocker { + return func(ctx sdk.Context, req abci.RequestEndBlock) abci.ResponseEndBlock { + tags, _ := EndBlocker(ctx, keeper) + return abci.ResponseEndBlock{ + Tags: tags, + } + } +} + +// gov and stake initchainer +func getInitChainer(mapp *mock.App, keeper Keeper, stakeKeeper stake.Keeper) sdk.InitChainer { + return func(ctx sdk.Context, req abci.RequestInitChain) abci.ResponseInitChain { + mapp.InitChainer(ctx, req) + + stakeGenesis := stake.DefaultGenesisState() + stakeGenesis.Pool.LooseTokens = sdk.NewRat(100000) + + err := stake.InitGenesis(ctx, stakeKeeper, stakeGenesis) + if err != nil { + panic(err) + } + InitGenesis(ctx, keeper, DefaultGenesisState()) + return abci.ResponseInitChain{} + } +} + +// Sorts Addresses +func SortAddresses(addrs []sdk.AccAddress) { + var byteAddrs [][]byte + for _, addr := range addrs { + byteAddrs = append(byteAddrs, addr.Bytes()) + } + SortByteArrays(byteAddrs) + for i, byteAddr := range byteAddrs { + addrs[i] = byteAddr + } +} + +// implement `Interface` in sort package. +type sortByteArrays [][]byte + +func (b sortByteArrays) Len() int { + return len(b) +} + +func (b sortByteArrays) Less(i, j int) bool { + // bytes package already implements Comparable for []byte. + switch bytes.Compare(b[i], b[j]) { + case -1: + return true + case 0, 1: + return false + default: + log.Panic("not fail-able with `bytes.Comparable` bounded [-1, 1].") + return false + } +} + +func (b sortByteArrays) Swap(i, j int) { + b[j], b[i] = b[i], b[j] +} + +// Public +func SortByteArrays(src [][]byte) [][]byte { + sorted := sortByteArrays(src) + sort.Sort(sorted) + return sorted +} diff --git a/modules/gov/wire.go b/modules/gov/wire.go new file mode 100644 index 000000000..405ee464e --- /dev/null +++ b/modules/gov/wire.go @@ -0,0 +1,18 @@ +package gov + +import ( + "github.com/cosmos/cosmos-sdk/wire" +) + +// Register concrete types on wire codec +func RegisterWire(cdc *wire.Codec) { + + cdc.RegisterConcrete(MsgSubmitProposal{}, "cosmos-sdk/MsgSubmitProposal", nil) + cdc.RegisterConcrete(MsgDeposit{}, "cosmos-sdk/MsgDeposit", nil) + cdc.RegisterConcrete(MsgVote{}, "cosmos-sdk/MsgVote", nil) + + cdc.RegisterInterface((*Proposal)(nil), nil) + cdc.RegisterConcrete(&TextProposal{}, "gov/TextProposal", nil) +} + +var msgCdc = wire.NewCodec()