package gov import ( "time" "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/params" "github.com/tendermint/tendermint/crypto" "github.com/tendermint/tendermint/libs/log" ) var ( // TODO: Find another way to implement this without using accounts, or find a cleaner way to implement it using accounts. DepositedCoinsAccAddr = sdk.AccAddress(crypto.AddressHash([]byte("govDepositedCoins"))) BurnedDepositCoinsAccAddr = sdk.AccAddress(crypto.AddressHash([]byte("govBurnedDepositCoins"))) ) // Governance Keeper type Keeper struct { // The reference to the Param Keeper to get and set Global Params paramsKeeper params.Keeper // The reference to the Paramstore to get and set gov specific params paramSpace params.Subspace // The reference to the CoinKeeper to modify balances ck BankKeeper // 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 codec codec for binary encoding/decoding. cdc *codec.Codec // Reserved codespace codespace sdk.CodespaceType // Proposal router router Router } // NewKeeper returns a governance keeper. It handles: // - submitting governance proposals // - depositing funds into proposals, and activating upon sufficient funds being deposited // - users voting on proposals, with weight proportional to stake in the system // - and tallying the result of the vote. func NewKeeper( cdc *codec.Codec, key sdk.StoreKey, paramsKeeper params.Keeper, paramSpace params.Subspace, ck BankKeeper, ds sdk.DelegationSet, codespace sdk.CodespaceType, rtr Router, ) Keeper { // It is vital to seal the governance proposal router here as to not allow // further handlers to be registered after the keeper is created since this // could create invalid or non-deterministic behavior. rtr.Seal() return Keeper{ storeKey: key, paramsKeeper: paramsKeeper, paramSpace: paramSpace.WithKeyTable(ParamKeyTable()), ck: ck, ds: ds, vs: ds.GetValidatorSet(), cdc: cdc, codespace: codespace, router: rtr, } } // Logger returns a module-specific logger. func (keeper Keeper) Logger(ctx sdk.Context) log.Logger { return ctx.Logger().With("module", "x/gov") } // Proposals func (keeper Keeper) SubmitProposal(ctx sdk.Context, content Content) (Proposal, sdk.Error) { if !keeper.router.HasRoute(content.ProposalRoute()) { return Proposal{}, ErrNoProposalHandlerExists(keeper.codespace, content) } // Execute the proposal content in a cache-wrapped context to validate the // actual parameter changes before the proposal proceeds through the // governance process. State is not persisted. cacheCtx, _ := ctx.CacheContext() handler := keeper.router.GetRoute(content.ProposalRoute()) if err := handler(cacheCtx, content); err != nil { return Proposal{}, ErrInvalidProposalContent(keeper.codespace, err.Result().Log) } proposalID, err := keeper.getNewProposalID(ctx) if err != nil { return Proposal{}, err } submitTime := ctx.BlockHeader().Time depositPeriod := keeper.GetDepositParams(ctx).MaxDepositPeriod proposal := NewProposal(content, proposalID, submitTime, submitTime.Add(depositPeriod)) keeper.SetProposal(ctx, proposal) keeper.InsertInactiveProposalQueue(ctx, proposal.DepositEndTime, proposalID) return proposal, nil } // Get Proposal from store by ProposalID func (keeper Keeper) GetProposal(ctx sdk.Context, proposalID uint64) (proposal Proposal, ok bool) { store := ctx.KVStore(keeper.storeKey) bz := store.Get(KeyProposal(proposalID)) if bz == nil { return } keeper.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &proposal) return proposal, true } // Implements sdk.AccountKeeper. func (keeper Keeper) SetProposal(ctx sdk.Context, proposal Proposal) { store := ctx.KVStore(keeper.storeKey) bz := keeper.cdc.MustMarshalBinaryLengthPrefixed(proposal) store.Set(KeyProposal(proposal.ProposalID), bz) } // Implements sdk.AccountKeeper. func (keeper Keeper) DeleteProposal(ctx sdk.Context, proposalID uint64) { store := ctx.KVStore(keeper.storeKey) proposal, ok := keeper.GetProposal(ctx, proposalID) if !ok { panic("DeleteProposal cannot fail to GetProposal") } keeper.RemoveFromInactiveProposalQueue(ctx, proposal.DepositEndTime, proposalID) keeper.RemoveFromActiveProposalQueue(ctx, proposal.VotingEndTime, proposalID) store.Delete(KeyProposal(proposalID)) } // Get Proposal from store by ProposalID // voterAddr will filter proposals by whether or not that address has voted on them // depositorAddr will filter proposals by whether or not that address has deposited to them // status will filter proposals by status // numLatest will fetch a specified number of the most recent proposals, or 0 for all proposals func (keeper Keeper) GetProposalsFiltered(ctx sdk.Context, voterAddr sdk.AccAddress, depositorAddr sdk.AccAddress, status ProposalStatus, numLatest uint64) []Proposal { maxProposalID, err := keeper.peekCurrentProposalID(ctx) if err != nil { return nil } matchingProposals := []Proposal{} if numLatest == 0 { numLatest = maxProposalID } for proposalID := maxProposalID - numLatest; proposalID < maxProposalID; proposalID++ { if voterAddr != nil && len(voterAddr) != 0 { _, found := keeper.GetVote(ctx, proposalID, voterAddr) if !found { continue } } if depositorAddr != nil && len(depositorAddr) != 0 { _, found := keeper.GetDeposit(ctx, proposalID, depositorAddr) if !found { continue } } proposal, ok := keeper.GetProposal(ctx, proposalID) if !ok { continue } if ValidProposalStatus(status) { if proposal.Status != status { continue } } matchingProposals = append(matchingProposals, proposal) } return matchingProposals } // Set the initial proposal ID func (keeper Keeper) setInitialProposalID(ctx sdk.Context, proposalID uint64) sdk.Error { store := ctx.KVStore(keeper.storeKey) bz := store.Get(KeyNextProposalID) if bz != nil { return ErrInvalidGenesis(keeper.codespace, "initial proposal ID already set") } bz = keeper.cdc.MustMarshalBinaryLengthPrefixed(proposalID) store.Set(KeyNextProposalID, bz) return nil } // Get the last used proposal ID func (keeper Keeper) GetLastProposalID(ctx sdk.Context) (proposalID uint64) { proposalID, err := keeper.peekCurrentProposalID(ctx) if err != nil { return 0 } proposalID-- return } // Gets the next available ProposalID and increments it func (keeper Keeper) getNewProposalID(ctx sdk.Context) (proposalID uint64, err sdk.Error) { store := ctx.KVStore(keeper.storeKey) bz := store.Get(KeyNextProposalID) if bz == nil { return 0, ErrInvalidGenesis(keeper.codespace, "initial proposal ID never set") } keeper.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &proposalID) bz = keeper.cdc.MustMarshalBinaryLengthPrefixed(proposalID + 1) store.Set(KeyNextProposalID, bz) return proposalID, nil } // Peeks the next available ProposalID without incrementing it func (keeper Keeper) peekCurrentProposalID(ctx sdk.Context) (proposalID uint64, err sdk.Error) { store := ctx.KVStore(keeper.storeKey) bz := store.Get(KeyNextProposalID) if bz == nil { return 0, ErrInvalidGenesis(keeper.codespace, "initial proposal ID never set") } keeper.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &proposalID) return proposalID, nil } func (keeper Keeper) activateVotingPeriod(ctx sdk.Context, proposal Proposal) { proposal.VotingStartTime = ctx.BlockHeader().Time votingPeriod := keeper.GetVotingParams(ctx).VotingPeriod proposal.VotingEndTime = proposal.VotingStartTime.Add(votingPeriod) proposal.Status = StatusVotingPeriod keeper.SetProposal(ctx, proposal) keeper.RemoveFromInactiveProposalQueue(ctx, proposal.DepositEndTime, proposal.ProposalID) keeper.InsertActiveProposalQueue(ctx, proposal.VotingEndTime, proposal.ProposalID) } // Params // Returns the current DepositParams from the global param store func (keeper Keeper) GetDepositParams(ctx sdk.Context) DepositParams { var depositParams DepositParams keeper.paramSpace.Get(ctx, ParamStoreKeyDepositParams, &depositParams) return depositParams } // Returns the current VotingParams from the global param store func (keeper Keeper) GetVotingParams(ctx sdk.Context) VotingParams { var votingParams VotingParams keeper.paramSpace.Get(ctx, ParamStoreKeyVotingParams, &votingParams) return votingParams } // Returns the current TallyParam from the global param store func (keeper Keeper) GetTallyParams(ctx sdk.Context) TallyParams { var tallyParams TallyParams keeper.paramSpace.Get(ctx, ParamStoreKeyTallyParams, &tallyParams) return tallyParams } func (keeper Keeper) setDepositParams(ctx sdk.Context, depositParams DepositParams) { keeper.paramSpace.Set(ctx, ParamStoreKeyDepositParams, &depositParams) } func (keeper Keeper) setVotingParams(ctx sdk.Context, votingParams VotingParams) { keeper.paramSpace.Set(ctx, ParamStoreKeyVotingParams, &votingParams) } func (keeper Keeper) setTallyParams(ctx sdk.Context, tallyParams TallyParams) { keeper.paramSpace.Set(ctx, ParamStoreKeyTallyParams, &tallyParams) } // Votes // Adds a vote on a specific proposal func (keeper Keeper) AddVote(ctx sdk.Context, proposalID uint64, voterAddr sdk.AccAddress, option VoteOption) sdk.Error { proposal, ok := keeper.GetProposal(ctx, proposalID) if !ok { return ErrUnknownProposal(keeper.codespace, proposalID) } if proposal.Status != 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 uint64, 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.MustUnmarshalBinaryLengthPrefixed(bz, &vote) return vote, true } func (keeper Keeper) setVote(ctx sdk.Context, proposalID uint64, voterAddr sdk.AccAddress, vote Vote) { store := ctx.KVStore(keeper.storeKey) bz := keeper.cdc.MustMarshalBinaryLengthPrefixed(vote) store.Set(KeyVote(proposalID, voterAddr), bz) } // Gets all the votes on a specific proposal func (keeper Keeper) GetVotes(ctx sdk.Context, proposalID uint64) sdk.Iterator { store := ctx.KVStore(keeper.storeKey) return sdk.KVStorePrefixIterator(store, KeyVotesSubspace(proposalID)) } func (keeper Keeper) deleteVote(ctx sdk.Context, proposalID uint64, voterAddr sdk.AccAddress) { store := ctx.KVStore(keeper.storeKey) store.Delete(KeyVote(proposalID, voterAddr)) } // Deposits // Gets the deposit of a specific depositor on a specific proposal func (keeper Keeper) GetDeposit(ctx sdk.Context, proposalID uint64, depositorAddr sdk.AccAddress) (Deposit, bool) { store := ctx.KVStore(keeper.storeKey) bz := store.Get(KeyDeposit(proposalID, depositorAddr)) if bz == nil { return Deposit{}, false } var deposit Deposit keeper.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &deposit) return deposit, true } func (keeper Keeper) setDeposit(ctx sdk.Context, proposalID uint64, depositorAddr sdk.AccAddress, deposit Deposit) { store := ctx.KVStore(keeper.storeKey) bz := keeper.cdc.MustMarshalBinaryLengthPrefixed(deposit) store.Set(KeyDeposit(proposalID, depositorAddr), bz) } // Adds or updates a deposit of a specific depositor on a specific proposal // Activates voting period when appropriate func (keeper Keeper) AddDeposit(ctx sdk.Context, proposalID uint64, depositorAddr sdk.AccAddress, depositAmount sdk.Coins) (sdk.Error, bool) { // Checks to see if proposal exists proposal, ok := keeper.GetProposal(ctx, proposalID) if !ok { return ErrUnknownProposal(keeper.codespace, proposalID), false } // Check if proposal is still depositable if (proposal.Status != StatusDepositPeriod) && (proposal.Status != StatusVotingPeriod) { return ErrAlreadyFinishedProposal(keeper.codespace, proposalID), false } // Send coins from depositor's account to DepositedCoinsAccAddr account // TODO: Don't use an account for this purpose; it's clumsy and prone to misuse. err := keeper.ck.SendCoins(ctx, depositorAddr, DepositedCoinsAccAddr, depositAmount) if err != nil { return err, false } // Update proposal proposal.TotalDeposit = proposal.TotalDeposit.Add(depositAmount) keeper.SetProposal(ctx, proposal) // Check if deposit has provided sufficient total funds to transition the proposal into the voting period activatedVotingPeriod := false if proposal.Status == StatusDepositPeriod && proposal.TotalDeposit.IsAllGTE(keeper.GetDepositParams(ctx).MinDeposit) { keeper.activateVotingPeriod(ctx, proposal) activatedVotingPeriod = true } // Add or update deposit object currDeposit, found := keeper.GetDeposit(ctx, proposalID, depositorAddr) if !found { newDeposit := Deposit{depositorAddr, proposalID, depositAmount} keeper.setDeposit(ctx, proposalID, depositorAddr, newDeposit) } else { currDeposit.Amount = currDeposit.Amount.Add(depositAmount) keeper.setDeposit(ctx, proposalID, depositorAddr, currDeposit) } return nil, activatedVotingPeriod } // Gets all the deposits on a specific proposal as an sdk.Iterator func (keeper Keeper) GetDeposits(ctx sdk.Context, proposalID uint64) sdk.Iterator { store := ctx.KVStore(keeper.storeKey) return sdk.KVStorePrefixIterator(store, KeyDepositsSubspace(proposalID)) } // Refunds and deletes all the deposits on a specific proposal func (keeper Keeper) RefundDeposits(ctx sdk.Context, proposalID uint64) { store := ctx.KVStore(keeper.storeKey) depositsIterator := keeper.GetDeposits(ctx, proposalID) defer depositsIterator.Close() for ; depositsIterator.Valid(); depositsIterator.Next() { deposit := &Deposit{} keeper.cdc.MustUnmarshalBinaryLengthPrefixed(depositsIterator.Value(), deposit) err := keeper.ck.SendCoins(ctx, DepositedCoinsAccAddr, deposit.Depositor, deposit.Amount) if err != nil { panic("should not happen") } store.Delete(depositsIterator.Key()) } } // Deletes all the deposits on a specific proposal without refunding them func (keeper Keeper) DeleteDeposits(ctx sdk.Context, proposalID uint64) { store := ctx.KVStore(keeper.storeKey) depositsIterator := keeper.GetDeposits(ctx, proposalID) defer depositsIterator.Close() for ; depositsIterator.Valid(); depositsIterator.Next() { deposit := &Deposit{} keeper.cdc.MustUnmarshalBinaryLengthPrefixed(depositsIterator.Value(), deposit) // TODO: Find a way to do this without using accounts. err := keeper.ck.SendCoins(ctx, DepositedCoinsAccAddr, BurnedDepositCoinsAccAddr, deposit.Amount) if err != nil { panic("should not happen") } store.Delete(depositsIterator.Key()) } } // ProposalQueues // Returns an iterator for all the proposals in the Active Queue that expire by endTime func (keeper Keeper) ActiveProposalQueueIterator(ctx sdk.Context, endTime time.Time) sdk.Iterator { store := ctx.KVStore(keeper.storeKey) return store.Iterator(PrefixActiveProposalQueue, sdk.PrefixEndBytes(PrefixActiveProposalQueueTime(endTime))) } // Inserts a ProposalID into the active proposal queue at endTime func (keeper Keeper) InsertActiveProposalQueue(ctx sdk.Context, endTime time.Time, proposalID uint64) { store := ctx.KVStore(keeper.storeKey) bz := keeper.cdc.MustMarshalBinaryLengthPrefixed(proposalID) store.Set(KeyActiveProposalQueueProposal(endTime, proposalID), bz) } // removes a proposalID from the Active Proposal Queue func (keeper Keeper) RemoveFromActiveProposalQueue(ctx sdk.Context, endTime time.Time, proposalID uint64) { store := ctx.KVStore(keeper.storeKey) store.Delete(KeyActiveProposalQueueProposal(endTime, proposalID)) } // Returns an iterator for all the proposals in the Inactive Queue that expire by endTime func (keeper Keeper) InactiveProposalQueueIterator(ctx sdk.Context, endTime time.Time) sdk.Iterator { store := ctx.KVStore(keeper.storeKey) return store.Iterator(PrefixInactiveProposalQueue, sdk.PrefixEndBytes(PrefixInactiveProposalQueueTime(endTime))) } // Inserts a ProposalID into the inactive proposal queue at endTime func (keeper Keeper) InsertInactiveProposalQueue(ctx sdk.Context, endTime time.Time, proposalID uint64) { store := ctx.KVStore(keeper.storeKey) bz := keeper.cdc.MustMarshalBinaryLengthPrefixed(proposalID) store.Set(KeyInactiveProposalQueueProposal(endTime, proposalID), bz) } // removes a proposalID from the Inactive Proposal Queue func (keeper Keeper) RemoveFromInactiveProposalQueue(ctx sdk.Context, endTime time.Time, proposalID uint64) { store := ctx.KVStore(keeper.storeKey) store.Delete(KeyInactiveProposalQueueProposal(endTime, proposalID)) }