2020-03-11 19:52:25 +00:00
package keeper
import (
2020-03-13 23:13:42 +00:00
"fmt"
2023-04-05 23:21:59 +00:00
errorsmod "cosmossdk.io/errors"
2020-03-11 19:52:25 +00:00
sdk "github.com/cosmos/cosmos-sdk/types"
2020-04-27 18:19:05 +00:00
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
2020-03-11 19:52:25 +00:00
"github.com/kava-labs/kava/x/committee/types"
)
2020-03-26 20:17:49 +00:00
// SubmitProposal adds a proposal to a committee so that it can be voted on.
2020-04-27 18:19:05 +00:00
func ( k Keeper ) SubmitProposal ( ctx sdk . Context , proposer sdk . AccAddress , committeeID uint64 , pubProposal types . PubProposal ) ( uint64 , error ) {
2020-03-11 19:52:25 +00:00
// Limit proposals to only be submitted by committee members
com , found := k . GetCommittee ( ctx , committeeID )
if ! found {
2023-04-05 23:21:59 +00:00
return 0 , errorsmod . Wrapf ( types . ErrUnknownCommittee , "%d" , committeeID )
2020-03-11 19:52:25 +00:00
}
if ! com . HasMember ( proposer ) {
2023-04-05 23:21:59 +00:00
return 0 , errorsmod . Wrap ( sdkerrors . ErrUnauthorized , "proposer not member of committee" )
2020-03-11 19:52:25 +00:00
}
// Check committee has permissions to enact proposal.
2022-01-08 00:39:27 +00:00
if ! com . HasPermissionsFor ( ctx , k . cdc , k . paramKeeper , pubProposal ) {
2023-04-05 23:21:59 +00:00
return 0 , errorsmod . Wrap ( sdkerrors . ErrUnauthorized , "committee does not have permissions to enact proposal" )
2020-03-11 19:52:25 +00:00
}
// Check proposal is valid
if err := k . ValidatePubProposal ( ctx , pubProposal ) ; err != nil {
return 0 , err
}
// Get a new ID and store the proposal
2021-06-07 16:08:03 +00:00
deadline := ctx . BlockTime ( ) . Add ( com . GetProposalDuration ( ) )
2020-03-13 23:13:42 +00:00
proposalID , err := k . StoreNewProposal ( ctx , pubProposal , committeeID , deadline )
if err != nil {
return 0 , err
}
ctx . EventManager ( ) . EmitEvent (
sdk . NewEvent (
2020-03-27 20:28:51 +00:00
types . EventTypeProposalSubmit ,
2021-06-07 16:08:03 +00:00
sdk . NewAttribute ( types . AttributeKeyCommitteeID , fmt . Sprintf ( "%d" , com . GetID ( ) ) ) ,
2020-03-13 23:13:42 +00:00
sdk . NewAttribute ( types . AttributeKeyProposalID , fmt . Sprintf ( "%d" , proposalID ) ) ,
2021-06-07 16:08:03 +00:00
sdk . NewAttribute ( types . AttributeKeyDeadline , deadline . String ( ) ) ,
2020-03-13 23:13:42 +00:00
) ,
)
return proposalID , nil
2020-03-11 19:52:25 +00:00
}
2020-03-26 20:17:49 +00:00
// AddVote submits a vote on a proposal.
2021-06-07 16:08:03 +00:00
func ( k Keeper ) AddVote ( ctx sdk . Context , proposalID uint64 , voter sdk . AccAddress , voteType types . VoteType ) error {
2020-03-11 19:52:25 +00:00
// Validate
pr , found := k . GetProposal ( ctx , proposalID )
if ! found {
2023-04-05 23:21:59 +00:00
return errorsmod . Wrapf ( types . ErrUnknownProposal , "%d" , proposalID )
2020-03-11 19:52:25 +00:00
}
2020-03-12 17:05:40 +00:00
if pr . HasExpiredBy ( ctx . BlockTime ( ) ) {
2023-04-05 23:21:59 +00:00
return errorsmod . Wrapf ( types . ErrProposalExpired , "%s ≥ %s" , ctx . BlockTime ( ) , pr . Deadline )
2020-03-12 17:05:40 +00:00
}
2020-03-11 19:52:25 +00:00
com , found := k . GetCommittee ( ctx , pr . CommitteeID )
if ! found {
2023-04-05 23:21:59 +00:00
return errorsmod . Wrapf ( types . ErrUnknownCommittee , "%d" , pr . CommitteeID )
2020-03-11 19:52:25 +00:00
}
2021-06-07 16:08:03 +00:00
2022-01-08 00:39:27 +00:00
if _ , ok := com . ( * types . MemberCommittee ) ; ok {
2021-06-07 16:08:03 +00:00
if ! com . HasMember ( voter ) {
2023-04-05 23:21:59 +00:00
return errorsmod . Wrap ( sdkerrors . ErrUnauthorized , "voter must be a member of committee" )
2021-06-07 16:08:03 +00:00
}
2022-01-08 00:39:27 +00:00
if voteType != types . VOTE_TYPE_YES {
2023-04-05 23:21:59 +00:00
return errorsmod . Wrap ( types . ErrInvalidVoteType , "member committees only accept yes votes" )
2021-06-07 16:08:03 +00:00
}
2020-03-11 19:52:25 +00:00
}
// Store vote, overwriting any prior vote
2021-06-07 16:08:03 +00:00
k . SetVote ( ctx , types . NewVote ( proposalID , voter , voteType ) )
2020-03-11 19:52:25 +00:00
2020-03-27 20:28:51 +00:00
ctx . EventManager ( ) . EmitEvent (
sdk . NewEvent (
types . EventTypeProposalVote ,
2021-06-07 16:08:03 +00:00
sdk . NewAttribute ( types . AttributeKeyCommitteeID , fmt . Sprintf ( "%d" , com . GetID ( ) ) ) ,
2020-03-27 20:28:51 +00:00
sdk . NewAttribute ( types . AttributeKeyProposalID , fmt . Sprintf ( "%d" , pr . ID ) ) ,
2020-05-07 20:58:39 +00:00
sdk . NewAttribute ( types . AttributeKeyVoter , voter . String ( ) ) ,
2021-06-07 16:08:03 +00:00
sdk . NewAttribute ( types . AttributeKeyVote , fmt . Sprintf ( "%d" , voteType ) ) ,
2020-03-27 20:28:51 +00:00
) ,
)
2020-03-11 19:52:25 +00:00
return nil
}
2021-06-07 16:08:03 +00:00
// ValidatePubProposal checks if a pubproposal is valid.
func ( k Keeper ) ValidatePubProposal ( ctx sdk . Context , pubProposal types . PubProposal ) ( returnErr error ) {
if pubProposal == nil {
2023-04-05 23:21:59 +00:00
return errorsmod . Wrap ( types . ErrInvalidPubProposal , "pub proposal cannot be nil" )
2020-03-11 19:52:25 +00:00
}
2021-06-07 16:08:03 +00:00
if err := pubProposal . ValidateBasic ( ) ; err != nil {
return err
}
if ! k . router . HasRoute ( pubProposal . ProposalRoute ( ) ) {
2023-04-05 23:21:59 +00:00
return errorsmod . Wrapf ( types . ErrNoProposalHandlerExists , "%T" , pubProposal )
2021-06-07 16:08:03 +00:00
}
// 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 {
2023-04-05 23:21:59 +00:00
returnErr = errorsmod . Wrapf ( types . ErrInvalidPubProposal , "proposal handler panicked: %s" , r )
2021-06-07 16:08:03 +00:00
}
} ( )
if err := handler ( cacheCtx , pubProposal ) ; err != nil {
return err
2020-03-11 19:52:25 +00:00
}
2021-06-07 16:08:03 +00:00
return nil
}
func ( k Keeper ) ProcessProposals ( ctx sdk . Context ) {
k . IterateProposals ( ctx , func ( proposal types . Proposal ) bool {
committee , found := k . GetCommittee ( ctx , proposal . CommitteeID )
if ! found {
k . CloseProposal ( ctx , proposal , types . Failed )
return false
}
if ! proposal . HasExpiredBy ( ctx . BlockTime ( ) ) {
2022-01-08 00:39:27 +00:00
if committee . GetTallyOption ( ) == types . TALLY_OPTION_FIRST_PAST_THE_POST {
2021-06-07 16:08:03 +00:00
passed := k . GetProposalResult ( ctx , proposal . ID , committee )
if passed {
outcome := k . attemptEnactProposal ( ctx , proposal )
k . CloseProposal ( ctx , proposal , outcome )
}
}
} else {
passed := k . GetProposalResult ( ctx , proposal . ID , committee )
outcome := types . Failed
if passed {
outcome = k . attemptEnactProposal ( ctx , proposal )
}
k . CloseProposal ( ctx , proposal , outcome )
}
return false
} )
}
2020-03-11 19:52:25 +00:00
2021-06-07 16:08:03 +00:00
func ( k Keeper ) GetProposalResult ( ctx sdk . Context , proposalID uint64 , committee types . Committee ) bool {
switch com := committee . ( type ) {
2022-01-08 00:39:27 +00:00
case * types . MemberCommittee :
2021-06-07 16:08:03 +00:00
return k . GetMemberCommitteeProposalResult ( ctx , proposalID , com )
2022-01-08 00:39:27 +00:00
case * types . TokenCommittee :
2021-06-07 16:08:03 +00:00
return k . GetTokenCommitteeProposalResult ( ctx , proposalID , com )
default : // Should never hit default case
return false
}
}
2020-03-26 20:17:49 +00:00
2021-06-07 16:08:03 +00:00
// GetMemberCommitteeProposalResult gets the result of a member committee proposal
func ( k Keeper ) GetMemberCommitteeProposalResult ( ctx sdk . Context , proposalID uint64 , committee types . Committee ) bool {
currVotes := k . TallyMemberCommitteeVotes ( ctx , proposalID )
possibleVotes := sdk . NewDec ( int64 ( len ( committee . GetMembers ( ) ) ) )
return currVotes . GTE ( committee . GetVoteThreshold ( ) . Mul ( possibleVotes ) ) // vote threshold requirements
}
2020-03-26 20:17:49 +00:00
2021-06-07 16:08:03 +00:00
// TallyMemberCommitteeVotes returns the polling status of a member committee vote
func ( k Keeper ) TallyMemberCommitteeVotes ( ctx sdk . Context , proposalID uint64 ) ( totalVotes sdk . Dec ) {
votes := k . GetVotesByProposal ( ctx , proposalID )
return sdk . NewDec ( int64 ( len ( votes ) ) )
2020-03-26 20:17:49 +00:00
}
2021-06-07 16:08:03 +00:00
// GetTokenCommitteeProposalResult gets the result of a token committee proposal
2022-01-08 00:39:27 +00:00
func ( k Keeper ) GetTokenCommitteeProposalResult ( ctx sdk . Context , proposalID uint64 , committee * types . TokenCommittee ) bool {
2021-06-07 16:08:03 +00:00
yesVotes , noVotes , totalVotes , possibleVotes := k . TallyTokenCommitteeVotes ( ctx , proposalID , committee . TallyDenom )
if totalVotes . GTE ( committee . Quorum . Mul ( possibleVotes ) ) { // quorum requirement
nonAbstainVotes := yesVotes . Add ( noVotes )
if yesVotes . GTE ( nonAbstainVotes . Mul ( committee . VoteThreshold ) ) { // vote threshold requirements
return true
}
}
return false
}
2020-03-26 20:17:49 +00:00
2021-06-07 16:08:03 +00:00
// TallyMemberCommitteeVotes returns the polling status of a token committee vote. Returns yes votes,
// total current votes, total possible votes (equal to token supply), vote threshold (yes vote ratio
// required for proposal to pass), and quorum (votes tallied at this percentage).
func ( k Keeper ) TallyTokenCommitteeVotes ( ctx sdk . Context , proposalID uint64 ,
2022-05-09 18:37:36 +00:00
tallyDenom string ,
) ( yesVotes , noVotes , totalVotes , possibleVotes sdk . Dec ) {
2020-04-27 14:04:47 +00:00
votes := k . GetVotesByProposal ( ctx , proposalID )
2020-03-26 20:17:49 +00:00
2021-06-07 16:08:03 +00:00
yesVotes = sdk . ZeroDec ( )
noVotes = sdk . ZeroDec ( )
totalVotes = sdk . ZeroDec ( )
for _ , vote := range votes {
// 1 token = 1 vote
acc := k . accountKeeper . GetAccount ( ctx , vote . Voter )
2022-01-08 00:39:27 +00:00
accNumCoins := k . bankKeeper . GetBalance ( ctx , acc . GetAddress ( ) , tallyDenom ) . Amount
2021-06-07 16:08:03 +00:00
// Add votes to counters
2023-04-04 00:08:45 +00:00
totalVotes = totalVotes . Add ( sdk . NewDecFromInt ( accNumCoins ) )
2022-01-08 00:39:27 +00:00
if vote . VoteType == types . VOTE_TYPE_YES {
2023-04-04 00:08:45 +00:00
yesVotes = yesVotes . Add ( sdk . NewDecFromInt ( accNumCoins ) )
2022-01-08 00:39:27 +00:00
} else if vote . VoteType == types . VOTE_TYPE_NO {
2023-04-04 00:08:45 +00:00
noVotes = noVotes . Add ( sdk . NewDecFromInt ( accNumCoins ) )
2021-06-07 16:08:03 +00:00
}
}
2022-01-08 00:39:27 +00:00
possibleVotesInt := k . bankKeeper . GetSupply ( ctx , tallyDenom ) . Amount
2023-04-04 00:08:45 +00:00
return yesVotes , noVotes , totalVotes , sdk . NewDecFromInt ( possibleVotesInt )
2020-03-11 19:52:25 +00:00
}
2021-06-07 16:08:03 +00:00
func ( k Keeper ) attemptEnactProposal ( ctx sdk . Context , proposal types . Proposal ) types . ProposalOutcome {
err := k . enactProposal ( ctx , proposal )
if err != nil {
return types . Invalid
}
return types . Passed
}
// enactProposal makes the changes proposed in a proposal.
func ( k Keeper ) enactProposal ( ctx sdk . Context , proposal types . Proposal ) error {
2020-05-15 19:25:49 +00:00
// Check committee still has permissions for the proposal
// Since the proposal was submitted params could have changed, invalidating the permission of the committee.
com , found := k . GetCommittee ( ctx , proposal . CommitteeID )
2020-03-26 20:17:49 +00:00
if ! found {
2023-04-05 23:21:59 +00:00
return errorsmod . Wrapf ( types . ErrUnknownCommittee , "%d" , proposal . CommitteeID )
2020-05-15 19:25:49 +00:00
}
2022-01-08 00:39:27 +00:00
if ! com . HasPermissionsFor ( ctx , k . cdc , k . paramKeeper , proposal . GetContent ( ) ) {
2023-04-05 23:21:59 +00:00
return errorsmod . Wrap ( sdkerrors . ErrUnauthorized , "committee does not have permissions to enact proposal" )
2020-03-26 20:17:49 +00:00
}
2022-01-08 00:39:27 +00:00
if err := k . ValidatePubProposal ( ctx , proposal . GetContent ( ) ) ; err != nil {
2020-03-26 20:17:49 +00:00
return err
}
2020-05-15 19:25:49 +00:00
// enact the proposal
2022-01-08 00:39:27 +00:00
handler := k . router . GetRoute ( proposal . GetContent ( ) . ProposalRoute ( ) )
if err := handler ( ctx , proposal . GetContent ( ) ) ; err != nil {
2020-04-25 16:39:59 +00:00
// the handler should not error as it was checked in ValidatePubProposal
panic ( fmt . Sprintf ( "unexpected handler error: %s" , err ) )
}
2020-03-26 20:17:49 +00:00
return nil
}
2022-01-08 00:39:27 +00:00
// GetProposalTallyResponse returns the tally results of a proposal.
func ( k Keeper ) GetProposalTallyResponse ( ctx sdk . Context , proposalID uint64 ) ( * types . QueryTallyResponse , bool ) {
proposal , found := k . GetProposal ( ctx , proposalID )
if ! found {
return nil , found
}
2021-06-22 14:49:46 +00:00
committee , found := k . GetCommittee ( ctx , proposal . CommitteeID )
2022-01-08 00:39:27 +00:00
if ! found {
return nil , found
}
var proposalTally types . QueryTallyResponse
switch com := committee . ( type ) {
case * types . MemberCommittee :
currVotes := k . TallyMemberCommitteeVotes ( ctx , proposal . ID )
possibleVotes := sdk . NewDec ( int64 ( len ( com . Members ) ) )
proposalTally = types . QueryTallyResponse {
ProposalID : proposal . ID ,
YesVotes : currVotes ,
NoVotes : sdk . ZeroDec ( ) ,
CurrentVotes : currVotes ,
PossibleVotes : possibleVotes ,
VoteThreshold : com . VoteThreshold ,
Quorum : sdk . ZeroDec ( ) ,
}
case * types . TokenCommittee :
yesVotes , noVotes , currVotes , possibleVotes := k . TallyTokenCommitteeVotes ( ctx , proposal . ID , com . TallyDenom )
proposalTally = types . QueryTallyResponse {
ProposalID : proposal . ID ,
YesVotes : yesVotes ,
NoVotes : noVotes ,
CurrentVotes : currVotes ,
PossibleVotes : possibleVotes ,
VoteThreshold : com . VoteThreshold ,
Quorum : com . Quorum ,
2021-06-22 14:49:46 +00:00
}
}
2022-01-08 00:39:27 +00:00
return & proposalTally , true
}
2021-06-22 14:49:46 +00:00
2022-01-08 00:39:27 +00:00
// CloseProposal deletes proposals and their votes, emitting an event denoting the final status of the proposal
func ( k Keeper ) CloseProposal ( ctx sdk . Context , proposal types . Proposal , outcome types . ProposalOutcome ) {
tally , _ := k . GetProposalTallyResponse ( ctx , proposal . ID )
2021-06-07 16:08:03 +00:00
k . DeleteProposalAndVotes ( ctx , proposal . ID )
2020-04-25 16:39:59 +00:00
2022-01-08 00:39:27 +00:00
bz , err := k . cdc . MarshalJSON ( tally )
2021-06-22 14:49:46 +00:00
if err != nil {
2022-01-08 00:39:27 +00:00
fmt . Println ( "error marshaling proposal tally to bytes:" , tally . String ( ) )
2021-06-22 14:49:46 +00:00
}
2021-06-07 16:08:03 +00:00
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 ) ) ,
2022-01-08 00:39:27 +00:00
sdk . NewAttribute ( types . AttributeKeyProposalTally , string ( bz ) ) ,
2021-06-07 16:08:03 +00:00
sdk . NewAttribute ( types . AttributeKeyProposalOutcome , outcome . String ( ) ) ,
) ,
)
2020-03-11 19:52:25 +00:00
}