Token holder governance (#917)

* Committee types (#899)

* committee types

* refactor to committee interface

* include tokencommitee stringer method

* add members to BaseCommittee

* address revisions

* update querier

* update querier

* fix compilation errors, tests, etc.

* Update MsgVote with vote type (#900)

* add vote to msg

* update querier/rest

* update example cli vote msg

* remove incorrect comments

* address revisions

* update handler, stub keeper method

* add vote type to vote struct

* Committee module keeper logic for token holder governance (#902)

* fix keeper/test compilation errors

* fix keeper/test compilation errors pt 2

* add setters to committee interface

* fix sims compilation errors

* fix incentive tests compilation errors

* update types, expected keepers

* core keeper logic

* don't allow bond denom

* implement vote tallying

* query proposal polling status

* update module keepers in app.go

* register committee interface

* fix failing incentive test

* commitee types tests

* refactor GetProposalResult by committee types

* update invariants

* implement most proposal keeper tests

* add nulls to custom enums

* remove abstain vote type

* add test for close proposal

* remove outdated TODOs

* update ProcessProposals

* switch on committee type directly

* reintroduce Abstain votes and update vote tallying

* don't allow divide by 0 panics

* delete unused setters on committee interface

* clean up tally methods return values for querier

* update enum validation to catch negative ints

* reintroduce setters for sims compilation

* address revisions

* remove commented out test

* implement ProcessProposals test

* additional revisions

* Committee migrations (#909)

* add committee v14 legacy types

* update migration imports for compile

* addRegisterCodec() to committee v14 legacy types

* migrate committee genesis state from v14 to v15

* set stability committee permissions properly

* fix committee allowed params

* migration test, kava-7 sample data

* add concrete types to committees (#911)

* revisions: migrate + tests

* register msgs on legacy codec

* Prepare Committee module for migrations (#906)

* remove invariants

* edits

* fix abci test

* fix keeper querier tests

* add committee interface registration

* use codec.Codec

* don't allow null vote types

* don't allow null tally option

* minor spelling fixes

* update example cli proposal

* fix cli tally query

* enable vote abstain from cli

* include vote options in cli help text

* call CloseProposal from handler

* custom enum marshaling

* committee: fix failing tests (#921)

* fix failing tests

* fix: spelling

Co-authored-by: rhuairahrighairigh <ruaridh.odonnell@gmail.com>
Co-authored-by: Ruaridh <rhuairahrighairidh@users.noreply.github.com>
Co-authored-by: Kevin Davis <karzak@users.noreply.github.com>
This commit is contained in:
Denali Marsh 2021-06-07 18:08:03 +02:00 committed by GitHub
parent 4d6f6aab3c
commit cae7503f7b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
50 changed files with 4100 additions and 658 deletions

View File

@ -299,6 +299,8 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, appOpts AppOptio
keys[committee.StoreKey], keys[committee.StoreKey],
committeeGovRouter, committeeGovRouter,
app.paramsKeeper, app.paramsKeeper,
app.accountKeeper,
app.supplyKeeper,
) )
app.kavadistKeeper = kavadist.NewKeeper( app.kavadistKeeper = kavadist.NewKeeper(
app.cdc, app.cdc,

1
go.mod
View File

@ -8,6 +8,7 @@ require (
github.com/spf13/cobra v1.0.0 github.com/spf13/cobra v1.0.0
github.com/spf13/viper v1.6.3 github.com/spf13/viper v1.6.3
github.com/stretchr/testify v1.6.1 github.com/stretchr/testify v1.6.1
github.com/tendermint/go-amino v0.15.1
github.com/tendermint/tendermint v0.33.9 github.com/tendermint/tendermint v0.33.9
github.com/tendermint/tm-db v0.5.1 github.com/tendermint/tm-db v0.5.1
gopkg.in/yaml.v2 v2.3.0 gopkg.in/yaml.v2 v2.3.0

View File

@ -19,8 +19,8 @@ import (
"github.com/kava-labs/kava/x/bep3" "github.com/kava-labs/kava/x/bep3"
v0_14cdp "github.com/kava-labs/kava/x/cdp" v0_14cdp "github.com/kava-labs/kava/x/cdp"
v0_11cdp "github.com/kava-labs/kava/x/cdp/legacy/v0_11" v0_11cdp "github.com/kava-labs/kava/x/cdp/legacy/v0_11"
v0_14committee "github.com/kava-labs/kava/x/committee"
v0_11committee "github.com/kava-labs/kava/x/committee/legacy/v0_11" v0_11committee "github.com/kava-labs/kava/x/committee/legacy/v0_11"
v0_14committee "github.com/kava-labs/kava/x/committee/legacy/v0_14"
v0_14hard "github.com/kava-labs/kava/x/hard" v0_14hard "github.com/kava-labs/kava/x/hard"
v0_11hard "github.com/kava-labs/kava/x/hard/legacy/v0_11" v0_11hard "github.com/kava-labs/kava/x/hard/legacy/v0_11"
v0_14incentive "github.com/kava-labs/kava/x/incentive" v0_14incentive "github.com/kava-labs/kava/x/incentive"

View File

@ -20,8 +20,8 @@ import (
"github.com/kava-labs/kava/app" "github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/bep3" "github.com/kava-labs/kava/x/bep3"
v0_11cdp "github.com/kava-labs/kava/x/cdp/legacy/v0_11" v0_11cdp "github.com/kava-labs/kava/x/cdp/legacy/v0_11"
v0_14committee "github.com/kava-labs/kava/x/committee"
v0_11committee "github.com/kava-labs/kava/x/committee/legacy/v0_11" v0_11committee "github.com/kava-labs/kava/x/committee/legacy/v0_11"
v0_14committee "github.com/kava-labs/kava/x/committee/legacy/v0_14"
v0_14hard "github.com/kava-labs/kava/x/hard" v0_14hard "github.com/kava-labs/kava/x/hard"
v0_11hard "github.com/kava-labs/kava/x/hard/legacy/v0_11" v0_11hard "github.com/kava-labs/kava/x/hard/legacy/v0_11"
v0_14incentive "github.com/kava-labs/kava/x/incentive" v0_14incentive "github.com/kava-labs/kava/x/incentive"

193
migrate/v0_15/migrate.go Normal file
View File

@ -0,0 +1,193 @@
package v0_15
import (
"time"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/genutil"
cryptoAmino "github.com/tendermint/tendermint/crypto/encoding/amino"
tmtypes "github.com/tendermint/tendermint/types"
"github.com/kava-labs/kava/app"
v0_14committee "github.com/kava-labs/kava/x/committee/legacy/v0_14"
"github.com/kava-labs/kava/x/committee/types"
v0_15committee "github.com/kava-labs/kava/x/committee/types"
)
var (
// TODO: update GenesisTime for kava-8 launch
GenesisTime = time.Date(2021, 4, 8, 15, 0, 0, 0, time.UTC)
)
// Migrate translates a genesis file from kava v0.14 format to kava v0.15 format
func Migrate(genDoc tmtypes.GenesisDoc) tmtypes.GenesisDoc {
// migrate app state
var appStateMap genutil.AppMap
cdc := codec.New()
cryptoAmino.RegisterAmino(cdc)
tmtypes.RegisterEvidences(cdc)
if err := cdc.UnmarshalJSON(genDoc.AppState, &appStateMap); err != nil {
panic(err)
}
newAppState := MigrateAppState(appStateMap)
v0_15Codec := app.MakeCodec()
marshaledNewAppState, err := v0_15Codec.MarshalJSON(newAppState)
if err != nil {
panic(err)
}
genDoc.AppState = marshaledNewAppState
genDoc.GenesisTime = GenesisTime
genDoc.ChainID = "kava-8"
return genDoc
}
// MigrateAppState migrates application state from v0.14 format to a kava v0.15 format
func MigrateAppState(v0_14AppState genutil.AppMap) genutil.AppMap {
v0_15AppState := v0_14AppState
// Migrate commmittee app state
if v0_14AppState[v0_14committee.ModuleName] != nil {
// Unmarshal v14 committee genesis state and delete it
var committeeGS v0_14committee.GenesisState
cdc := codec.New()
sdk.RegisterCodec(cdc)
v0_14committee.RegisterCodec(cdc)
cdc.MustUnmarshalJSON(v0_14AppState[v0_14committee.ModuleName], &committeeGS)
delete(v0_14AppState, v0_14committee.ModuleName)
// Marshal v15 committee genesis state
cdc = app.MakeCodec()
v0_15AppState[v0_15committee.ModuleName] = cdc.MustMarshalJSON(Committee(committeeGS))
}
return v0_15AppState
}
// Committee migrates from a v0.14 committee genesis state to a v0.15 committee genesis state
func Committee(genesisState v0_14committee.GenesisState) v0_15committee.GenesisState {
committees := []v0_15committee.Committee{}
votes := []v0_15committee.Vote{}
proposals := []v0_15committee.Proposal{}
for _, com := range genesisState.Committees {
if com.ID == 1 {
// Initialize member committee without permissions
stabilityCom := types.NewMemberCommittee(com.ID, com.Description, com.Members,
[]v0_15committee.Permission{}, com.VoteThreshold, com.ProposalDuration,
v0_15committee.FirstPastThePost)
// Build stability committee permissions
var newStabilityCommitteePermissions []v0_15committee.Permission
var newStabilitySubParamPermissions v0_15committee.SubParamChangePermission
for _, perm := range com.Permissions {
subPerm, ok := perm.(v0_14committee.SubParamChangePermission)
if ok {
// update AllowedParams
var newAllowedParams v0_15committee.AllowedParams
for _, ap := range subPerm.AllowedParams {
newAP := v0_15committee.AllowedParam(ap)
newAllowedParams = append(newAllowedParams, newAP)
}
newStabilitySubParamPermissions.AllowedParams = newAllowedParams
// update AllowedCollateralParams
var newCollateralParams v0_15committee.AllowedCollateralParams
collateralTypes := []string{"bnb-a", "busd-a", "busd-b", "btcb-a", "xrpb-a", "ukava-a", "hard-a", "hbtc-a"}
for _, cp := range subPerm.AllowedCollateralParams {
newCP := v0_15committee.NewAllowedCollateralParam(
cp.Type,
cp.Denom,
cp.LiquidationRatio,
cp.DebtLimit,
cp.StabilityFee,
cp.AuctionSize,
cp.LiquidationPenalty,
cp.Prefix,
cp.SpotMarketID,
cp.LiquidationMarketID,
cp.ConversionFactor,
true,
true,
)
newCollateralParams = append(newCollateralParams, newCP)
}
for _, cType := range collateralTypes {
var foundCtype bool
for _, cp := range newCollateralParams {
if cType == cp.Type {
foundCtype = true
}
}
if !foundCtype {
newCP := v0_15committee.NewAllowedCollateralParam(cType, false, false, true, true, true, false, false, false, false, false, true, true)
newCollateralParams = append(newCollateralParams, newCP)
}
}
newStabilitySubParamPermissions.AllowedCollateralParams = newCollateralParams
// update AllowedDebtParam
newDP := v0_15committee.AllowedDebtParam{
Denom: subPerm.AllowedDebtParam.Denom,
ReferenceAsset: subPerm.AllowedDebtParam.ReferenceAsset,
ConversionFactor: subPerm.AllowedDebtParam.ConversionFactor,
DebtFloor: subPerm.AllowedDebtParam.DebtFloor,
}
newStabilitySubParamPermissions.AllowedDebtParam = newDP
// update AllowedAssetParams
var newAssetParams v0_15committee.AllowedAssetParams
for _, ap := range subPerm.AllowedAssetParams {
newAP := v0_15committee.AllowedAssetParam(ap)
newAssetParams = append(newAssetParams, newAP)
}
newStabilitySubParamPermissions.AllowedAssetParams = newAssetParams
// Update Allowed Markets
var newMarketParams v0_15committee.AllowedMarkets
for _, mp := range subPerm.AllowedMarkets {
newMP := v0_15committee.AllowedMarket(mp)
newMarketParams = append(newMarketParams, newMP)
}
newStabilitySubParamPermissions.AllowedMarkets = newMarketParams
// Add hard money market committee permissions
var newMoneyMarketParams v0_15committee.AllowedMoneyMarkets
hardMMDenoms := []string{"bnb", "busd", "btcb", "xrpb", "usdx", "ukava", "hard"}
for _, mmDenom := range hardMMDenoms {
newMoneyMarketParam := v0_15committee.NewAllowedMoneyMarket(mmDenom, true, false, false, true, true, true)
newMoneyMarketParams = append(newMoneyMarketParams, newMoneyMarketParam)
}
newStabilitySubParamPermissions.AllowedMoneyMarkets = newMoneyMarketParams
newStabilityCommitteePermissions = append(newStabilityCommitteePermissions, newStabilitySubParamPermissions)
}
}
newStabilityCommitteePermissions = append(newStabilityCommitteePermissions, v0_15committee.TextPermission{})
// Set stability committee permissions
baseStabilityCom := stabilityCom.SetPermissions(newStabilityCommitteePermissions)
newStabilityCom := v0_15committee.MemberCommittee{BaseCommittee: baseStabilityCom}
committees = append(committees, newStabilityCom)
} else {
safetyCom := types.NewMemberCommittee(com.ID, com.Description, com.Members,
[]v0_15committee.Permission{v0_15committee.SoftwareUpgradePermission{}},
com.VoteThreshold, com.ProposalDuration, v0_15committee.FirstPastThePost)
committees = append(committees, safetyCom)
}
}
for _, v := range genesisState.Votes {
newVote := v0_15committee.NewVote(v.ProposalID, v.Voter, types.Yes)
votes = append(votes, v0_15committee.Vote(newVote))
}
for _, p := range genesisState.Proposals {
newPubProp := v0_15committee.PubProposal(p.PubProposal)
newProp := v0_15committee.NewProposal(newPubProp, p.ID, p.CommitteeID, p.Deadline)
proposals = append(proposals, newProp)
}
return v0_15committee.NewGenesisState(
genesisState.NextProposalID, committees, proposals, votes)
}

View File

@ -0,0 +1,56 @@
package v0_15
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/app"
v0_14committee "github.com/kava-labs/kava/x/committee/legacy/v0_14"
v0_15committee "github.com/kava-labs/kava/x/committee/types"
"github.com/stretchr/testify/require"
)
func TestMain(m *testing.M) {
config := sdk.GetConfig()
app.SetBech32AddressPrefixes(config)
app.SetBip44CoinType(config)
os.Exit(m.Run())
}
func TestCommittee(t *testing.T) {
bz, err := ioutil.ReadFile(filepath.Join("testdata", "kava-7-committee-state.json"))
require.NoError(t, err)
var oldGenState v0_14committee.GenesisState
cdc := codec.New()
sdk.RegisterCodec(cdc)
v0_14committee.RegisterCodec(cdc)
require.NotPanics(t, func() {
cdc.MustUnmarshalJSON(bz, &oldGenState)
})
newGenState := Committee(oldGenState)
err = newGenState.Validate()
require.NoError(t, err)
require.Equal(t, len(oldGenState.Committees), len(newGenState.Committees))
for i := 0; i < len(oldGenState.Committees); i++ {
require.Equal(t, len(oldGenState.Committees[i].Permissions), len(newGenState.Committees[i].GetPermissions()))
}
oldSPCP := oldGenState.Committees[0].Permissions[0].(v0_14committee.SubParamChangePermission)
newSPCP := newGenState.Committees[0].GetPermissions()[0].(v0_15committee.SubParamChangePermission)
require.Equal(t, len(oldSPCP.AllowedParams), len(newSPCP.AllowedParams))
require.Equal(t, len(oldSPCP.AllowedAssetParams), len(newSPCP.AllowedAssetParams))
require.Equal(t, len(oldSPCP.AllowedCollateralParams), len(newSPCP.AllowedCollateralParams))
require.Equal(t, len(oldSPCP.AllowedMarkets), len(newSPCP.AllowedMarkets))
require.Equal(t, len(oldSPCP.AllowedMoneyMarkets), len(newSPCP.AllowedMoneyMarkets))
}

View File

@ -0,0 +1,402 @@
{
"committees": [
{
"id": "1",
"description": "Kava Stability Committee",
"members": [
"kava1gru35up50ql2wxhegr880qy6ynl63ujlv8gum2",
"kava1sc3mh3pkas5e7xd269am4xm5mp6zweyzmhjagj",
"kava1c9ye54e3pzwm3e0zpdlel6pnavrj9qqv6e8r4h",
"kava1m7p6sjqrz6mylz776ct48wj6lpnpcd0z82209d",
"kava1a9pmkzk570egv3sflu3uwdf3gejl7qfy9hghzl"
],
"permissions": [
{
"type": "kava/SubParamChangePermission",
"value": {
"allowed_params": [
{
"subspace": "auction",
"key": "BidDuration"
},
{
"subspace": "auction",
"key": "IncrementSurplus"
},
{
"subspace": "auction",
"key": "IncrementDebt"
},
{
"subspace": "auction",
"key": "IncrementCollateral"
},
{
"subspace": "bep3",
"key": "AssetParams"
},
{
"subspace": "cdp",
"key": "GlobalDebtLimit"
},
{
"subspace": "cdp",
"key": "SurplusThreshold"
},
{
"subspace": "cdp",
"key": "SurplusLot"
},
{
"subspace": "cdp",
"key": "DebtThreshold"
},
{
"subspace": "cdp",
"key": "DebtLot"
},
{
"subspace": "cdp",
"key": "DistributionFrequency"
},
{
"subspace": "cdp",
"key": "CollateralParams"
},
{
"subspace": "cdp",
"key": "DebtParam"
},
{
"subspace": "incentive",
"key": "Active"
},
{
"subspace": "kavadist",
"key": "Active"
},
{
"subspace": "pricefeed",
"key": "Markets"
},
{
"subspace": "hard",
"key": "MoneyMarkets"
},
{
"subspace": "hard",
"key": "MinimumBorrowUSDValue"
}
],
"allowed_collateral_params": [
{
"type": "bnb-a",
"denom": false,
"liquidation_ratio": false,
"debt_limit": true,
"stability_fee": true,
"auction_size": true,
"liquidation_penalty": false,
"prefix": false,
"spot_market_id": false,
"liquidation_market_id": false,
"conversion_factor": false,
"keeper_reward_percentage": true,
"check_collateralization_index_count": true
},
{
"type": "busd-a",
"denom": false,
"liquidation_ratio": false,
"debt_limit": true,
"stability_fee": true,
"auction_size": true,
"liquidation_penalty": false,
"prefix": false,
"spot_market_id": false,
"liquidation_market_id": false,
"conversion_factor": false,
"keeper_reward_percentage": true,
"check_collateralization_index_count": true
},
{
"type": "busd-b",
"denom": false,
"liquidation_ratio": false,
"debt_limit": true,
"stability_fee": true,
"auction_size": true,
"liquidation_penalty": false,
"prefix": false,
"spot_market_id": false,
"liquidation_market_id": false,
"conversion_factor": false,
"keeper_reward_percentage": true,
"check_collateralization_index_count": true
},
{
"type": "btcb-a",
"denom": false,
"liquidation_ratio": false,
"debt_limit": true,
"stability_fee": true,
"auction_size": true,
"liquidation_penalty": false,
"prefix": false,
"spot_market_id": false,
"liquidation_market_id": false,
"conversion_factor": false,
"keeper_reward_percentage": true,
"check_collateralization_index_count": true
},
{
"type": "xrpb-a",
"denom": false,
"liquidation_ratio": false,
"debt_limit": true,
"stability_fee": true,
"auction_size": true,
"liquidation_penalty": false,
"prefix": false,
"spot_market_id": false,
"liquidation_market_id": false,
"conversion_factor": false,
"keeper_reward_percentage": true,
"check_collateralization_index_count": true
},
{
"type": "ukava-a",
"denom": false,
"liquidation_ratio": false,
"debt_limit": true,
"stability_fee": true,
"auction_size": true,
"liquidation_penalty": false,
"prefix": false,
"spot_market_id": false,
"liquidation_market_id": false,
"conversion_factor": false,
"keeper_reward_percentage": true,
"check_collateralization_index_count": true
},
{
"type": "hard-a",
"denom": false,
"liquidation_ratio": false,
"debt_limit": true,
"stability_fee": true,
"auction_size": true,
"liquidation_penalty": false,
"prefix": false,
"spot_market_id": false,
"liquidation_market_id": false,
"conversion_factor": false,
"keeper_reward_percentage": true,
"check_collateralization_index_count": true
},
{
"type": "hbtc-a",
"denom": false,
"liquidation_ratio": false,
"debt_limit": true,
"stability_fee": true,
"auction_size": true,
"liquidation_penalty": false,
"prefix": false,
"spot_market_id": false,
"liquidation_market_id": false,
"conversion_factor": false,
"keeper_reward_percentage": true,
"check_collateralization_index_count": true
}
],
"allowed_debt_param": {
"denom": false,
"reference_asset": false,
"conversion_factor": false,
"debt_floor": true
},
"allowed_asset_params": [
{
"denom": "bnb",
"coin_id": false,
"limit": true,
"active": true,
"max_swap_amount": true,
"min_block_lock": true
},
{
"denom": "busd",
"coin_id": true,
"limit": true,
"active": true,
"max_swap_amount": true,
"min_block_lock": true
},
{
"denom": "btcb",
"coin_id": false,
"limit": true,
"active": true,
"max_swap_amount": true,
"min_block_lock": true
},
{
"denom": "xrpb",
"coin_id": false,
"limit": true,
"active": true,
"max_swap_amount": true,
"min_block_lock": true
}
],
"allowed_markets": [
{
"market_id": "bnb:usd",
"base_asset": false,
"quote_asset": false,
"oracles": false,
"active": true
},
{
"market_id": "bnb:usd:30",
"base_asset": false,
"quote_asset": false,
"oracles": false,
"active": true
},
{
"market_id": "btc:usd",
"base_asset": false,
"quote_asset": false,
"oracles": false,
"active": true
},
{
"market_id": "btc:usd:30",
"base_asset": false,
"quote_asset": false,
"oracles": false,
"active": true
},
{
"market_id": "xrp:usd",
"base_asset": false,
"quote_asset": false,
"oracles": false,
"active": true
},
{
"market_id": "xrp:usd:30",
"base_asset": false,
"quote_asset": false,
"oracles": false,
"active": true
},
{
"market_id": "busd:usd",
"base_asset": false,
"quote_asset": false,
"oracles": false,
"active": true
},
{
"market_id": "busd:usd:30",
"base_asset": false,
"quote_asset": false,
"oracles": false,
"active": true
}
],
"allowed_money_markets": [
{
"denom": "bnb",
"borrow_limit": true,
"spot_market_id": false,
"conversion_factor": false,
"interest_rate_model": true,
"reserve_factor": true,
"keeper_reward_percentage": true
},
{
"denom": "busd",
"borrow_limit": true,
"spot_market_id": false,
"conversion_factor": false,
"interest_rate_model": true,
"reserve_factor": true,
"keeper_reward_percentage": true
},
{
"denom": "btcb",
"borrow_limit": true,
"spot_market_id": false,
"conversion_factor": false,
"interest_rate_model": true,
"reserve_factor": true,
"keeper_reward_percentage": true
},
{
"denom": "xrpb",
"borrow_limit": true,
"spot_market_id": false,
"conversion_factor": false,
"interest_rate_model": true,
"reserve_factor": true,
"keeper_reward_percentage": true
},
{
"denom": "usdx",
"borrow_limit": true,
"spot_market_id": false,
"conversion_factor": false,
"interest_rate_model": true,
"reserve_factor": true,
"keeper_reward_percentage": true
},
{
"denom": "ukava",
"borrow_limit": true,
"spot_market_id": false,
"conversion_factor": false,
"interest_rate_model": true,
"reserve_factor": true,
"keeper_reward_percentage": true
},
{
"denom": "hard",
"borrow_limit": true,
"spot_market_id": false,
"conversion_factor": false,
"interest_rate_model": true,
"reserve_factor": true,
"keeper_reward_percentage": true
}
]
}
},
{
"type": "kava/TextPermission",
"value": {}
}
],
"vote_threshold": "0.500000000000000000",
"proposal_duration": "604800000000000"
},
{
"id": "2",
"description": "Kava Safety Committee",
"members": [
"kava1e0agyg6eug9r62fly9sls77ycjgw8ax6xk73es"
],
"permissions": [
{
"type": "kava/SoftwareUpgradePermission",
"value": {}
}
],
"vote_threshold": "0.500000000000000000",
"proposal_duration": "604800000000000"
}
]
}

View File

@ -8,7 +8,5 @@ import (
// BeginBlocker runs at the start of every block. // BeginBlocker runs at the start of every block.
func BeginBlocker(ctx sdk.Context, _ abci.RequestBeginBlock, k Keeper) { func BeginBlocker(ctx sdk.Context, _ abci.RequestBeginBlock, k Keeper) {
// enact proposals ignoring their expiry time - they could have received enough votes last block before expiring this block k.ProcessProposals(ctx)
k.EnactPassedProposals(ctx)
k.CloseExpiredProposals(ctx)
} }

View File

@ -17,6 +17,7 @@ import (
"github.com/kava-labs/kava/x/cdp" "github.com/kava-labs/kava/x/cdp"
cdptypes "github.com/kava-labs/kava/x/cdp/types" cdptypes "github.com/kava-labs/kava/x/cdp/types"
"github.com/kava-labs/kava/x/committee" "github.com/kava-labs/kava/x/committee"
"github.com/kava-labs/kava/x/committee/types"
) )
type ModuleTestSuite struct { type ModuleTestSuite struct {
@ -39,26 +40,28 @@ func (suite *ModuleTestSuite) SetupTest() {
func (suite *ModuleTestSuite) TestBeginBlock_ClosesExpired() { func (suite *ModuleTestSuite) TestBeginBlock_ClosesExpired() {
suite.app.InitializeFromGenesisStates() suite.app.InitializeFromGenesisStates()
normalCom := committee.Committee{ memberCom := committee.MemberCommittee{
ID: 12, BaseCommittee: committee.BaseCommittee{
Members: suite.addresses[:2], ID: 12,
Permissions: []committee.Permission{committee.GodPermission{}}, Members: suite.addresses[:2],
VoteThreshold: d("0.8"), Permissions: []committee.Permission{committee.GodPermission{}},
ProposalDuration: time.Hour * 24 * 7, VoteThreshold: d("0.8"),
ProposalDuration: time.Hour * 24 * 7,
},
} }
suite.keeper.SetCommittee(suite.ctx, normalCom) suite.keeper.SetCommittee(suite.ctx, memberCom)
pprop1 := gov.NewTextProposal("Title 1", "A description of this proposal.") pprop1 := gov.NewTextProposal("Title 1", "A description of this proposal.")
id1, err := suite.keeper.SubmitProposal(suite.ctx, normalCom.Members[0], normalCom.ID, pprop1) id1, err := suite.keeper.SubmitProposal(suite.ctx, memberCom.Members[0], memberCom.ID, pprop1)
suite.NoError(err) suite.NoError(err)
oneHrLaterCtx := suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(time.Hour)) oneHrLaterCtx := suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(time.Hour))
pprop2 := gov.NewTextProposal("Title 2", "A description of this proposal.") pprop2 := gov.NewTextProposal("Title 2", "A description of this proposal.")
id2, err := suite.keeper.SubmitProposal(oneHrLaterCtx, normalCom.Members[0], normalCom.ID, pprop2) id2, err := suite.keeper.SubmitProposal(oneHrLaterCtx, memberCom.Members[0], memberCom.ID, pprop2)
suite.NoError(err) suite.NoError(err)
// Run BeginBlocker // Run BeginBlocker
proposalDurationLaterCtx := suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(normalCom.ProposalDuration)) proposalDurationLaterCtx := suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(memberCom.ProposalDuration))
suite.NotPanics(func() { suite.NotPanics(func() {
committee.BeginBlocker(proposalDurationLaterCtx, abci.RequestBeginBlock{}, suite.keeper) committee.BeginBlocker(proposalDurationLaterCtx, abci.RequestBeginBlock{}, suite.keeper)
}) })
@ -74,13 +77,9 @@ func (suite *ModuleTestSuite) TestBeginBlock_EnactsPassed() {
suite.app.InitializeFromGenesisStates() suite.app.InitializeFromGenesisStates()
// setup committee // setup committee
normalCom := committee.Committee{ normalCom := committee.NewMemberCommittee(12, "committee description", suite.addresses[:2],
ID: 12, []committee.Permission{committee.GodPermission{}}, d("0.8"), time.Hour*24*7, types.FirstPastThePost)
Members: suite.addresses[:2],
Permissions: []committee.Permission{committee.GodPermission{}},
VoteThreshold: d("0.8"),
ProposalDuration: time.Hour * 24 * 7,
}
suite.keeper.SetCommittee(suite.ctx, normalCom) suite.keeper.SetCommittee(suite.ctx, normalCom)
// setup 2 proposals // setup 2 proposals
@ -109,9 +108,9 @@ func (suite *ModuleTestSuite) TestBeginBlock_EnactsPassed() {
suite.NoError(err) suite.NoError(err)
// add enough votes to make the first proposal pass, but not the second // add enough votes to make the first proposal pass, but not the second
suite.NoError(suite.keeper.AddVote(suite.ctx, id1, suite.addresses[0])) suite.NoError(suite.keeper.AddVote(suite.ctx, id1, suite.addresses[0], types.Yes))
suite.NoError(suite.keeper.AddVote(suite.ctx, id1, suite.addresses[1])) suite.NoError(suite.keeper.AddVote(suite.ctx, id1, suite.addresses[1], types.Yes))
suite.NoError(suite.keeper.AddVote(suite.ctx, id2, suite.addresses[0])) suite.NoError(suite.keeper.AddVote(suite.ctx, id2, suite.addresses[0], types.Yes))
// Run BeginBlocker // Run BeginBlocker
suite.NotPanics(func() { suite.NotPanics(func() {
@ -131,16 +130,12 @@ func (suite *ModuleTestSuite) TestBeginBlock_DoesntEnactFailed() {
suite.app.InitializeFromGenesisStates() suite.app.InitializeFromGenesisStates()
// setup committee // setup committee
normalCom := committee.Committee{ memberCom := committee.NewMemberCommittee(12, "committee description", suite.addresses[:1],
ID: 12, []committee.Permission{committee.SoftwareUpgradePermission{}}, d("1.0"), time.Hour*24*7, types.FirstPastThePost)
Members: suite.addresses[:1],
Permissions: []committee.Permission{committee.SoftwareUpgradePermission{}},
VoteThreshold: d("1.0"),
ProposalDuration: time.Hour * 24 * 7,
}
firstBlockTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC) firstBlockTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
ctx := suite.ctx.WithBlockTime(firstBlockTime) ctx := suite.ctx.WithBlockTime(firstBlockTime)
suite.keeper.SetCommittee(ctx, normalCom) suite.keeper.SetCommittee(ctx, memberCom)
// setup an upgrade proposal // setup an upgrade proposal
pprop1 := upgrade.NewSoftwareUpgradeProposal("Title 1", "A description of this proposal.", pprop1 := upgrade.NewSoftwareUpgradeProposal("Title 1", "A description of this proposal.",
@ -150,11 +145,11 @@ func (suite *ModuleTestSuite) TestBeginBlock_DoesntEnactFailed() {
Info: "some information about the upgrade", Info: "some information about the upgrade",
}, },
) )
id1, err := suite.keeper.SubmitProposal(ctx, normalCom.Members[0], normalCom.ID, pprop1) id1, err := suite.keeper.SubmitProposal(ctx, memberCom.Members[0], memberCom.ID, pprop1)
suite.NoError(err) suite.NoError(err)
// add enough votes to make the proposal pass // add enough votes to make the proposal pass
suite.NoError(suite.keeper.AddVote(ctx, id1, suite.addresses[0])) suite.NoError(suite.keeper.AddVote(ctx, id1, suite.addresses[0], types.Yes))
// Run BeginBlocker 10 seconds later (5 seconds after upgrade expires) // Run BeginBlocker 10 seconds later (5 seconds after upgrade expires)
tenSecLaterCtx := ctx.WithBlockTime(ctx.BlockTime().Add(time.Second * 10)) tenSecLaterCtx := ctx.WithBlockTime(ctx.BlockTime().Add(time.Second * 10))
@ -180,16 +175,20 @@ func (suite *ModuleTestSuite) TestBeginBlock_EnactsPassedUpgrade() {
suite.app.InitializeFromGenesisStates() suite.app.InitializeFromGenesisStates()
// setup committee // setup committee
normalCom := committee.Committee{ memberCom := committee.MemberCommittee{
ID: 12, BaseCommittee: committee.BaseCommittee{
Members: suite.addresses[:1], ID: 12,
Permissions: []committee.Permission{committee.SoftwareUpgradePermission{}}, Members: suite.addresses[:1],
VoteThreshold: d("1.0"), Permissions: []committee.Permission{committee.SoftwareUpgradePermission{}},
ProposalDuration: time.Hour * 24 * 7, VoteThreshold: d("1.0"),
ProposalDuration: time.Hour * 24 * 7,
TallyOption: types.FirstPastThePost,
},
} }
firstBlockTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC) firstBlockTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
ctx := suite.ctx.WithBlockTime(firstBlockTime) ctx := suite.ctx.WithBlockTime(firstBlockTime)
suite.keeper.SetCommittee(ctx, normalCom) suite.keeper.SetCommittee(ctx, memberCom)
// setup an upgrade proposal // setup an upgrade proposal
pprop1 := upgrade.NewSoftwareUpgradeProposal("Title 1", "A description of this proposal.", pprop1 := upgrade.NewSoftwareUpgradeProposal("Title 1", "A description of this proposal.",
@ -199,11 +198,11 @@ func (suite *ModuleTestSuite) TestBeginBlock_EnactsPassedUpgrade() {
Info: "some information about the upgrade", Info: "some information about the upgrade",
}, },
) )
id1, err := suite.keeper.SubmitProposal(ctx, normalCom.Members[0], normalCom.ID, pprop1) id1, err := suite.keeper.SubmitProposal(ctx, memberCom.Members[0], memberCom.ID, pprop1)
suite.NoError(err) suite.NoError(err)
// add enough votes to make the proposal pass // add enough votes to make the proposal pass
suite.NoError(suite.keeper.AddVote(ctx, id1, suite.addresses[0])) suite.NoError(suite.keeper.AddVote(ctx, id1, suite.addresses[0], types.Yes))
// Run BeginBlocker // Run BeginBlocker
fiveSecLaterCtx := ctx.WithBlockTime(ctx.BlockTime().Add(time.Second * 5)) fiveSecLaterCtx := ctx.WithBlockTime(ctx.BlockTime().Add(time.Second * 5))

View File

@ -14,9 +14,6 @@ const (
AttributeKeyProposalID = types.AttributeKeyProposalID AttributeKeyProposalID = types.AttributeKeyProposalID
AttributeKeyVoter = types.AttributeKeyVoter AttributeKeyVoter = types.AttributeKeyVoter
AttributeValueCategory = types.AttributeValueCategory AttributeValueCategory = types.AttributeValueCategory
AttributeValueProposalFailed = types.AttributeValueProposalFailed
AttributeValueProposalPassed = types.AttributeValueProposalPassed
AttributeValueProposalTimeout = types.AttributeValueProposalTimeout
DefaultNextProposalID = types.DefaultNextProposalID DefaultNextProposalID = types.DefaultNextProposalID
DefaultParamspace = types.DefaultParamspace DefaultParamspace = types.DefaultParamspace
EventTypeProposalClose = types.EventTypeProposalClose EventTypeProposalClose = types.EventTypeProposalClose
@ -24,6 +21,7 @@ const (
EventTypeProposalVote = types.EventTypeProposalVote EventTypeProposalVote = types.EventTypeProposalVote
MaxCommitteeDescriptionLength = types.MaxCommitteeDescriptionLength MaxCommitteeDescriptionLength = types.MaxCommitteeDescriptionLength
ModuleName = types.ModuleName ModuleName = types.ModuleName
No = types.No
ProposalTypeCommitteeChange = types.ProposalTypeCommitteeChange ProposalTypeCommitteeChange = types.ProposalTypeCommitteeChange
ProposalTypeCommitteeDelete = types.ProposalTypeCommitteeDelete ProposalTypeCommitteeDelete = types.ProposalTypeCommitteeDelete
QuerierRoute = types.QuerierRoute QuerierRoute = types.QuerierRoute
@ -40,25 +38,23 @@ const (
StoreKey = types.StoreKey StoreKey = types.StoreKey
TypeMsgSubmitProposal = types.TypeMsgSubmitProposal TypeMsgSubmitProposal = types.TypeMsgSubmitProposal
TypeMsgVote = types.TypeMsgVote TypeMsgVote = types.TypeMsgVote
Yes = types.Yes
) )
var ( var (
// function aliases // function aliases
NewKeeper = keeper.NewKeeper NewKeeper = keeper.NewKeeper
NewQuerier = keeper.NewQuerier NewQuerier = keeper.NewQuerier
RegisterInvariants = keeper.RegisterInvariants
ValidCommitteesInvariant = keeper.ValidCommitteesInvariant
ValidProposalsInvariant = keeper.ValidProposalsInvariant
ValidVotesInvariant = keeper.ValidVotesInvariant
DefaultGenesisState = types.DefaultGenesisState DefaultGenesisState = types.DefaultGenesisState
GetKeyFromID = types.GetKeyFromID GetKeyFromID = types.GetKeyFromID
GetVoteKey = types.GetVoteKey GetVoteKey = types.GetVoteKey
NewAllowedCollateralParam = types.NewAllowedCollateralParam NewAllowedCollateralParam = types.NewAllowedCollateralParam
NewAllowedMoneyMarket = types.NewAllowedMoneyMarket NewAllowedMoneyMarket = types.NewAllowedMoneyMarket
NewCommittee = types.NewCommittee
NewCommitteeChangeProposal = types.NewCommitteeChangeProposal NewCommitteeChangeProposal = types.NewCommitteeChangeProposal
NewCommitteeDeleteProposal = types.NewCommitteeDeleteProposal NewCommitteeDeleteProposal = types.NewCommitteeDeleteProposal
NewGenesisState = types.NewGenesisState NewGenesisState = types.NewGenesisState
NewMemberCommittee = types.NewMemberCommittee
NewTokenCommittee = types.NewTokenCommittee
NewMsgSubmitProposal = types.NewMsgSubmitProposal NewMsgSubmitProposal = types.NewMsgSubmitProposal
NewMsgVote = types.NewMsgVote NewMsgVote = types.NewMsgVote
NewProposal = types.NewProposal NewProposal = types.NewProposal
@ -104,12 +100,15 @@ type (
AllowedParam = types.AllowedParam AllowedParam = types.AllowedParam
AllowedParams = types.AllowedParams AllowedParams = types.AllowedParams
Committee = types.Committee Committee = types.Committee
BaseCommittee = types.BaseCommittee
CommitteeChangeProposal = types.CommitteeChangeProposal CommitteeChangeProposal = types.CommitteeChangeProposal
CommitteeDeleteProposal = types.CommitteeDeleteProposal CommitteeDeleteProposal = types.CommitteeDeleteProposal
GenesisState = types.GenesisState GenesisState = types.GenesisState
GodPermission = types.GodPermission GodPermission = types.GodPermission
MsgSubmitProposal = types.MsgSubmitProposal MsgSubmitProposal = types.MsgSubmitProposal
MemberCommittee = types.MemberCommittee
MsgVote = types.MsgVote MsgVote = types.MsgVote
TokenCommittee = types.TokenCommittee
ParamKeeper = types.ParamKeeper ParamKeeper = types.ParamKeeper
Permission = types.Permission Permission = types.Permission
Proposal = types.Proposal Proposal = types.Proposal

View File

@ -74,7 +74,7 @@ func GetCmdQueryCommittee(queryRoute string, cdc *codec.Codec) *cobra.Command {
} }
// Decode and print result // Decode and print result
committee := types.Committee{} var committee types.Committee
if err = cdc.UnmarshalJSON(res, &committee); err != nil { if err = cdc.UnmarshalJSON(res, &committee); err != nil {
return err return err
} }
@ -251,11 +251,11 @@ func GetCmdQueryTally(queryRoute string, cdc *codec.Codec) *cobra.Command {
} }
// Decode and print results // Decode and print results
var tally bool var pollingStatus types.ProposalPollingStatus
if err = cdc.UnmarshalJSON(res, &tally); err != nil { if err = cdc.UnmarshalJSON(res, &pollingStatus); err != nil {
return err return err
} }
return cliCtx.PrintOutput(tally) return cliCtx.PrintOutput(pollingStatus)
}, },
} }
} }

View File

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"strconv" "strconv"
"strings"
"time" "time"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -100,11 +101,11 @@ For example:
// GetCmdVote returns the command to vote on a proposal. // GetCmdVote returns the command to vote on a proposal.
func GetCmdVote(cdc *codec.Codec) *cobra.Command { func GetCmdVote(cdc *codec.Codec) *cobra.Command {
return &cobra.Command{ return &cobra.Command{
Use: "vote [proposal-id]", Use: "vote [proposal-id] [vote]",
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(2),
Short: "Vote for an active proposal", Short: "Vote for an active proposal",
Long: "Submit a yes vote for the proposal with id [proposal-id].", Long: "Submit a [yes/no/abstain] vote for the proposal with id [proposal-id].",
Example: fmt.Sprintf("%s tx %s vote 2", version.ClientName, types.ModuleName), Example: fmt.Sprintf("%s tx %s vote 2 yes", version.ClientName, types.ModuleName),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
inBuf := bufio.NewReader(cmd.InOrStdin()) inBuf := bufio.NewReader(cmd.InOrStdin())
txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc))
@ -119,8 +120,25 @@ func GetCmdVote(cdc *codec.Codec) *cobra.Command {
return fmt.Errorf("proposal-id %s not a valid int, please input a valid proposal-id", args[0]) return fmt.Errorf("proposal-id %s not a valid int, please input a valid proposal-id", args[0])
} }
rawVote := strings.ToLower(strings.TrimSpace(args[1]))
if len(rawVote) == 0 {
return fmt.Errorf("must specify a vote")
}
var vote types.VoteType
switch rawVote {
case "yes", "y":
vote = types.Yes
case "no", "n":
vote = types.No
case "abstain", "a":
vote = types.Abstain
default:
return fmt.Errorf("must specify a valid vote type: (yes/y, no/n, abstain/a)")
}
// Build vote message and run basic validation // Build vote message and run basic validation
msg := types.NewMsgVote(from, proposalID) msg := types.NewMsgVote(from, proposalID, vote)
err = msg.ValidateBasic() err = msg.ValidateBasic()
if err != nil { if err != nil {
return err return err
@ -192,7 +210,7 @@ func MustGetExampleCommitteeChangeProposal(cdc *codec.Codec) string {
exampleChangeProposal := types.NewCommitteeChangeProposal( exampleChangeProposal := types.NewCommitteeChangeProposal(
"A Title", "A Title",
"A description of this proposal.", "A description of this proposal.",
types.NewCommittee( types.NewMemberCommittee(
1, 1,
"The description of this committee.", "The description of this committee.",
[]sdk.AccAddress{sdk.AccAddress(crypto.AddressHash([]byte("exampleAddress")))}, []sdk.AccAddress{sdk.AccAddress(crypto.AddressHash([]byte("exampleAddress")))},
@ -203,6 +221,7 @@ func MustGetExampleCommitteeChangeProposal(cdc *codec.Codec) string {
}, },
sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("0.8"),
time.Hour*24*7, time.Hour*24*7,
types.FirstPastThePost,
), ),
) )
exampleChangeProposalBz, err := cdc.MarshalJSONIndent(exampleChangeProposal, "", " ") exampleChangeProposalBz, err := cdc.MarshalJSONIndent(exampleChangeProposal, "", " ")
@ -228,10 +247,11 @@ func MustGetExampleCommitteeDeleteProposal(cdc *codec.Codec) string {
// MustGetExampleParameterChangeProposal is a helper function to return an example json proposal // MustGetExampleParameterChangeProposal is a helper function to return an example json proposal
func MustGetExampleParameterChangeProposal(cdc *codec.Codec) string { func MustGetExampleParameterChangeProposal(cdc *codec.Codec) string {
value := fmt.Sprintf("\"%d\"", 1000000000)
exampleParameterChangeProposal := params.NewParameterChangeProposal( exampleParameterChangeProposal := params.NewParameterChangeProposal(
"A Title", "A Title",
"A description of this proposal.", "A description of this proposal.",
[]params.ParamChange{params.NewParamChange("cdp", "SurplusAuctionThreshold", "1000000000")}, []params.ParamChange{params.NewParamChange("cdp", "SurplusAuctionThreshold", value)},
) )
exampleParameterChangeProposalBz, err := cdc.MarshalJSONIndent(exampleParameterChangeProposal, "", " ") exampleParameterChangeProposalBz, err := cdc.MarshalJSONIndent(exampleParameterChangeProposal, "", " ")
if err != nil { if err != nil {

View File

@ -159,6 +159,6 @@ func calculateDeadline(cliCtx context.CLIContext, cdc *codec.Codec, queryRoute s
return deadline, err return deadline, err
} }
deadline = resultBlock.Block.Header.Time.Add(committee.ProposalDuration) deadline = resultBlock.Block.Header.Time.Add(committee.GetProposalDuration())
return deadline, nil return deadline, nil
} }

View File

@ -10,6 +10,7 @@ import (
const ( const (
RestProposalID = "proposal-id" RestProposalID = "proposal-id"
RestCommitteeID = "committee-id" RestCommitteeID = "committee-id"
RestVote = "vote"
) )
// RegisterRoutes - Central function to define routes that get registered by the main application // RegisterRoutes - Central function to define routes that get registered by the main application

View File

@ -3,6 +3,7 @@ package rest
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"strings"
"github.com/gorilla/mux" "github.com/gorilla/mux"
@ -70,6 +71,7 @@ func postProposalHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
type PostVoteReq struct { type PostVoteReq struct {
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"` BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
Voter sdk.AccAddress `json:"voter" yaml:"voter"` Voter sdk.AccAddress `json:"voter" yaml:"voter"`
Vote types.VoteType `json:"vote" yaml:"vote"`
} }
func postVoteHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { func postVoteHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
@ -86,6 +88,28 @@ func postVoteHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return return
} }
if len(vars[RestVote]) == 0 {
rest.WriteErrorResponse(w, http.StatusBadRequest, fmt.Sprintf("%s required but not specified", RestVote))
return
}
rawVote := strings.ToLower(strings.TrimSpace(vars[RestVote]))
if len(rawVote) == 0 {
rest.WriteErrorResponse(w, http.StatusBadRequest, fmt.Sprintf("invalid %s: %s", RestVote, rawVote))
return
}
var vote types.VoteType
switch rawVote {
case "yes", "y":
vote = types.Yes
case "no", "n":
vote = types.No
default:
rest.WriteErrorResponse(w, http.StatusBadRequest, "must specify a valid vote type (\"yes\", \"y\"/\"no\" \"n\")")
return
}
// Parse and validate http request body // Parse and validate http request body
var req PostVoteReq var req PostVoteReq
if !rest.ReadRESTReq(w, r, cliCtx.Codec, &req) { if !rest.ReadRESTReq(w, r, cliCtx.Codec, &req) {
@ -97,7 +121,7 @@ func postVoteHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
} }
// Create and return a StdTx // Create and return a StdTx
msg := types.NewMsgVote(req.Voter, proposalID) msg := types.NewMsgVote(req.Voter, proposalID, vote)
if err := msg.ValidateBasic(); err != nil { if err := msg.ValidateBasic(); err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return return

View File

@ -2,6 +2,7 @@ package committee_test
import ( import (
"testing" "testing"
"time"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
@ -17,12 +18,49 @@ import (
type GenesisTestSuite struct { type GenesisTestSuite struct {
suite.Suite suite.Suite
app app.TestApp app app.TestApp
ctx sdk.Context ctx sdk.Context
keeper committee.Keeper keeper committee.Keeper
addresses []sdk.AccAddress
} }
func (suite *GenesisTestSuite) TestGenesis() { func (suite *GenesisTestSuite) SetupTest() {
suite.app = app.NewTestApp()
suite.keeper = suite.app.GetCommitteeKeeper()
suite.ctx = suite.app.NewContext(true, abci.Header{})
_, suite.addresses = app.GeneratePrivKeyAddressPairs(10)
}
func (suite *GenesisTestSuite) TestInitGenesis() {
memberCom := types.MemberCommittee{
BaseCommittee: types.BaseCommittee{
ID: 1,
Description: "This member committee is for testing.",
Members: suite.addresses[:2],
Permissions: []types.Permission{types.GodPermission{}},
VoteThreshold: d("0.667"),
ProposalDuration: time.Hour * 24 * 7,
TallyOption: types.FirstPastThePost,
},
}
tokenCom := types.TokenCommittee{
BaseCommittee: types.BaseCommittee{
ID: 1,
Description: "This token committee is for testing.",
Members: suite.addresses[:2],
Permissions: []types.Permission{types.GodPermission{}},
VoteThreshold: d("0.667"),
ProposalDuration: time.Hour * 24 * 7,
TallyOption: types.FirstPastThePost,
},
Quorum: d("0.4"),
TallyDenom: "hard",
}
// Most genesis validation tests are located in the types directory. The 'invalid' test cases are
// randomly selected subset of those tests.
testCases := []struct { testCases := []struct {
name string name string
genState types.GenesisState genState types.GenesisState
@ -34,7 +72,37 @@ func (suite *GenesisTestSuite) TestGenesis() {
expectPass: true, expectPass: true,
}, },
{ {
name: "invalid", name: "member committee is correctly validated",
genState: types.NewGenesisState(
1,
[]types.Committee{memberCom},
[]types.Proposal{},
[]types.Vote{},
),
expectPass: true,
},
{
name: "token committee is correctly validated",
genState: types.NewGenesisState(
1,
[]types.Committee{tokenCom},
[]types.Proposal{},
[]types.Vote{},
),
expectPass: true,
},
{
name: "invalid: duplicate committee ID",
genState: types.NewGenesisState(
1,
[]types.Committee{memberCom, memberCom},
[]types.Proposal{},
[]types.Vote{},
),
expectPass: false,
},
{
name: "invalid: proposal doesn't have committee",
genState: types.NewGenesisState( genState: types.NewGenesisState(
2, 2,
[]types.Committee{}, []types.Committee{},
@ -43,6 +111,26 @@ func (suite *GenesisTestSuite) TestGenesis() {
), ),
expectPass: false, expectPass: false,
}, },
{
name: "invalid: vote doesn't have proposal",
genState: types.NewGenesisState(
1,
[]types.Committee{},
[]types.Proposal{},
[]types.Vote{{Voter: suite.addresses[0], ProposalID: 1, VoteType: types.Yes}},
),
expectPass: false,
},
{
name: "invalid: next proposal ID isn't greater than proposal ID",
genState: types.NewGenesisState(
4,
[]types.Committee{memberCom},
[]types.Proposal{{ID: 3, CommitteeID: 1}, {ID: 4, CommitteeID: 1}},
[]types.Vote{},
),
expectPass: false,
},
} }
for _, tc := range testCases { for _, tc := range testCases {
suite.Run(tc.name, func() { suite.Run(tc.name, func() {

View File

@ -45,7 +45,7 @@ func handleMsgSubmitProposal(ctx sdk.Context, k keeper.Keeper, msg types.MsgSubm
} }
func handleMsgVote(ctx sdk.Context, k keeper.Keeper, msg types.MsgVote) (*sdk.Result, error) { func handleMsgVote(ctx sdk.Context, k keeper.Keeper, msg types.MsgVote) (*sdk.Result, error) {
err := k.AddVote(ctx, msg.ProposalID, msg.Voter) err := k.AddVote(ctx, msg.ProposalID, msg.Voter, msg.VoteType)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -49,13 +49,16 @@ func (suite *HandlerTestSuite) SetupTest() {
testGenesis := types.NewGenesisState( testGenesis := types.NewGenesisState(
3, 3,
[]types.Committee{ []types.Committee{
{ types.MemberCommittee{
ID: 1, BaseCommittee: types.BaseCommittee{
Description: "This committee is for testing.", ID: 1,
Members: suite.addresses[:3], Description: "This committee is for testing.",
Permissions: []types.Permission{types.GodPermission{}}, Members: suite.addresses[:3],
VoteThreshold: d("0.5"), Permissions: []types.Permission{types.GodPermission{}},
ProposalDuration: time.Hour * 24 * 7, VoteThreshold: d("0.5"),
ProposalDuration: time.Hour * 24 * 7,
TallyOption: types.FirstPastThePost,
},
}, },
}, },
[]types.Proposal{}, []types.Proposal{},

View File

@ -167,13 +167,14 @@ func (suite *TypesTestSuite) TestCommittee_HasPermissionsFor() {
tApp := app.NewTestApp() tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{}) ctx := tApp.NewContext(true, abci.Header{})
tApp.InitializeFromGenesisStates() tApp.InitializeFromGenesisStates()
com := types.NewCommittee( com := types.NewMemberCommittee(
12, 12,
"a description of this committee", "a description of this committee",
nil, nil,
tc.permissions, tc.permissions,
d("0.5"), d("0.5"),
24*time.Hour, 24*time.Hour,
types.FirstPastThePost,
) )
suite.Equal( suite.Equal(
tc.expectHasPermissions, tc.expectHasPermissions,

View File

@ -3,6 +3,7 @@ package keeper_test
import ( import (
"github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
authexported "github.com/cosmos/cosmos-sdk/x/auth/exported"
"github.com/kava-labs/kava/app" "github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/committee" "github.com/kava-labs/kava/x/committee"
@ -28,6 +29,11 @@ func getProposalVoteMap(k keeper.Keeper, ctx sdk.Context) map[uint64]([]types.Vo
return proposalVoteMap return proposalVoteMap
} }
func (suite *KeeperTestSuite) getAccount(addr sdk.AccAddress) authexported.Account {
ak := suite.app.GetAccountKeeper()
return ak.GetAccount(suite.ctx, addr)
}
// NewCommitteeGenesisState marshals a committee genesis state into json for use in initializing test apps. // NewCommitteeGenesisState marshals a committee genesis state into json for use in initializing test apps.
func NewCommitteeGenesisState(cdc *codec.Codec, gs committee.GenesisState) app.GenesisState { func NewCommitteeGenesisState(cdc *codec.Codec, gs committee.GenesisState) app.GenesisState {
return app.GenesisState{committee.ModuleName: cdc.MustMarshalJSON(gs)} return app.GenesisState{committee.ModuleName: cdc.MustMarshalJSON(gs)}

View File

@ -1,140 +0,0 @@
package keeper
import (
"fmt"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/committee/types"
)
// RegisterInvariants registers all committee invariants
func RegisterInvariants(ir sdk.InvariantRegistry, k Keeper) {
ir.RegisterRoute(types.ModuleName, "valid-committees",
ValidCommitteesInvariant(k))
ir.RegisterRoute(types.ModuleName, "valid-proposals",
ValidProposalsInvariant(k))
ir.RegisterRoute(types.ModuleName, "valid-votes",
ValidVotesInvariant(k))
}
// ValidCommitteesInvariant verifies that all committees in the store are independently valid
func ValidCommitteesInvariant(k Keeper) sdk.Invariant {
return func(ctx sdk.Context) (string, bool) {
var validationErr error
var invalidCommittee types.Committee
k.IterateCommittees(ctx, func(com types.Committee) bool {
if err := com.Validate(); err != nil {
validationErr = err
invalidCommittee = com
return true
}
return false
})
broken := validationErr != nil
invariantMessage := sdk.FormatInvariant(
types.ModuleName,
"valid committees",
fmt.Sprintf(
"\tfound invalid committee, reason: %s\n"+
"\tcommittee:\n\t%+v\n",
validationErr, invalidCommittee),
)
return invariantMessage, broken
}
}
// ValidProposalsInvariant verifies that all proposals in the store are valid
func ValidProposalsInvariant(k Keeper) sdk.Invariant {
return func(ctx sdk.Context) (string, bool) {
var validationErr error
var invalidProposal types.Proposal
k.IterateProposals(ctx, func(proposal types.Proposal) bool {
invalidProposal = proposal
if err := proposal.PubProposal.ValidateBasic(); err != nil {
validationErr = err
return true
}
currentTime := ctx.BlockTime()
if !currentTime.Equal(time.Time{}) { // this avoids a simulator bug where app.InitGenesis is called with blockTime=0 instead of the correct time
if proposal.Deadline.Before(currentTime) {
validationErr = fmt.Errorf("deadline after current block time %s", currentTime)
return true
}
}
_, found := k.GetCommittee(ctx, proposal.CommitteeID)
if !found {
validationErr = fmt.Errorf("proposal has no committee %d", proposal.CommitteeID)
return true
}
return false
})
broken := validationErr != nil
invariantMessage := sdk.FormatInvariant(
types.ModuleName,
"valid proposals",
fmt.Sprintf(
"\tfound invalid proposal, reason: %s\n"+
"\tproposal:\n\t%s\n",
validationErr, invalidProposal),
)
return invariantMessage, broken
}
}
// ValidVotesInvariant verifies that all votes in the store are valid
func ValidVotesInvariant(k Keeper) sdk.Invariant {
return func(ctx sdk.Context) (string, bool) {
var validationErr error
var invalidVote types.Vote
k.IterateVotes(ctx, func(vote types.Vote) bool {
invalidVote = vote
if err := vote.Validate(); err != nil {
validationErr = err
return true
}
proposal, found := k.GetProposal(ctx, vote.ProposalID)
if !found {
validationErr = fmt.Errorf("vote has no proposal %d", vote.ProposalID)
return true
}
com, found := k.GetCommittee(ctx, proposal.CommitteeID)
if !found {
validationErr = fmt.Errorf("vote's proposal has no committee %d", proposal.CommitteeID)
return true
}
if !com.HasMember(vote.Voter) {
validationErr = fmt.Errorf("voter is not a member of committee %+v", com)
return true
}
return false
})
broken := validationErr != nil
invariantMessage := sdk.FormatInvariant(
types.ModuleName,
"valid votes",
fmt.Sprintf(
"\tfound invalid vote, reason: %s\n"+
"\tvote:\n\t%+v\n",
validationErr, invalidVote),
)
return invariantMessage, broken
}
}

View File

@ -16,22 +16,27 @@ type Keeper struct {
cdc *codec.Codec cdc *codec.Codec
storeKey sdk.StoreKey storeKey sdk.StoreKey
ParamKeeper types.ParamKeeper // TODO ideally don't export, only sims need it exported ParamKeeper types.ParamKeeper // TODO ideally don't export, only sims need it exported
accountKeeper types.AccountKeeper
supplyKeeper types.SupplyKeeper
// Proposal router // Proposal router
router govtypes.Router router govtypes.Router
} }
func NewKeeper(cdc *codec.Codec, storeKey sdk.StoreKey, router govtypes.Router, paramKeeper types.ParamKeeper) Keeper { func NewKeeper(cdc *codec.Codec, storeKey sdk.StoreKey, router govtypes.Router,
paramKeeper types.ParamKeeper, ak types.AccountKeeper, sk types.SupplyKeeper) Keeper {
// Logic in the keeper methods assume the set of gov handlers is fixed. // 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. // So the gov router must be sealed so no handlers can be added or removed after the keeper is created.
router.Seal() router.Seal()
return Keeper{ return Keeper{
cdc: cdc, cdc: cdc,
storeKey: storeKey, storeKey: storeKey,
ParamKeeper: paramKeeper, ParamKeeper: paramKeeper,
router: router, accountKeeper: ak,
supplyKeeper: sk,
router: router,
} }
} }
@ -41,12 +46,12 @@ func NewKeeper(cdc *codec.Codec, storeKey sdk.StoreKey, router govtypes.Router,
// GetCommittee gets a committee from the store. // GetCommittee gets a committee from the store.
func (k Keeper) GetCommittee(ctx sdk.Context, committeeID uint64) (types.Committee, bool) { func (k Keeper) GetCommittee(ctx sdk.Context, committeeID uint64) (types.Committee, bool) {
var committee types.Committee
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.CommitteeKeyPrefix) store := prefix.NewStore(ctx.KVStore(k.storeKey), types.CommitteeKeyPrefix)
bz := store.Get(types.GetKeyFromID(committeeID)) bz := store.Get(types.GetKeyFromID(committeeID))
if bz == nil { if bz == nil {
return types.Committee{}, false return committee, false
} }
var committee types.Committee
k.cdc.MustUnmarshalBinaryBare(bz, &committee) k.cdc.MustUnmarshalBinaryBare(bz, &committee)
return committee, true return committee, true
} }
@ -55,7 +60,7 @@ func (k Keeper) GetCommittee(ctx sdk.Context, committeeID uint64) (types.Committ
func (k Keeper) SetCommittee(ctx sdk.Context, committee types.Committee) { func (k Keeper) SetCommittee(ctx sdk.Context, committee types.Committee) {
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.CommitteeKeyPrefix) store := prefix.NewStore(ctx.KVStore(k.storeKey), types.CommitteeKeyPrefix)
bz := k.cdc.MustMarshalBinaryBare(committee) bz := k.cdc.MustMarshalBinaryBare(committee)
store.Set(types.GetKeyFromID(committee.ID), bz) store.Set(types.GetKeyFromID(committee.GetID()), bz)
} }
// DeleteCommittee removes a committee from the store. // DeleteCommittee removes a committee from the store.

View File

@ -8,6 +8,7 @@ import (
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/gov" "github.com/cosmos/cosmos-sdk/x/gov"
"github.com/cosmos/cosmos-sdk/x/supply"
abci "github.com/tendermint/tendermint/abci/types" abci "github.com/tendermint/tendermint/abci/types"
@ -19,9 +20,10 @@ import (
type KeeperTestSuite struct { type KeeperTestSuite struct {
suite.Suite suite.Suite
keeper keeper.Keeper keeper keeper.Keeper
app app.TestApp supplyKeeper supply.Keeper
ctx sdk.Context app app.TestApp
ctx sdk.Context
addresses []sdk.AccAddress addresses []sdk.AccAddress
} }
@ -29,19 +31,23 @@ type KeeperTestSuite struct {
func (suite *KeeperTestSuite) SetupTest() { func (suite *KeeperTestSuite) SetupTest() {
suite.app = app.NewTestApp() suite.app = app.NewTestApp()
suite.keeper = suite.app.GetCommitteeKeeper() suite.keeper = suite.app.GetCommitteeKeeper()
suite.supplyKeeper = suite.app.GetSupplyKeeper()
suite.ctx = suite.app.NewContext(true, abci.Header{}) suite.ctx = suite.app.NewContext(true, abci.Header{})
_, suite.addresses = app.GeneratePrivKeyAddressPairs(5) _, suite.addresses = app.GeneratePrivKeyAddressPairs(10)
} }
func (suite *KeeperTestSuite) TestGetSetDeleteCommittee() { func (suite *KeeperTestSuite) TestGetSetDeleteCommittee() {
// setup test // setup test
com := types.Committee{ com := types.MemberCommittee{
ID: 12, BaseCommittee: types.BaseCommittee{
Description: "This committee is for testing.", ID: 12,
Members: suite.addresses, Description: "This committee is for testing.",
Permissions: []types.Permission{types.GodPermission{}}, Members: suite.addresses,
VoteThreshold: d("0.667"), Permissions: []types.Permission{types.GodPermission{}},
ProposalDuration: time.Hour * 24 * 7, VoteThreshold: d("0.667"),
ProposalDuration: time.Hour * 24 * 7,
TallyOption: types.FirstPastThePost,
},
} }
// write and read from store // write and read from store

View File

@ -31,7 +31,7 @@ func (k Keeper) SubmitProposal(ctx sdk.Context, proposer sdk.AccAddress, committ
} }
// Get a new ID and store the proposal // Get a new ID and store the proposal
deadline := ctx.BlockTime().Add(com.ProposalDuration) deadline := ctx.BlockTime().Add(com.GetProposalDuration())
proposalID, err := k.StoreNewProposal(ctx, pubProposal, committeeID, deadline) proposalID, err := k.StoreNewProposal(ctx, pubProposal, committeeID, deadline)
if err != nil { if err != nil {
return 0, err return 0, err
@ -40,15 +40,16 @@ func (k Keeper) SubmitProposal(ctx sdk.Context, proposer sdk.AccAddress, committ
ctx.EventManager().EmitEvent( ctx.EventManager().EmitEvent(
sdk.NewEvent( sdk.NewEvent(
types.EventTypeProposalSubmit, types.EventTypeProposalSubmit,
sdk.NewAttribute(types.AttributeKeyCommitteeID, fmt.Sprintf("%d", com.ID)), sdk.NewAttribute(types.AttributeKeyCommitteeID, fmt.Sprintf("%d", com.GetID())),
sdk.NewAttribute(types.AttributeKeyProposalID, fmt.Sprintf("%d", proposalID)), sdk.NewAttribute(types.AttributeKeyProposalID, fmt.Sprintf("%d", proposalID)),
sdk.NewAttribute(types.AttributeKeyDeadline, deadline.String()),
), ),
) )
return proposalID, nil return proposalID, nil
} }
// AddVote submits a vote on a proposal. // AddVote submits a vote on a proposal.
func (k Keeper) AddVote(ctx sdk.Context, proposalID uint64, voter sdk.AccAddress) error { func (k Keeper) AddVote(ctx sdk.Context, proposalID uint64, voter sdk.AccAddress, voteType types.VoteType) error {
// Validate // Validate
pr, found := k.GetProposal(ctx, proposalID) pr, found := k.GetProposal(ctx, proposalID)
if !found { if !found {
@ -62,129 +63,31 @@ func (k Keeper) AddVote(ctx sdk.Context, proposalID uint64, voter sdk.AccAddress
if !found { if !found {
return sdkerrors.Wrapf(types.ErrUnknownCommittee, "%d", pr.CommitteeID) return sdkerrors.Wrapf(types.ErrUnknownCommittee, "%d", pr.CommitteeID)
} }
if !com.HasMember(voter) {
return sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "voter must be a member of committee") if _, ok := com.(types.MemberCommittee); ok {
if !com.HasMember(voter) {
return sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "voter must be a member of committee")
}
if voteType != types.Yes {
return sdkerrors.Wrap(types.ErrInvalidVoteType, "member committees only accept yes votes")
}
} }
// Store vote, overwriting any prior vote // Store vote, overwriting any prior vote
k.SetVote(ctx, types.NewVote(proposalID, voter)) k.SetVote(ctx, types.NewVote(proposalID, voter, voteType))
ctx.EventManager().EmitEvent( ctx.EventManager().EmitEvent(
sdk.NewEvent( sdk.NewEvent(
types.EventTypeProposalVote, types.EventTypeProposalVote,
sdk.NewAttribute(types.AttributeKeyCommitteeID, fmt.Sprintf("%d", com.ID)), sdk.NewAttribute(types.AttributeKeyCommitteeID, fmt.Sprintf("%d", com.GetID())),
sdk.NewAttribute(types.AttributeKeyProposalID, fmt.Sprintf("%d", pr.ID)), sdk.NewAttribute(types.AttributeKeyProposalID, fmt.Sprintf("%d", pr.ID)),
sdk.NewAttribute(types.AttributeKeyVoter, voter.String()), sdk.NewAttribute(types.AttributeKeyVoter, voter.String()),
sdk.NewAttribute(types.AttributeKeyVote, fmt.Sprintf("%d", voteType)),
), ),
) )
return nil 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, proposal types.Proposal) error {
// 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)
if !found {
return sdkerrors.Wrapf(types.ErrUnknownCommittee, "%d", proposal.CommitteeID)
}
if !com.HasPermissionsFor(ctx, k.cdc, k.ParamKeeper, proposal.PubProposal) {
return sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "committee does not have permissions to enact proposal")
}
if err := k.ValidatePubProposal(ctx, proposal.PubProposal); err != nil {
return err
}
// enact the proposal
handler := k.router.GetRoute(proposal.ProposalRoute())
if err := handler(ctx, proposal.PubProposal); err != nil {
// the handler should not error as it was checked in ValidatePubProposal
panic(fmt.Sprintf("unexpected handler error: %s", err))
}
return nil
}
// EnactPassedProposals puts in place the changes proposed in any proposal that has enough votes
func (k Keeper) EnactPassedProposals(ctx sdk.Context) {
k.IterateProposals(ctx, func(proposal types.Proposal) bool {
passes, err := k.GetProposalResult(ctx, proposal.ID)
if err != nil {
panic(err)
}
if !passes {
// continue to next proposal
return false
}
err = k.EnactProposal(ctx, proposal)
outcome := types.AttributeValueProposalPassed
if err != nil {
outcome = types.AttributeValueProposalFailed
}
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, outcome),
),
)
return false
})
}
// 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. // ValidatePubProposal checks if a pubproposal is valid.
func (k Keeper) ValidatePubProposal(ctx sdk.Context, pubProposal types.PubProposal) (returnErr error) { func (k Keeper) ValidatePubProposal(ctx sdk.Context, pubProposal types.PubProposal) (returnErr error) {
if pubProposal == nil { if pubProposal == nil {
@ -217,3 +120,145 @@ func (k Keeper) ValidatePubProposal(ctx sdk.Context, pubProposal types.PubPropos
} }
return nil 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()) {
if committee.GetTallyOption() == types.FirstPastThePost {
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
})
}
func (k Keeper) GetProposalResult(ctx sdk.Context, proposalID uint64, committee types.Committee) bool {
switch com := committee.(type) {
case types.MemberCommittee:
return k.GetMemberCommitteeProposalResult(ctx, proposalID, com)
case types.TokenCommittee:
return k.GetTokenCommitteeProposalResult(ctx, proposalID, com)
default: // Should never hit default case
return false
}
}
// 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
}
// 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)))
}
// GetTokenCommitteeProposalResult gets the result of a token committee proposal
func (k Keeper) GetTokenCommitteeProposalResult(ctx sdk.Context, proposalID uint64, committee types.TokenCommittee) bool {
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
}
// 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,
tallyDenom string) (yesVotes, noVotes, totalVotes, possibleVotes sdk.Dec) {
votes := k.GetVotesByProposal(ctx, proposalID)
yesVotes = sdk.ZeroDec()
noVotes = sdk.ZeroDec()
totalVotes = sdk.ZeroDec()
for _, vote := range votes {
// 1 token = 1 vote
acc := k.accountKeeper.GetAccount(ctx, vote.Voter)
accNumCoins := acc.GetCoins().AmountOf(tallyDenom)
// Add votes to counters
totalVotes = totalVotes.Add(accNumCoins.ToDec())
if vote.VoteType == types.Yes {
yesVotes = yesVotes.Add(accNumCoins.ToDec())
} else if vote.VoteType == types.No {
noVotes = noVotes.Add(accNumCoins.ToDec())
}
}
possibleVotesInt := k.supplyKeeper.GetSupply(ctx).GetTotal().AmountOf(tallyDenom)
return yesVotes, noVotes, totalVotes, possibleVotesInt.ToDec()
}
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 {
// 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)
if !found {
return sdkerrors.Wrapf(types.ErrUnknownCommittee, "%d", proposal.CommitteeID)
}
if !com.HasPermissionsFor(ctx, k.cdc, k.ParamKeeper, proposal.PubProposal) {
return sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "committee does not have permissions to enact proposal")
}
if err := k.ValidatePubProposal(ctx, proposal.PubProposal); err != nil {
return err
}
// enact the proposal
handler := k.router.GetRoute(proposal.ProposalRoute())
if err := handler(ctx, proposal.PubProposal); err != nil {
// the handler should not error as it was checked in ValidatePubProposal
panic(fmt.Sprintf("unexpected handler error: %s", err))
}
return nil
}
// 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) {
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.AttributeKeyProposalOutcome, outcome.String()),
),
)
}

File diff suppressed because it is too large Load Diff

View File

@ -173,13 +173,32 @@ func queryTally(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Ke
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error())
} }
_, found := keeper.GetProposal(ctx, params.ProposalID) proposal, found := keeper.GetProposal(ctx, params.ProposalID)
if !found { if !found {
return nil, sdkerrors.Wrapf(types.ErrUnknownProposal, "%d", params.ProposalID) return nil, sdkerrors.Wrapf(types.ErrUnknownProposal, "%d", params.ProposalID)
} }
numVotes := keeper.TallyVotes(ctx, params.ProposalID)
bz, err := codec.MarshalJSONIndent(keeper.cdc, numVotes) committee, found := keeper.GetCommittee(ctx, proposal.CommitteeID)
if !found {
return nil, sdkerrors.Wrapf(types.ErrUnknownCommittee, "%d", proposal.CommitteeID)
}
var pollingStatus types.ProposalPollingStatus
switch com := committee.(type) {
case types.MemberCommittee:
currVotes := keeper.TallyMemberCommitteeVotes(ctx, params.ProposalID)
possibleVotes := sdk.NewDec(int64(len(com.Members)))
memberPollingStatus := types.NewProposalPollingStatus(params.ProposalID, currVotes,
currVotes, possibleVotes, com.VoteThreshold, sdk.Dec{Int: nil})
pollingStatus = memberPollingStatus
case types.TokenCommittee:
yesVotes, _, currVotes, possibleVotes := keeper.TallyTokenCommitteeVotes(ctx, params.ProposalID, com.TallyDenom)
tokenPollingStatus := types.NewProposalPollingStatus(params.ProposalID, yesVotes,
currVotes, possibleVotes, com.VoteThreshold, com.Quorum)
pollingStatus = tokenPollingStatus
}
bz, err := codec.MarshalJSONIndent(keeper.cdc, pollingStatus)
if err != nil { if err != nil {
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error()) return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
} }

View File

@ -51,20 +51,26 @@ func (suite *QuerierTestSuite) SetupTest() {
suite.testGenesis = types.NewGenesisState( suite.testGenesis = types.NewGenesisState(
3, 3,
[]types.Committee{ []types.Committee{
{ types.MemberCommittee{
ID: 1, BaseCommittee: types.BaseCommittee{
Description: "This committee is for testing.", ID: 1,
Members: suite.addresses[:3], Description: "This committee is for testing.",
Permissions: []types.Permission{types.GodPermission{}}, Members: suite.addresses[:3],
VoteThreshold: d("0.667"), Permissions: []types.Permission{types.GodPermission{}},
ProposalDuration: time.Hour * 24 * 7, VoteThreshold: d("0.667"),
ProposalDuration: time.Hour * 24 * 7,
TallyOption: types.FirstPastThePost,
},
}, },
{ types.MemberCommittee{
ID: 2, BaseCommittee: types.BaseCommittee{
Members: suite.addresses[2:], ID: 2,
Permissions: nil, Members: suite.addresses[2:],
VoteThreshold: d("0.667"), Permissions: nil,
ProposalDuration: time.Hour * 24 * 7, VoteThreshold: d("0.667"),
ProposalDuration: time.Hour * 24 * 7,
TallyOption: types.FirstPastThePost,
},
}, },
}, },
[]types.Proposal{ []types.Proposal{
@ -72,9 +78,9 @@ func (suite *QuerierTestSuite) SetupTest() {
{ID: 2, CommitteeID: 1, PubProposal: gov.NewTextProposal("Another Title", "A description of this other proposal."), Deadline: testTime.Add(21 * 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{ []types.Vote{
{ProposalID: 1, Voter: suite.addresses[0]}, {ProposalID: 1, Voter: suite.addresses[0], VoteType: types.Yes},
{ProposalID: 1, Voter: suite.addresses[1]}, {ProposalID: 1, Voter: suite.addresses[1], VoteType: types.Yes},
{ProposalID: 2, Voter: suite.addresses[2]}, {ProposalID: 2, Voter: suite.addresses[2], VoteType: types.Yes},
}, },
) )
suite.app.InitializeFromGenesisStates( suite.app.InitializeFromGenesisStates(
@ -97,7 +103,7 @@ func (suite *QuerierTestSuite) TestQueryCommittees() {
suite.NotNil(bz) suite.NotNil(bz)
// Unmarshal the bytes // Unmarshal the bytes
var committees []types.Committee var committees types.Committees
suite.NoError(suite.cdc.UnmarshalJSON(bz, &committees)) suite.NoError(suite.cdc.UnmarshalJSON(bz, &committees))
// Check // Check
@ -105,11 +111,11 @@ func (suite *QuerierTestSuite) TestQueryCommittees() {
} }
func (suite *QuerierTestSuite) TestQueryCommittee() { func (suite *QuerierTestSuite) TestQueryCommittee() {
ctx := suite.ctx.WithIsCheckTx(false) // ? ctx := suite.ctx.WithIsCheckTx(false)
// Set up request query // Set up request query
query := abci.RequestQuery{ query := abci.RequestQuery{
Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryCommittee}, "/"), Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryCommittee}, "/"),
Data: suite.cdc.MustMarshalJSON(types.NewQueryCommitteeParams(suite.testGenesis.Committees[0].ID)), Data: suite.cdc.MustMarshalJSON(types.NewQueryCommitteeParams(suite.testGenesis.Committees[0].GetID())),
} }
// Execute query and check the []byte result // Execute query and check the []byte result
@ -154,7 +160,7 @@ func (suite *QuerierTestSuite) TestQueryProposals() {
} }
func (suite *QuerierTestSuite) TestQueryProposal() { func (suite *QuerierTestSuite) TestQueryProposal() {
ctx := suite.ctx.WithIsCheckTx(false) // ? ctx := suite.ctx.WithIsCheckTx(false)
// Set up request query // Set up request query
query := abci.RequestQuery{ query := abci.RequestQuery{
Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryProposal}, "/"), Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryProposal}, "/"),
@ -209,7 +215,7 @@ func (suite *QuerierTestSuite) TestQueryVotes() {
} }
func (suite *QuerierTestSuite) TestQueryVote() { func (suite *QuerierTestSuite) TestQueryVote() {
ctx := suite.ctx.WithIsCheckTx(false) // ? ctx := suite.ctx.WithIsCheckTx(false)
// Set up request query // Set up request query
propID := suite.testGenesis.Proposals[0].ID propID := suite.testGenesis.Proposals[0].ID
query := abci.RequestQuery{ query := abci.RequestQuery{
@ -231,9 +237,21 @@ func (suite *QuerierTestSuite) TestQueryVote() {
} }
func (suite *QuerierTestSuite) TestQueryTally() { func (suite *QuerierTestSuite) TestQueryTally() {
ctx := suite.ctx.WithIsCheckTx(false) // ?
// Set up request query ctx := suite.ctx.WithIsCheckTx(false)
// Expected result
propID := suite.testGenesis.Proposals[0].ID propID := suite.testGenesis.Proposals[0].ID
expectedPollingStatus := types.ProposalPollingStatus{
ProposalID: 1,
YesVotes: sdk.NewDec(int64(len(suite.votes[propID]))),
CurrentVotes: sdk.NewDec(int64(len(suite.votes[propID]))),
PossibleVotes: d("3.0"),
VoteThreshold: d("0.667"),
Quorum: d("0"),
}
// Set up request query
query := abci.RequestQuery{ query := abci.RequestQuery{
Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryTally}, "/"), Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryTally}, "/"),
Data: suite.cdc.MustMarshalJSON(types.NewQueryProposalParams(propID)), Data: suite.cdc.MustMarshalJSON(types.NewQueryProposalParams(propID)),
@ -245,11 +263,9 @@ func (suite *QuerierTestSuite) TestQueryTally() {
suite.NotNil(bz) suite.NotNil(bz)
// Unmarshal the bytes // Unmarshal the bytes
var tally int64 var propPollingStatus types.ProposalPollingStatus
suite.NoError(suite.cdc.UnmarshalJSON(bz, &tally)) suite.NoError(suite.cdc.UnmarshalJSON(bz, &propPollingStatus))
suite.Equal(expectedPollingStatus, propPollingStatus)
// Check
suite.Equal(int64(len(suite.votes[propID])), tally)
} }
type TestSubParam struct { type TestSubParam struct {
@ -269,7 +285,7 @@ func (p *TestParams) ParamSetPairs() params.ParamSetPairs {
} }
} }
func (suite *QuerierTestSuite) TestQueryRawParams() { func (suite *QuerierTestSuite) TestQueryRawParams() {
ctx := suite.ctx.WithIsCheckTx(false) // ? ctx := suite.ctx.WithIsCheckTx(false)
// Create a new param subspace to avoid adding dependency to another module. Set a test param value. // Create a new param subspace to avoid adding dependency to another module. Set a test param value.
subspaceName := "test" subspaceName := "test"
@ -277,9 +293,12 @@ func (suite *QuerierTestSuite) TestQueryRawParams() {
subspace = subspace.WithKeyTable(params.NewKeyTable().RegisterParamSet(&TestParams{})) subspace = subspace.WithKeyTable(params.NewKeyTable().RegisterParamSet(&TestParams{}))
paramValue := TestSubParam{ paramValue := TestSubParam{
Some: "test", Some: "test",
Test: d("1000000000000.000000000000000001"), Test: d("1000000000000.000000000000000001"),
Params: []types.Vote{{1, suite.addresses[0]}, {12, suite.addresses[1]}}, Params: []types.Vote{
types.NewVote(1, suite.addresses[0], types.Yes),
types.NewVote(12, suite.addresses[1], types.Yes),
},
} }
subspace.Set(ctx, []byte(paramKey), paramValue) subspace.Set(ctx, []byte(paramKey), paramValue)

File diff suppressed because it is too large Load Diff

View File

@ -95,9 +95,7 @@ func (AppModule) Name() string {
} }
// RegisterInvariants register module invariants // RegisterInvariants register module invariants
func (am AppModule) RegisterInvariants(ir sdk.InvariantRegistry) { func (AppModule) RegisterInvariants(_ sdk.InvariantRegistry) {}
RegisterInvariants(ir, am.keeper)
}
// Route module message route name // Route module message route name
func (AppModule) Route() string { func (AppModule) Route() string {

View File

@ -4,6 +4,7 @@ import (
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
"github.com/kava-labs/kava/x/committee/types"
) )
func NewProposalHandler(k Keeper) govtypes.Handler { func NewProposalHandler(k Keeper) govtypes.Handler {
@ -26,9 +27,9 @@ func handleCommitteeChangeProposal(ctx sdk.Context, k Keeper, committeeProposal
} }
// Remove all committee's ongoing proposals // Remove all committee's ongoing proposals
proposals := k.GetProposalsByCommittee(ctx, committeeProposal.NewCommittee.ID) proposals := k.GetProposalsByCommittee(ctx, committeeProposal.NewCommittee.GetID())
for _, p := range proposals { for _, p := range proposals {
k.DeleteProposalAndVotes(ctx, p.ID) k.CloseProposal(ctx, p, types.Failed)
} }
// update/create the committee // update/create the committee
@ -44,7 +45,7 @@ func handleCommitteeDeleteProposal(ctx sdk.Context, k Keeper, committeeProposal
// Remove all committee's ongoing proposals // Remove all committee's ongoing proposals
proposals := k.GetProposalsByCommittee(ctx, committeeProposal.CommitteeID) proposals := k.GetProposalsByCommittee(ctx, committeeProposal.CommitteeID)
for _, p := range proposals { for _, p := range proposals {
k.DeleteProposalAndVotes(ctx, p.ID) k.CloseProposal(ctx, p, types.Failed)
} }
k.DeleteCommittee(ctx, committeeProposal.CommitteeID) k.DeleteCommittee(ctx, committeeProposal.CommitteeID)

View File

@ -39,27 +39,33 @@ func (suite *ProposalHandlerTestSuite) SetupTest() {
suite.testGenesis = committee.NewGenesisState( suite.testGenesis = committee.NewGenesisState(
2, 2,
[]committee.Committee{ []committee.Committee{
{ committee.MemberCommittee{
ID: 1, BaseCommittee: committee.BaseCommittee{
Description: "This committee is for testing.", ID: 1,
Members: suite.addresses[:3], Description: "This committee is for testing.",
Permissions: []types.Permission{types.GodPermission{}}, Members: suite.addresses[:3],
VoteThreshold: d("0.667"), Permissions: []types.Permission{types.GodPermission{}},
ProposalDuration: time.Hour * 24 * 7, VoteThreshold: d("0.667"),
ProposalDuration: time.Hour * 24 * 7,
TallyOption: types.FirstPastThePost,
},
}, },
{ committee.MemberCommittee{
ID: 2, BaseCommittee: committee.BaseCommittee{
Members: suite.addresses[2:], ID: 2,
Permissions: nil, Members: suite.addresses[2:],
VoteThreshold: d("0.667"), Permissions: nil,
ProposalDuration: time.Hour * 24 * 7, VoteThreshold: d("0.667"),
ProposalDuration: time.Hour * 24 * 7,
TallyOption: types.FirstPastThePost,
},
}, },
}, },
[]committee.Proposal{ []committee.Proposal{
{ID: 1, CommitteeID: 1, PubProposal: gov.NewTextProposal("A Title", "A description of this proposal."), Deadline: testTime.Add(7 * 24 * time.Hour)}, {ID: 1, CommitteeID: 1, PubProposal: gov.NewTextProposal("A Title", "A description of this proposal."), Deadline: testTime.Add(7 * 24 * time.Hour)},
}, },
[]committee.Vote{ []committee.Vote{
{ProposalID: 1, Voter: suite.addresses[0]}, {ProposalID: 1, Voter: suite.addresses[0], VoteType: types.Yes},
}, },
) )
} }
@ -75,11 +81,14 @@ func (suite *ProposalHandlerTestSuite) TestProposalHandler_ChangeCommittee() {
proposal: committee.NewCommitteeChangeProposal( proposal: committee.NewCommitteeChangeProposal(
"A Title", "A Title",
"A proposal description.", "A proposal description.",
committee.Committee{ committee.MemberCommittee{
ID: 34, BaseCommittee: committee.BaseCommittee{
Members: suite.addresses[:1], ID: 34,
VoteThreshold: d("1"), Members: suite.addresses[:1],
ProposalDuration: time.Hour * 24, VoteThreshold: d("1"),
ProposalDuration: time.Hour * 24,
TallyOption: types.FirstPastThePost,
},
}, },
), ),
expectPass: true, expectPass: true,
@ -89,12 +98,16 @@ func (suite *ProposalHandlerTestSuite) TestProposalHandler_ChangeCommittee() {
proposal: committee.NewCommitteeChangeProposal( proposal: committee.NewCommitteeChangeProposal(
"A Title", "A Title",
"A proposal description.", "A proposal description.",
committee.Committee{ committee.MemberCommittee{
ID: suite.testGenesis.Committees[0].ID, BaseCommittee: committee.BaseCommittee{
Members: suite.addresses, // add new members ID: suite.testGenesis.Committees[0].GetID(),
Permissions: suite.testGenesis.Committees[0].Permissions, Members: suite.addresses, // add new members
VoteThreshold: suite.testGenesis.Committees[0].VoteThreshold, Permissions: suite.testGenesis.Committees[0].GetPermissions(),
ProposalDuration: suite.testGenesis.Committees[0].ProposalDuration, VoteThreshold: suite.testGenesis.Committees[0].GetVoteThreshold(),
ProposalDuration: suite.testGenesis.Committees[0].GetProposalDuration(),
TallyOption: types.FirstPastThePost,
Type: types.MemberCommitteeType,
},
}, },
), ),
expectPass: true, expectPass: true,
@ -113,12 +126,14 @@ func (suite *ProposalHandlerTestSuite) TestProposalHandler_ChangeCommittee() {
proposal: committee.NewCommitteeChangeProposal( proposal: committee.NewCommitteeChangeProposal(
"A Title", "A Title",
"A proposal description.", "A proposal description.",
committee.Committee{ committee.MemberCommittee{
ID: suite.testGenesis.Committees[0].ID, BaseCommittee: committee.BaseCommittee{
Members: append(suite.addresses, suite.addresses[0]), // duplicate address ID: suite.testGenesis.Committees[0].GetID(),
Permissions: suite.testGenesis.Committees[0].Permissions, Members: append(suite.addresses, suite.addresses[0]), // duplicate address
VoteThreshold: suite.testGenesis.Committees[0].VoteThreshold, Permissions: suite.testGenesis.Committees[0].GetPermissions(),
ProposalDuration: suite.testGenesis.Committees[0].ProposalDuration, VoteThreshold: suite.testGenesis.Committees[0].GetVoteThreshold(),
ProposalDuration: suite.testGenesis.Committees[0].GetProposalDuration(),
},
}, },
), ),
expectPass: false, expectPass: false,
@ -135,7 +150,7 @@ func (suite *ProposalHandlerTestSuite) TestProposalHandler_ChangeCommittee() {
suite.ctx = suite.app.NewContext(true, abci.Header{Height: 1, Time: testTime}) suite.ctx = suite.app.NewContext(true, abci.Header{Height: 1, Time: testTime})
handler := committee.NewProposalHandler(suite.keeper) handler := committee.NewProposalHandler(suite.keeper)
oldProposals := suite.keeper.GetProposalsByCommittee(suite.ctx, tc.proposal.NewCommittee.ID) oldProposals := suite.keeper.GetProposalsByCommittee(suite.ctx, tc.proposal.NewCommittee.GetID())
// Run // Run
err := handler(suite.ctx, tc.proposal) err := handler(suite.ctx, tc.proposal)
@ -144,12 +159,12 @@ func (suite *ProposalHandlerTestSuite) TestProposalHandler_ChangeCommittee() {
if tc.expectPass { if tc.expectPass {
suite.NoError(err) suite.NoError(err)
// check committee is accurate // check committee is accurate
actualCom, found := suite.keeper.GetCommittee(suite.ctx, tc.proposal.NewCommittee.ID) actualCom, found := suite.keeper.GetCommittee(suite.ctx, tc.proposal.NewCommittee.GetID())
suite.True(found) suite.True(found)
suite.Equal(tc.proposal.NewCommittee, actualCom) suite.Equal(tc.proposal.NewCommittee, actualCom)
// check proposals and votes for this committee have been removed // check proposals and votes for this committee have been removed
suite.Empty(suite.keeper.GetProposalsByCommittee(suite.ctx, tc.proposal.NewCommittee.ID)) suite.Empty(suite.keeper.GetProposalsByCommittee(suite.ctx, tc.proposal.NewCommittee.GetID()))
for _, p := range oldProposals { for _, p := range oldProposals {
suite.Empty(suite.keeper.GetVotesByProposal(suite.ctx, p.ID)) suite.Empty(suite.keeper.GetVotesByProposal(suite.ctx, p.ID))
} }
@ -172,7 +187,7 @@ func (suite *ProposalHandlerTestSuite) TestProposalHandler_DeleteCommittee() {
proposal: committee.NewCommitteeDeleteProposal( proposal: committee.NewCommitteeDeleteProposal(
"A Title", "A Title",
"A proposal description.", "A proposal description.",
suite.testGenesis.Committees[0].ID, suite.testGenesis.Committees[0].GetID(),
), ),
expectPass: true, expectPass: true,
}, },
@ -181,7 +196,7 @@ func (suite *ProposalHandlerTestSuite) TestProposalHandler_DeleteCommittee() {
proposal: committee.NewCommitteeDeleteProposal( proposal: committee.NewCommitteeDeleteProposal(
"A Title That Is Much Too Long And Really Quite Unreasonable Given That It Is Trying To Fulfill 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 Title That Is Much Too Long And Really Quite Unreasonable Given That It Is Trying To Fulfill 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.", "A proposal description.",
suite.testGenesis.Committees[1].ID, suite.testGenesis.Committees[1].GetID(),
), ),
expectPass: false, expectPass: false,
}, },

View File

@ -26,13 +26,14 @@ func makeTestCodec() (cdc *codec.Codec) {
func TestDecodeStore(t *testing.T) { func TestDecodeStore(t *testing.T) {
cdc := makeTestCodec() cdc := makeTestCodec()
committee := types.NewCommittee( committee := types.NewMemberCommittee(
12, 12,
"This committee is for testing.", "This committee is for testing.",
nil, nil,
[]types.Permission{types.TextPermission{}}, []types.Permission{types.TextPermission{}},
sdk.MustNewDecFromStr("0.667"), sdk.MustNewDecFromStr("0.667"),
time.Hour*24*7, time.Hour*24*7,
types.FirstPastThePost,
) )
proposal := types.Proposal{ proposal := types.Proposal{
ID: 34, ID: 34,

View File

@ -3,6 +3,7 @@ package simulation
import ( import (
"fmt" "fmt"
"math/rand" "math/rand"
"strings"
"time" "time"
"github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/codec"
@ -32,13 +33,14 @@ func RandomizedGenState(simState *module.SimulationState) {
// Without this, proposals can often not be submitted as there aren't any committees with the right set of permissions available. // Without this, proposals can often not be submitted as there aren't any committees with the right set of permissions available.
// It provides more control over how often different proposal types happen during simulation. // It provides more control over how often different proposal types happen during simulation.
// It also makes the code simpler--proposals can just be randomly generated and submitted without having to comply to permissions that happen to be available at the time. // It also makes the code simpler--proposals can just be randomly generated and submitted without having to comply to permissions that happen to be available at the time.
fallbackCommittee := types.NewCommittee( fallbackCommittee := types.NewMemberCommittee(
FallbackCommitteeID, FallbackCommitteeID,
"A committee with god permissions that will always be in state and not deleted. It ensures any generated proposal can always be submitted and passed.", "A committee with god permissions that will always be in state and not deleted. It ensures any generated proposal can always be submitted and passed.",
RandomAddresses(r, simState.Accounts), RandomAddresses(r, simState.Accounts),
[]types.Permission{types.GodPermission{}}, []types.Permission{types.GodPermission{}},
sdk.MustNewDecFromStr("0.5"), sdk.MustNewDecFromStr("0.5"),
AverageBlockTime*10, AverageBlockTime*10,
types.FirstPastThePost,
) )
// Create other committees // Create other committees
@ -66,7 +68,7 @@ func RandomizedGenState(simState *module.SimulationState) {
func RandomCommittee(r *rand.Rand, availableAccs []simulation.Account, allowedParams []types.AllowedParam) (types.Committee, error) { func RandomCommittee(r *rand.Rand, availableAccs []simulation.Account, allowedParams []types.AllowedParam) (types.Committee, error) {
// pick committee members // pick committee members
if len(availableAccs) < 1 { if len(availableAccs) < 1 {
return types.Committee{}, fmt.Errorf("must be ≥ 1 addresses") return types.MemberCommittee{}, fmt.Errorf("must be ≥ 1 addresses")
} }
var members []sdk.AccAddress var members []sdk.AccAddress
for len(members) < 1 { for len(members) < 1 {
@ -76,20 +78,40 @@ func RandomCommittee(r *rand.Rand, availableAccs []simulation.Account, allowedPa
// pick proposal duration // pick proposal duration
dur, err := RandomPositiveDuration(r, 0, AverageBlockTime*10) dur, err := RandomPositiveDuration(r, 0, AverageBlockTime*10)
if err != nil { if err != nil {
return types.Committee{}, err return types.MemberCommittee{}, err
} }
// pick committee vote threshold, must be in interval (0,1] // pick committee vote threshold, must be in interval (0,1]
threshold := simulation.RandomDecAmount(r, sdk.MustNewDecFromStr("1").Sub(sdk.SmallestDec())).Add(sdk.SmallestDec()) threshold := simulation.RandomDecAmount(r, sdk.MustNewDecFromStr("1").Sub(sdk.SmallestDec())).Add(sdk.SmallestDec())
return types.NewCommittee( var committee types.Committee
r.Uint64(), // could collide with other committees, but unlikely if r.Uint64()%2 == 0 {
simulation.RandStringOfLength(r, r.Intn(types.MaxCommitteeDescriptionLength+1)), committee = types.NewMemberCommittee(
members, r.Uint64(), // could collide with other committees, but unlikely
RandomPermissions(r, allowedParams), simulation.RandStringOfLength(r, r.Intn(types.MaxCommitteeDescriptionLength+1)),
threshold, members,
dur, RandomPermissions(r, allowedParams),
), nil threshold,
dur,
types.FirstPastThePost,
)
} else {
tallyDenom := strings.ToLower(simulation.RandStringOfLength(r, (r.Intn(3) + 3)))
committee = types.NewTokenCommittee(
r.Uint64(), // could collide with other committees, but unlikely
simulation.RandStringOfLength(r, r.Intn(types.MaxCommitteeDescriptionLength+1)),
members,
RandomPermissions(r, allowedParams),
threshold,
dur,
types.FirstPastThePost,
sdk.MustNewDecFromStr("0.25"),
tallyDenom,
)
}
return committee, nil
} }
func RandomPermissions(r *rand.Rand, allowedParams []types.AllowedParam) []types.Permission { func RandomPermissions(r *rand.Rand, allowedParams []types.AllowedParam) []types.Permission {

View File

@ -69,7 +69,7 @@ func SimulateMsgSubmitProposal(cdc *codec.Codec, ak AccountKeeper, k keeper.Keep
}) })
// move fallback committee to the end of slice // move fallback committee to the end of slice
for i, c := range committees { for i, c := range committees {
if c.ID == FallbackCommitteeID { if c.GetID() == FallbackCommitteeID {
// switch places with last element // switch places with last element
committees[i], committees[len(committees)-1] = committees[len(committees)-1], committees[i] committees[i], committees[len(committees)-1] = committees[len(committees)-1], committees[i]
} }
@ -94,11 +94,11 @@ func SimulateMsgSubmitProposal(cdc *codec.Codec, ak AccountKeeper, k keeper.Keep
} }
// create the msg and tx // create the msg and tx
proposer := selectedCommittee.Members[r.Intn(len(selectedCommittee.Members))] // won't panic as committees must have ≥ 1 members proposer := selectedCommittee.GetMembers()[r.Intn(len(selectedCommittee.GetMembers()))] // won't panic as committees must have ≥ 1 members
msg := types.NewMsgSubmitProposal( msg := types.NewMsgSubmitProposal(
pp, pp,
proposer, proposer,
selectedCommittee.ID, selectedCommittee.GetID(),
) )
account := ak.GetAccount(ctx, proposer) account := ak.GetAccount(ctx, proposer)
fees, err := simulation.RandomFees(r, ctx, account.SpendableCoins(ctx.BlockTime())) fees, err := simulation.RandomFees(r, ctx, account.SpendableCoins(ctx.BlockTime()))
@ -138,15 +138,15 @@ func SimulateMsgSubmitProposal(cdc *codec.Codec, ak AccountKeeper, k keeper.Keep
// pick the voters // pick the voters
// num voters determined by whether the proposal should pass or not // num voters determined by whether the proposal should pass or not
numMembers := int64(len(selectedCommittee.Members)) numMembers := int64(len(selectedCommittee.GetMembers()))
majority := selectedCommittee.VoteThreshold.Mul(sdk.NewInt(numMembers).ToDec()).Ceil().TruncateInt64() majority := selectedCommittee.GetVoteThreshold().Mul(sdk.NewInt(numMembers).ToDec()).Ceil().TruncateInt64()
numVoters := r.Int63n(majority) // in interval [0, majority) numVoters := r.Int63n(majority) // in interval [0, majority)
shouldPass := r.Float64() < proposalPassPercentage shouldPass := r.Float64() < proposalPassPercentage
if shouldPass { if shouldPass {
numVoters = majority + r.Int63n(numMembers-majority+1) // in interval [majority, numMembers] numVoters = majority + r.Int63n(numMembers-majority+1) // in interval [majority, numMembers]
} }
voters := selectedCommittee.Members[:numVoters] voters := selectedCommittee.GetMembers()[:numVoters]
// schedule vote operations // schedule vote operations
var futureOps []simulation.FutureOperation var futureOps []simulation.FutureOperation
@ -155,9 +155,17 @@ func SimulateMsgSubmitProposal(cdc *codec.Codec, ak AccountKeeper, k keeper.Keep
if err != nil { if err != nil {
return simulation.NoOpMsg(types.ModuleName), nil, fmt.Errorf("random time generation failed: %w", err) return simulation.NoOpMsg(types.ModuleName), nil, fmt.Errorf("random time generation failed: %w", err)
} }
// Valid vote types: 0, 1, 2
randInt, err := RandIntInclusive(r, sdk.ZeroInt(), sdk.NewInt(2))
if err != nil {
return simulation.NoOpMsg(types.ModuleName), nil, fmt.Errorf("random vote type generation failed: %w", err)
}
voteType := types.VoteType(randInt.Int64())
fop := simulation.FutureOperation{ fop := simulation.FutureOperation{
BlockTime: voteTime, BlockTime: voteTime,
Op: SimulateMsgVote(k, ak, v, proposal.ID), Op: SimulateMsgVote(k, ak, v, proposal.ID, voteType),
} }
futureOps = append(futureOps, fop) futureOps = append(futureOps, fop)
} }
@ -166,11 +174,11 @@ func SimulateMsgSubmitProposal(cdc *codec.Codec, ak AccountKeeper, k keeper.Keep
} }
} }
func SimulateMsgVote(k keeper.Keeper, ak AccountKeeper, voter sdk.AccAddress, proposalID uint64) simulation.Operation { func SimulateMsgVote(k keeper.Keeper, ak AccountKeeper, voter sdk.AccAddress, proposalID uint64, voteType types.VoteType) simulation.Operation {
return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simulation.Account, chainID string) ( return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simulation.Account, chainID string) (
opMsg simulation.OperationMsg, fOps []simulation.FutureOperation, err error) { opMsg simulation.OperationMsg, fOps []simulation.FutureOperation, err error) {
msg := types.NewMsgVote(voter, proposalID) msg := types.NewMsgVote(voter, proposalID, voteType)
account := ak.GetAccount(ctx, voter) account := ak.GetAccount(ctx, voter)
fees, err := simulation.RandomFees(r, ctx, account.SpendableCoins(ctx.BlockTime())) fees, err := simulation.RandomFees(r, ctx, account.SpendableCoins(ctx.BlockTime()))

View File

@ -38,7 +38,7 @@ func SimulateCommitteeChangeProposalContent(k keeper.Keeper, paramChanges []simu
// get current committees, ignoring the fallback committee // get current committees, ignoring the fallback committee
var committees []types.Committee var committees []types.Committee
k.IterateCommittees(ctx, func(com types.Committee) bool { k.IterateCommittees(ctx, func(com types.Committee) bool {
if com.ID != FallbackCommitteeID { if com.GetID() != FallbackCommitteeID {
committees = append(committees, com) committees = append(committees, com)
} }
return false return false
@ -85,11 +85,11 @@ func SimulateCommitteeChangeProposalContent(k keeper.Keeper, paramChanges []simu
for len(members) < 1 { for len(members) < 1 {
members = RandomAddresses(r, firstNAccounts(25, accs)) // limit num members to avoid overflowing hardcoded gov ops gas limit members = RandomAddresses(r, firstNAccounts(25, accs)) // limit num members to avoid overflowing hardcoded gov ops gas limit
} }
com.Members = members com.SetMembers(members)
} }
// update permissions // update permissions
if r.Intn(100) < 50 { if r.Intn(100) < 50 {
com.Permissions = RandomPermissions(r, allowedParams) com.SetPermissions(RandomPermissions(r, allowedParams))
} }
// update proposal duration // update proposal duration
if r.Intn(100) < 50 { if r.Intn(100) < 50 {
@ -97,12 +97,12 @@ func SimulateCommitteeChangeProposalContent(k keeper.Keeper, paramChanges []simu
if err != nil { if err != nil {
panic(err) panic(err)
} }
com.ProposalDuration = dur com.SetProposalDuration(dur)
} }
// update vote threshold // update vote threshold
if r.Intn(100) < 50 { if r.Intn(100) < 50 {
// VoteThreshold must be in interval (0,1] // VoteThreshold must be in interval (0,1]
com.VoteThreshold = simulation.RandomDecAmount(r, sdk.MustNewDecFromStr("1").Sub(sdk.SmallestDec())).Add(sdk.SmallestDec()) com.SetVoteThreshold(simulation.RandomDecAmount(r, sdk.MustNewDecFromStr("1").Sub(sdk.SmallestDec())).Add(sdk.SmallestDec()))
} }
content = types.NewCommitteeChangeProposal( content = types.NewCommitteeChangeProposal(
@ -117,7 +117,7 @@ func SimulateCommitteeChangeProposalContent(k keeper.Keeper, paramChanges []simu
content = types.NewCommitteeDeleteProposal( content = types.NewCommitteeDeleteProposal(
simulation.RandStringOfLength(r, 10), simulation.RandStringOfLength(r, 10),
simulation.RandStringOfLength(r, 100), simulation.RandStringOfLength(r, 100),
com.ID, com.GetID(),
) )
} }

View File

@ -34,6 +34,11 @@ func RegisterCodec(cdc *codec.Codec) {
cdc.RegisterConcrete(CommitteeChangeProposal{}, "kava/CommitteeChangeProposal", nil) cdc.RegisterConcrete(CommitteeChangeProposal{}, "kava/CommitteeChangeProposal", nil)
cdc.RegisterConcrete(CommitteeDeleteProposal{}, "kava/CommitteeDeleteProposal", nil) cdc.RegisterConcrete(CommitteeDeleteProposal{}, "kava/CommitteeDeleteProposal", nil)
// Committees
cdc.RegisterInterface((*Committee)(nil), nil)
cdc.RegisterConcrete(MemberCommittee{}, "kava/MemberCommittee", nil)
cdc.RegisterConcrete(TokenCommittee{}, "kava/TokenCommittee", nil)
// Permissions // Permissions
cdc.RegisterInterface((*Permission)(nil), nil) cdc.RegisterInterface((*Permission)(nil), nil)
cdc.RegisterConcrete(GodPermission{}, "kava/GodPermission", nil) cdc.RegisterConcrete(GodPermission{}, "kava/GodPermission", nil)

View File

@ -1,7 +1,9 @@
package types package types
import ( import (
"encoding/json"
"fmt" "fmt"
"strings"
"time" "time"
yaml "gopkg.in/yaml.v2" yaml "gopkg.in/yaml.v2"
@ -13,32 +15,169 @@ import (
const MaxCommitteeDescriptionLength int = 512 const MaxCommitteeDescriptionLength int = 512
// ------------------------------------------ type TallyOption uint64
// Committees
// ------------------------------------------
// A Committee is a collection of addresses that are allowed to vote and enact any governance proposal that passes their permissions. const (
type Committee struct { NullTallyOption TallyOption = iota
FirstPastThePost TallyOption = iota // Votes are tallied each block and the proposal passes as soon as the vote threshold is reached
Deadline TallyOption = iota // Votes are tallied exactly once, when the deadline time is reached
)
const (
MemberCommitteeType = "kava/MemberCommittee" // Committee is composed of member addresses that vote to enact proposals within their permissions
TokenCommitteeType = "kava/TokenCommittee" // Committee is composed of token holders with voting power determined by total token balance
BondDenom = "ukava"
)
func init() {
// CommitteeChange/Delete proposals are registered on gov's ModuleCdc (see proposal.go).
// But since these proposals contain Committees, these types also need registering:
govtypes.ModuleCdc.RegisterInterface((*Committee)(nil), nil)
govtypes.RegisterProposalTypeCodec(MemberCommittee{}, "kava/MemberCommittee")
govtypes.RegisterProposalTypeCodec(TokenCommittee{}, "kava/TokenCommittee")
}
// TallyOptionFromString returns a TallyOption from a string. It returns an error
// if the string is invalid.
func TallyOptionFromString(str string) (TallyOption, error) {
switch strings.ToLower(str) {
case "firstpastthepost", "fptp":
return FirstPastThePost, nil
case "deadline", "d":
return Deadline, nil
default:
return TallyOption(0xff), fmt.Errorf("'%s' is not a valid tally option", str)
}
}
// Marshal needed for protobuf compatibility.
func (t TallyOption) Marshal() ([]byte, error) {
return []byte{byte(t)}, nil
}
// Unmarshal needed for protobuf compatibility.
func (t *TallyOption) Unmarshal(data []byte) error {
*t = TallyOption(data[0])
return nil
}
// Marshals to JSON using string.
func (t TallyOption) MarshalJSON() ([]byte, error) {
return json.Marshal(t.String())
}
// UnmarshalJSON decodes from JSON assuming Bech32 encoding.
func (t *TallyOption) UnmarshalJSON(data []byte) error {
var s string
err := json.Unmarshal(data, &s)
if err != nil {
return err
}
bz2, err := TallyOptionFromString(s)
if err != nil {
return err
}
*t = bz2
return nil
}
// Marshals to YAML using string.
func (t TallyOption) MarshalYAML() ([]byte, error) {
return yaml.Marshal(t.String())
}
// UnmarshalJSON decodes from YAML assuming Bech32 encoding.
func (t *TallyOption) UnmarshalYAML(data []byte) error {
var s string
err := yaml.Unmarshal(data, &s)
if err != nil {
return err
}
bz2, err := TallyOptionFromString(s)
if err != nil {
return err
}
*t = bz2
return nil
}
// String implements the Stringer interface.
func (t TallyOption) String() string {
switch t {
case FirstPastThePost:
return "FirstPastThePost"
case Deadline:
return "Deadline"
default:
return ""
}
}
// Committee is an interface for handling common actions on committees
type Committee interface {
GetID() uint64
GetType() string
GetDescription() string
GetMembers() []sdk.AccAddress
SetMembers([]sdk.AccAddress) BaseCommittee
HasMember(addr sdk.AccAddress) bool
GetPermissions() []Permission
SetPermissions([]Permission) BaseCommittee
HasPermissionsFor(ctx sdk.Context, appCdc *codec.Codec, pk ParamKeeper, proposal PubProposal) bool
GetProposalDuration() time.Duration
SetProposalDuration(time.Duration) BaseCommittee
GetVoteThreshold() sdk.Dec
SetVoteThreshold(sdk.Dec) BaseCommittee
GetTallyOption() TallyOption
Validate() error
}
// Committees is a slice of committees
type Committees []Committee
// BaseCommittee is a common type shared by all Committees
type BaseCommittee struct {
Type string `json:"type" yaml:"type"`
ID uint64 `json:"id" yaml:"id"` ID uint64 `json:"id" yaml:"id"`
Description string `json:"description" yaml:"description"` Description string `json:"description" yaml:"description"`
Members []sdk.AccAddress `json:"members" yaml:"members"` Members []sdk.AccAddress `json:"members" yaml:"members"`
Permissions []Permission `json:"permissions" yaml:"permissions"` 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. VoteThreshold sdk.Dec `json:"vote_threshold" yaml:"vote_threshold"` // Smallest percentage 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. 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.
TallyOption TallyOption `json:"tally_option" yaml:"tally_option"`
} }
func NewCommittee(id uint64, description string, members []sdk.AccAddress, permissions []Permission, threshold sdk.Dec, duration time.Duration) Committee { // GetType is a getter for committee type
return Committee{ func (c BaseCommittee) GetType() string { return c.Type }
ID: id,
Description: description, // GetID is a getter for committee ID
Members: members, func (c BaseCommittee) GetID() uint64 { return c.ID }
Permissions: permissions,
VoteThreshold: threshold, // GetDescription is a getter for committee description
ProposalDuration: duration, func (c BaseCommittee) GetDescription() string { return c.Description }
}
// GetMembers is a getter for committee members
func (c BaseCommittee) GetMembers() []sdk.AccAddress { return c.Members }
// SetMembers is a setter for committee members
func (c BaseCommittee) SetMembers(members []sdk.AccAddress) BaseCommittee {
c.Members = members
return c
} }
func (c Committee) HasMember(addr sdk.AccAddress) bool { // HasMember returns if a committee contains a given member address
func (c BaseCommittee) HasMember(addr sdk.AccAddress) bool {
for _, m := range c.Members { for _, m := range c.Members {
if m.Equals(addr) { if m.Equals(addr) {
return true return true
@ -47,9 +186,18 @@ func (c Committee) HasMember(addr sdk.AccAddress) bool {
return false return false
} }
// GetPermissions is a getter for committee permissions
func (c BaseCommittee) GetPermissions() []Permission { return c.Permissions }
// SetPermissions is a setter for committee permissions
func (c BaseCommittee) SetPermissions(permissions []Permission) BaseCommittee {
c.Permissions = permissions
return c
}
// HasPermissionsFor returns whether the committee is authorized to enact a proposal. // 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. // As long as one permission allows the proposal then it goes through. Its the OR of all permissions.
func (c Committee) HasPermissionsFor(ctx sdk.Context, appCdc *codec.Codec, pk ParamKeeper, proposal PubProposal) bool { func (c BaseCommittee) HasPermissionsFor(ctx sdk.Context, appCdc *codec.Codec, pk ParamKeeper, proposal PubProposal) bool {
for _, p := range c.Permissions { for _, p := range c.Permissions {
if p.Allows(ctx, appCdc, pk, proposal) { if p.Allows(ctx, appCdc, pk, proposal) {
return true return true
@ -58,13 +206,42 @@ func (c Committee) HasPermissionsFor(ctx sdk.Context, appCdc *codec.Codec, pk Pa
return false return false
} }
func (c Committee) Validate() error { // GetVoteThreshold is a getter for committee VoteThreshold
func (c BaseCommittee) GetVoteThreshold() sdk.Dec { return c.VoteThreshold }
// SetVoteThreshold is a setter for committee VoteThreshold
func (c BaseCommittee) SetVoteThreshold(voteThreshold sdk.Dec) BaseCommittee {
c.VoteThreshold = voteThreshold
return c
}
// GetProposalDuration is a getter for committee ProposalDuration
func (c BaseCommittee) GetProposalDuration() time.Duration { return c.ProposalDuration }
// SetProposalDuration is a setter for committee ProposalDuration
func (c BaseCommittee) SetProposalDuration(proposalDuration time.Duration) BaseCommittee {
c.ProposalDuration = proposalDuration
return c
}
// GetTallyOption is a getter for committee TallyOption
func (c BaseCommittee) GetTallyOption() TallyOption { return c.TallyOption }
// Validate validates BaseCommittee fields
func (c BaseCommittee) Validate() error {
if len(c.Description) > MaxCommitteeDescriptionLength {
return fmt.Errorf("description length %d longer than max allowed %d", len(c.Description), MaxCommitteeDescriptionLength)
}
if len(c.Members) <= 0 {
return fmt.Errorf("committee must have members")
}
addressMap := make(map[string]bool, len(c.Members)) addressMap := make(map[string]bool, len(c.Members))
for _, m := range c.Members { for _, m := range c.Members {
// check there are no duplicate members // check there are no duplicate members
if _, ok := addressMap[m.String()]; ok { if _, ok := addressMap[m.String()]; ok {
return fmt.Errorf("committe cannot have duplicate members, %s", m) return fmt.Errorf("committee cannot have duplicate members, %s", m)
} }
// check for valid addresses // check for valid addresses
if m.Empty() { if m.Empty() {
@ -73,32 +250,138 @@ func (c Committee) Validate() error {
addressMap[m.String()] = true addressMap[m.String()] = true
} }
if len(c.Members) == 0 {
return fmt.Errorf("committee cannot have zero members")
}
if len(c.Description) > MaxCommitteeDescriptionLength {
return fmt.Errorf("description length %d longer than max allowed %d", len(c.Description), MaxCommitteeDescriptionLength)
}
for _, p := range c.Permissions { for _, p := range c.Permissions {
if p == nil { if p == nil {
return fmt.Errorf("committee cannot have a nil permission") return fmt.Errorf("committee cannot have a nil permission")
} }
} }
// 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: %s", c.VoteThreshold)
}
if c.ProposalDuration < 0 { if c.ProposalDuration < 0 {
return fmt.Errorf("invalid proposal duration: %s", c.ProposalDuration) return fmt.Errorf("invalid proposal duration: %s", c.ProposalDuration)
} }
// 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: %s", c.VoteThreshold)
}
if c.TallyOption <= 0 || c.TallyOption > 2 {
return fmt.Errorf("invalid tally option: %d", c.TallyOption)
}
return nil return nil
} }
// String implements fmt.Stringer
func (c BaseCommittee) String() string {
return fmt.Sprintf(`Committee %d:
Type: %s
Description: %s
Members: %s
Permissions: %s
VoteThreshold: %s
ProposalDuration: %s
TallyOption: %s`,
c.ID, c.Type, c.Description, c.GetMembers(), c.Permissions,
c.VoteThreshold.String(), c.ProposalDuration.String(),
c.TallyOption.String(),
)
}
// MemberCommittee is an alias of BaseCommittee
type MemberCommittee struct {
BaseCommittee `json:"base_committee" yaml:"base_committee"`
}
// NewMemberCommittee instantiates a new instance of MemberCommittee
func NewMemberCommittee(id uint64, description string, members []sdk.AccAddress, permissions []Permission,
threshold sdk.Dec, duration time.Duration, tallyOption TallyOption) MemberCommittee {
return MemberCommittee{
BaseCommittee: BaseCommittee{
Type: MemberCommitteeType,
ID: id,
Description: description,
Members: members,
Permissions: permissions,
VoteThreshold: threshold,
ProposalDuration: duration,
TallyOption: tallyOption,
},
}
}
// Validate validates the committee's fields
func (c MemberCommittee) Validate() error {
return c.BaseCommittee.Validate()
}
// TokenCommittee supports voting on proposals by token holders
type TokenCommittee struct {
BaseCommittee `json:"base_committee" yaml:"base_committee"`
Quorum sdk.Dec `json:"quorum" yaml:"quorum"`
TallyDenom string `json:"tally_denom" yaml:"tally_denom"`
}
// NewTokenCommittee instantiates a new instance of TokenCommittee
func NewTokenCommittee(id uint64, description string, members []sdk.AccAddress, permissions []Permission,
threshold sdk.Dec, duration time.Duration, tallyOption TallyOption, quorum sdk.Dec, tallyDenom string) TokenCommittee {
return TokenCommittee{
BaseCommittee: BaseCommittee{
Type: TokenCommitteeType,
ID: id,
Description: description,
Members: members,
Permissions: permissions,
VoteThreshold: threshold,
ProposalDuration: duration,
TallyOption: tallyOption,
},
Quorum: quorum,
TallyDenom: tallyDenom,
}
}
// GetQuorum returns the quorum of the committee
func (c TokenCommittee) GetQuorum() sdk.Dec { return c.Quorum }
// GetTallyDenom returns the tally denom of the committee
func (c TokenCommittee) GetTallyDenom() string { return c.TallyDenom }
// Validate validates the committee's fields
func (c TokenCommittee) Validate() error {
if c.TallyDenom == BondDenom {
return fmt.Errorf("invalid tally denom: %s", c.TallyDenom)
}
err := sdk.ValidateDenom(c.TallyDenom)
if err != nil {
return err
}
if c.Quorum.IsNil() || c.Quorum.IsNegative() || c.Quorum.GT(sdk.NewDec(1)) {
return fmt.Errorf("invalid quorum: %s", c.Quorum)
}
return c.BaseCommittee.Validate()
}
// String implements fmt.Stringer
func (c TokenCommittee) String() string {
return fmt.Sprintf(`Committee %d:
Type: %s
Description: %s
Permissions: %s
VoteThreshold: %s
ProposalDuration: %s
TallyOption: %d
Quorum: %s
TallyDenom: %s`,
c.ID, c.GetType(), c.Description, c.Permissions,
c.VoteThreshold.String(), c.ProposalDuration.String(),
c.TallyOption, c.Quorum, c.TallyDenom,
)
}
// ------------------------------------------ // ------------------------------------------
// Proposals // Proposals
// ------------------------------------------ // ------------------------------------------
@ -144,12 +427,14 @@ func (p Proposal) String() string {
type Vote struct { type Vote struct {
ProposalID uint64 `json:"proposal_id" yaml:"proposal_id"` ProposalID uint64 `json:"proposal_id" yaml:"proposal_id"`
Voter sdk.AccAddress `json:"voter" yaml:"voter"` Voter sdk.AccAddress `json:"voter" yaml:"voter"`
VoteType VoteType `json:"vote_type" yaml:"vote_type"`
} }
func NewVote(proposalID uint64, voter sdk.AccAddress) Vote { func NewVote(proposalID uint64, voter sdk.AccAddress, voteType VoteType) Vote {
return Vote{ return Vote{
ProposalID: proposalID, ProposalID: proposalID,
Voter: voter, Voter: voter,
VoteType: voteType,
} }
} }
@ -157,5 +442,6 @@ func (v Vote) Validate() error {
if v.Voter.Empty() { if v.Voter.Empty() {
return fmt.Errorf("voter address cannot be empty") return fmt.Errorf("voter address cannot be empty")
} }
return nil
return v.VoteType.Validate()
} }

View File

@ -0,0 +1,342 @@
package types
import (
"fmt"
"testing"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/require"
"github.com/tendermint/tendermint/crypto"
)
func TestBaseCommittee(t *testing.T) {
addresses := []sdk.AccAddress{
sdk.AccAddress(crypto.AddressHash([]byte("KavaTest1"))),
sdk.AccAddress(crypto.AddressHash([]byte("KavaTest2"))),
sdk.AccAddress(crypto.AddressHash([]byte("KavaTest3"))),
}
testCases := []struct {
name string
committee BaseCommittee
expectPass bool
}{
{
name: "normal",
committee: BaseCommittee{
ID: 1,
Description: "This base committee is for testing.",
Members: addresses[:3],
Permissions: []Permission{GodPermission{}},
VoteThreshold: d("0.667"),
ProposalDuration: time.Hour * 24 * 7,
TallyOption: FirstPastThePost,
},
expectPass: true,
},
{
name: "description length too long",
committee: BaseCommittee{
ID: 1,
Description: fmt.Sprintln("This base committee has a long description.",
"This base committee has a long description. This base committee has a long description.",
"This base committee has a long description. This base committee has a long description.",
"This base committee has a long description. This base committee has a long description.",
"This base committee has a long description. This base committee has a long description.",
"This base committee has a long description. This base committee has a long description.",
"This base committee has a long description. This base committee has a long description.",
"This base committee has a long description. This base committee has a long description."),
Members: addresses[:3],
Permissions: []Permission{GodPermission{}},
VoteThreshold: d("0.667"),
ProposalDuration: time.Hour * 24 * 7,
TallyOption: FirstPastThePost,
},
expectPass: false,
},
{
name: "no members",
committee: BaseCommittee{
ID: 1,
Description: "This base committee is for testing.",
Members: []sdk.AccAddress{},
Permissions: []Permission{GodPermission{}},
VoteThreshold: d("0.667"),
ProposalDuration: time.Hour * 24 * 7,
TallyOption: FirstPastThePost,
},
expectPass: false,
},
{
name: "duplicate member",
committee: BaseCommittee{
ID: 1,
Description: "This base committee is for testing.",
Members: []sdk.AccAddress{addresses[2], addresses[2]},
Permissions: []Permission{GodPermission{}},
VoteThreshold: d("0.667"),
ProposalDuration: time.Hour * 24 * 7,
TallyOption: FirstPastThePost,
},
expectPass: false,
},
{
name: "nil permissions",
committee: BaseCommittee{
ID: 1,
Description: "This base committee is for testing.",
Members: addresses[:3],
Permissions: []Permission{nil},
VoteThreshold: d("0.667"),
ProposalDuration: time.Hour * 24 * 7,
TallyOption: FirstPastThePost,
},
expectPass: false,
},
{
name: "negative proposal duration",
committee: BaseCommittee{
ID: 1,
Description: "This base committee is for testing.",
Members: addresses[:3],
Permissions: []Permission{GodPermission{}},
VoteThreshold: d("0.667"),
ProposalDuration: time.Hour * 24 * -7,
TallyOption: FirstPastThePost,
},
expectPass: false,
},
{
name: "vote threshold is nil",
committee: BaseCommittee{
ID: 1,
Description: "This base committee is for testing.",
Members: addresses[:3],
Permissions: []Permission{GodPermission{}},
VoteThreshold: sdk.Dec{Int: nil},
ProposalDuration: time.Hour * 24 * 7,
TallyOption: FirstPastThePost,
},
expectPass: false,
},
{
name: "vote threshold is 0",
committee: BaseCommittee{
ID: 1,
Description: "This base committee is for testing.",
Members: addresses[:3],
Permissions: []Permission{GodPermission{}},
VoteThreshold: d("0"),
ProposalDuration: time.Hour * 24 * 7,
TallyOption: FirstPastThePost,
},
expectPass: false,
},
{
name: "vote threshold above 1",
committee: BaseCommittee{
ID: 1,
Description: "This base committee is for testing.",
Members: addresses[:3],
Permissions: []Permission{GodPermission{}},
VoteThreshold: d("1.001"),
ProposalDuration: time.Hour * 24 * 7,
TallyOption: FirstPastThePost,
},
expectPass: false,
},
{
name: "invalid tally option",
committee: BaseCommittee{
ID: 1,
Description: "This base committee is for testing.",
Members: addresses[:3],
Permissions: []Permission{GodPermission{}},
VoteThreshold: d("0.667"),
ProposalDuration: time.Hour * 24 * 7,
TallyOption: NullTallyOption,
},
expectPass: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := tc.committee.Validate()
if tc.expectPass {
require.NoError(t, err)
} else {
require.Error(t, err)
}
})
}
}
// TestMemberCommittee is an alias for BaseCommittee that has 'MemberCommittee' type
func TestMemberCommittee(t *testing.T) {
addresses := []sdk.AccAddress{
sdk.AccAddress(crypto.AddressHash([]byte("KavaTest1"))),
sdk.AccAddress(crypto.AddressHash([]byte("KavaTest2"))),
sdk.AccAddress(crypto.AddressHash([]byte("KavaTest3"))),
}
testCases := []struct {
name string
committee MemberCommittee
expectPass bool
}{
{
name: "normal",
committee: MemberCommittee{
BaseCommittee: BaseCommittee{
ID: 1,
Description: "This member committee is for testing.",
Members: addresses[:3],
Permissions: []Permission{GodPermission{}},
VoteThreshold: d("0.667"),
ProposalDuration: time.Hour * 24 * 7,
TallyOption: FirstPastThePost,
Type: MemberCommitteeType,
},
},
expectPass: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
require.Equal(t, MemberCommitteeType, tc.committee.GetType())
err := tc.committee.Validate()
if tc.expectPass {
require.NoError(t, err)
} else {
require.Error(t, err)
}
})
}
}
// TestTokenCommittee tests unique TokenCommittee functionality
func TestTokenCommittee(t *testing.T) {
addresses := []sdk.AccAddress{
sdk.AccAddress(crypto.AddressHash([]byte("KavaTest1"))),
sdk.AccAddress(crypto.AddressHash([]byte("KavaTest2"))),
sdk.AccAddress(crypto.AddressHash([]byte("KavaTest3"))),
}
testCases := []struct {
name string
committee TokenCommittee
expectPass bool
}{
{
name: "normal",
committee: TokenCommittee{
BaseCommittee: BaseCommittee{
ID: 1,
Description: "This token committee is for testing.",
Members: addresses[:3],
Permissions: []Permission{GodPermission{}},
VoteThreshold: d("0.667"),
ProposalDuration: time.Hour * 24 * 7,
TallyOption: FirstPastThePost,
Type: TokenCommitteeType,
},
Quorum: d("0.4"),
TallyDenom: "hard",
},
expectPass: true,
},
{
name: "nil quorum",
committee: TokenCommittee{
BaseCommittee: BaseCommittee{
ID: 1,
Description: "This token committee is for testing.",
Members: addresses[:3],
Permissions: []Permission{GodPermission{}},
VoteThreshold: d("0.667"),
ProposalDuration: time.Hour * 24 * 7,
TallyOption: FirstPastThePost,
Type: TokenCommitteeType,
},
Quorum: sdk.Dec{Int: nil},
TallyDenom: "hard",
},
expectPass: false,
},
{
name: "negative quorum",
committee: TokenCommittee{
BaseCommittee: BaseCommittee{
ID: 1,
Description: "This token committee is for testing.",
Members: addresses[:3],
Permissions: []Permission{GodPermission{}},
VoteThreshold: d("0.667"),
ProposalDuration: time.Hour * 24 * 7,
TallyOption: FirstPastThePost,
Type: TokenCommitteeType,
},
Quorum: d("-0.1"),
TallyDenom: "hard",
},
expectPass: false,
},
{
name: "quroum greater than 1",
committee: TokenCommittee{
BaseCommittee: BaseCommittee{
ID: 1,
Description: "This token committee is for testing.",
Members: addresses[:3],
Permissions: []Permission{GodPermission{}},
VoteThreshold: d("0.667"),
ProposalDuration: time.Hour * 24 * 7,
TallyOption: FirstPastThePost,
Type: TokenCommitteeType,
},
Quorum: d("1.001"),
TallyDenom: "hard",
},
expectPass: false,
},
{
name: "bond denom as tally denom",
committee: TokenCommittee{
BaseCommittee: BaseCommittee{
ID: 1,
Description: "This token committee is for testing.",
Members: addresses[:3],
Permissions: []Permission{GodPermission{}},
VoteThreshold: d("0.667"),
ProposalDuration: time.Hour * 24 * 7,
TallyOption: FirstPastThePost,
Type: TokenCommitteeType,
},
Quorum: d("0.4"),
TallyDenom: BondDenom,
},
expectPass: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
require.Equal(t, TokenCommitteeType, tc.committee.GetType())
err := tc.committee.Validate()
if tc.expectPass {
require.NoError(t, err)
} else {
require.Error(t, err)
}
})
}
}

View File

@ -14,4 +14,5 @@ var (
ErrInvalidGenesis = sdkerrors.Register(ModuleName, 8, "invalid genesis") ErrInvalidGenesis = sdkerrors.Register(ModuleName, 8, "invalid genesis")
ErrNoProposalHandlerExists = sdkerrors.Register(ModuleName, 9, "pubproposal has no corresponding handler") ErrNoProposalHandlerExists = sdkerrors.Register(ModuleName, 9, "pubproposal has no corresponding handler")
ErrUnknownSubspace = sdkerrors.Register(ModuleName, 10, "subspace not found") ErrUnknownSubspace = sdkerrors.Register(ModuleName, 10, "subspace not found")
ErrInvalidVoteType = sdkerrors.Register(ModuleName, 11, "invalid vote type")
) )

View File

@ -9,9 +9,9 @@ const (
AttributeValueCategory = "committee" AttributeValueCategory = "committee"
AttributeKeyCommitteeID = "committee_id" AttributeKeyCommitteeID = "committee_id"
AttributeKeyProposalID = "proposal_id" AttributeKeyProposalID = "proposal_id"
AttributeKeyDeadline = "deadline"
AttributeKeyProposalCloseStatus = "status" AttributeKeyProposalCloseStatus = "status"
AttributeKeyVoter = "voter" AttributeKeyVoter = "voter"
AttributeValueProposalPassed = "proposal_passed" AttributeKeyVote = "vote"
AttributeValueProposalTimeout = "proposal_timeout" AttributeKeyProposalOutcome = "proposal_outcome"
AttributeValueProposalFailed = "proposal_failed"
) )

View File

@ -1,9 +1,22 @@
package types package types
import ( import (
sdk "github.com/cosmos/cosmos-sdk/types"
authexported "github.com/cosmos/cosmos-sdk/x/auth/exported"
"github.com/cosmos/cosmos-sdk/x/params" "github.com/cosmos/cosmos-sdk/x/params"
supplyexported "github.com/cosmos/cosmos-sdk/x/supply/exported"
) )
type ParamKeeper interface { type ParamKeeper interface {
GetSubspace(string) (params.Subspace, bool) GetSubspace(string) (params.Subspace, bool)
} }
// AccountKeeper defines the expected account keeper (noalias)
type AccountKeeper interface {
GetAccount(ctx sdk.Context, addr sdk.AccAddress) authexported.Account
}
// SupplyKeeper defines the expected supply keeper (noalias)
type SupplyKeeper interface {
GetSupply(ctx sdk.Context) (supply supplyexported.SupplyI)
}

View File

@ -10,14 +10,14 @@ const DefaultNextProposalID uint64 = 1
// GenesisState is state that must be provided at chain genesis. // GenesisState is state that must be provided at chain genesis.
type GenesisState struct { type GenesisState struct {
NextProposalID uint64 `json:"next_proposal_id" yaml:"next_proposal_id"` NextProposalID uint64 `json:"next_proposal_id" yaml:"next_proposal_id"`
Committees []Committee `json:"committees" yaml:"committees"` Committees Committees `json:"committees" yaml:"committees"`
Proposals []Proposal `json:"proposals" yaml:"proposals"` Proposals []Proposal `json:"proposals" yaml:"proposals"`
Votes []Vote `json:"votes" yaml:"votes"` Votes []Vote `json:"votes" yaml:"votes"`
} }
// NewGenesisState returns a new genesis state object for the module. // NewGenesisState returns a new genesis state object for the module.
func NewGenesisState(nextProposalID uint64, committees []Committee, proposals []Proposal, votes []Vote) GenesisState { func NewGenesisState(nextProposalID uint64, committees Committees, proposals []Proposal, votes []Vote) GenesisState {
return GenesisState{ return GenesisState{
NextProposalID: nextProposalID, NextProposalID: nextProposalID,
Committees: committees, Committees: committees,
@ -30,7 +30,7 @@ func NewGenesisState(nextProposalID uint64, committees []Committee, proposals []
func DefaultGenesisState() GenesisState { func DefaultGenesisState() GenesisState {
return NewGenesisState( return NewGenesisState(
DefaultNextProposalID, DefaultNextProposalID,
[]Committee{}, Committees{},
[]Proposal{}, []Proposal{},
[]Vote{}, []Vote{},
) )
@ -54,10 +54,10 @@ func (gs GenesisState) Validate() error {
committeeMap := make(map[uint64]bool, len(gs.Committees)) committeeMap := make(map[uint64]bool, len(gs.Committees))
for _, com := range gs.Committees { for _, com := range gs.Committees {
// check there are no duplicate IDs // check there are no duplicate IDs
if _, ok := committeeMap[com.ID]; ok { if _, ok := committeeMap[com.GetID()]; ok {
return fmt.Errorf("duplicate committee ID found in genesis state; id: %d", com.ID) return fmt.Errorf("duplicate committee ID found in genesis state; id: %d", com.GetID())
} }
committeeMap[com.ID] = true committeeMap[com.GetID()] = true
// validate committee // validate committee
if err := com.Validate(); err != nil { if err := com.Validate(); err != nil {

View File

@ -21,32 +21,55 @@ func TestGenesisState_Validate(t *testing.T) {
sdk.AccAddress(crypto.AddressHash([]byte("KavaTest4"))), sdk.AccAddress(crypto.AddressHash([]byte("KavaTest4"))),
sdk.AccAddress(crypto.AddressHash([]byte("KavaTest5"))), sdk.AccAddress(crypto.AddressHash([]byte("KavaTest5"))),
} }
testGenesis := GenesisState{ testGenesis := GenesisState{
NextProposalID: 2, NextProposalID: 2,
Committees: []Committee{ Committees: Committees{
{ MemberCommittee{
ID: 1, BaseCommittee: BaseCommittee{
Description: "This committee is for testing.", ID: 1,
Members: addresses[:3], Description: "This members committee is for testing.",
Permissions: []Permission{GodPermission{}}, Members: addresses[:3],
VoteThreshold: d("0.667"), Permissions: []Permission{GodPermission{}},
ProposalDuration: time.Hour * 24 * 7, VoteThreshold: d("0.667"),
ProposalDuration: time.Hour * 24 * 7,
Type: MemberCommitteeType,
TallyOption: FirstPastThePost,
},
}, },
{ MemberCommittee{
ID: 2, BaseCommittee: BaseCommittee{
Description: "This committee is also for testing.", ID: 2,
Members: addresses[2:], Description: "This members committee is also for testing.",
Permissions: nil, Members: addresses[:3],
VoteThreshold: d("0.8"), Permissions: nil,
ProposalDuration: time.Hour * 24 * 21, VoteThreshold: d("0.8"),
ProposalDuration: time.Hour * 24 * 21,
Type: MemberCommitteeType,
TallyOption: FirstPastThePost,
},
},
TokenCommittee{
BaseCommittee: BaseCommittee{
ID: 3,
Description: "This token committee is for testing.",
Members: addresses[:3],
Permissions: nil,
VoteThreshold: d("0.8"),
ProposalDuration: time.Hour * 24 * 21,
Type: TokenCommitteeType,
TallyOption: Deadline,
},
Quorum: sdk.MustNewDecFromStr("0.4"),
TallyDenom: "hard",
}, },
}, },
Proposals: []Proposal{ Proposals: []Proposal{
{ID: 1, CommitteeID: 1, PubProposal: govtypes.NewTextProposal("A Title", "A description of this proposal."), Deadline: testTime.Add(7 * 24 * time.Hour)}, {ID: 1, CommitteeID: 1, PubProposal: govtypes.NewTextProposal("A Title", "A description of this proposal."), Deadline: testTime.Add(7 * 24 * time.Hour)},
}, },
Votes: []Vote{ Votes: []Vote{
{ProposalID: 1, Voter: addresses[0]}, {ProposalID: 1, Voter: addresses[0], VoteType: Yes},
{ProposalID: 1, Voter: addresses[1]}, {ProposalID: 1, Voter: addresses[1], VoteType: Yes},
}, },
} }
@ -79,7 +102,7 @@ func TestGenesisState_Validate(t *testing.T) {
name: "invalid committee", name: "invalid committee",
genState: GenesisState{ genState: GenesisState{
NextProposalID: testGenesis.NextProposalID, NextProposalID: testGenesis.NextProposalID,
Committees: append(testGenesis.Committees, Committee{}), Committees: append(testGenesis.Committees, MemberCommittee{}),
Proposals: testGenesis.Proposals, Proposals: testGenesis.Proposals,
Votes: testGenesis.Votes, Votes: testGenesis.Votes,
}, },

View File

@ -1,8 +1,13 @@
package types package types
import ( import (
"encoding/json"
"fmt"
"strings"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
yaml "gopkg.in/yaml.v2"
) )
const ( const (
@ -57,15 +62,119 @@ func (msg MsgSubmitProposal) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{msg.Proposer} return []sdk.AccAddress{msg.Proposer}
} }
type VoteType uint64
const (
NullVoteType VoteType = iota // 0
Yes VoteType = iota // 1
No VoteType = iota // 2
Abstain VoteType = iota // 3
)
// VoteTypeFromString returns a VoteType from a string. It returns an error
// if the string is invalid.
func VoteTypeFromString(str string) (VoteType, error) {
switch strings.ToLower(str) {
case "yes", "y":
return Yes, nil
case "abstain", "a":
return Abstain, nil
case "no", "n":
return No, nil
default:
return VoteType(0xff), fmt.Errorf("'%s' is not a valid vote type", str)
}
}
// Marshal needed for protobuf compatibility.
func (vt VoteType) Marshal() ([]byte, error) {
return []byte{byte(vt)}, nil
}
// Unmarshal needed for protobuf compatibility.
func (vt *VoteType) Unmarshal(data []byte) error {
*vt = VoteType(data[0])
return nil
}
// Marshals to JSON using string.
func (vt VoteType) MarshalJSON() ([]byte, error) {
return json.Marshal(vt.String())
}
// UnmarshalJSON decodes from JSON assuming Bech32 encoding.
func (vt *VoteType) UnmarshalJSON(data []byte) error {
var s string
err := json.Unmarshal(data, &s)
if err != nil {
return err
}
bz2, err := VoteTypeFromString(s)
if err != nil {
return err
}
*vt = bz2
return nil
}
// Marshals to YAML using string.
func (vt VoteType) MarshalYAML() ([]byte, error) {
return yaml.Marshal(vt.String())
}
// UnmarshalJSON decodes from YAML assuming Bech32 encoding.
func (vt *VoteType) UnmarshalYAML(data []byte) error {
var s string
err := yaml.Unmarshal(data, &s)
if err != nil {
return err
}
bz2, err := VoteTypeFromString(s)
if err != nil {
return err
}
*vt = bz2
return nil
}
// String implements the Stringer interface.
func (vt VoteType) String() string {
switch vt {
case Yes:
return "Yes"
case Abstain:
return "Abstain"
case No:
return "No"
default:
return ""
}
}
func (vt VoteType) Validate() error {
if vt <= 0 || vt > 3 {
return fmt.Errorf("invalid vote type: %d", vt)
}
return nil
}
// MsgVote is submitted by committee members to vote on proposals. // MsgVote is submitted by committee members to vote on proposals.
type MsgVote struct { type MsgVote struct {
ProposalID uint64 `json:"proposal_id" yaml:"proposal_id"` ProposalID uint64 `json:"proposal_id" yaml:"proposal_id"`
Voter sdk.AccAddress `json:"voter" yaml:"voter"` Voter sdk.AccAddress `json:"voter" yaml:"voter"`
VoteType VoteType `json:"vote_type" yaml:"vote_type"`
} }
// NewMsgVote creates a message to cast a vote on an active proposal // NewMsgVote creates a message to cast a vote on an active proposal
func NewMsgVote(voter sdk.AccAddress, proposalID uint64) MsgVote { func NewMsgVote(voter sdk.AccAddress, proposalID uint64, voteType VoteType) MsgVote {
return MsgVote{proposalID, voter} return MsgVote{proposalID, voter, voteType}
} }
// Route return the message type used for routing the message. // Route return the message type used for routing the message.
@ -79,7 +188,8 @@ func (msg MsgVote) ValidateBasic() error {
if msg.Voter.Empty() { if msg.Voter.Empty() {
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "voter address cannot be empty") return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "voter address cannot be empty")
} }
return nil
return msg.VoteType.Validate()
} }
// GetSignBytes gets the canonical byte representation of the Msg. // GetSignBytes gets the canonical byte representation of the Msg.

View File

@ -56,12 +56,32 @@ func TestMsgVote_ValidateBasic(t *testing.T) {
}{ }{
{ {
name: "normal", name: "normal",
msg: MsgVote{5, addr}, msg: MsgVote{5, addr, Yes},
expectPass: true, expectPass: true,
}, },
{
name: "No",
msg: MsgVote{5, addr, No},
expectPass: true,
},
{
name: "Abstain",
msg: MsgVote{5, addr, Abstain},
expectPass: true,
},
{
name: "Null vote",
msg: MsgVote{5, addr, NullVoteType},
expectPass: false,
},
{ {
name: "empty address", name: "empty address",
msg: MsgVote{5, nil}, msg: MsgVote{5, nil, Yes},
expectPass: false,
},
{
name: "invalid vote (greater)",
msg: MsgVote{5, addr, 4},
expectPass: false, expectPass: false,
}, },
} }

View File

@ -1,8 +1,11 @@
package types package types
import ( import (
"bytes"
yaml "gopkg.in/yaml.v2" yaml "gopkg.in/yaml.v2"
"github.com/cosmos/cosmos-sdk/codec"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
) )
@ -12,6 +15,61 @@ const (
ProposalTypeCommitteeDelete = "CommitteeDelete" ProposalTypeCommitteeDelete = "CommitteeDelete"
) )
// ProposalOutcome indicates the status of a proposal when it's closed and deleted from the store
type ProposalOutcome uint64
const (
// Passed indicates that the proposal passed and was successfully enacted
Passed ProposalOutcome = iota
// Failed indicates that the proposal failed and was not enacted
Failed
// Invalid indicates that proposal passed but an error occurred when attempting to enact it
Invalid
)
var toString = map[ProposalOutcome]string{
Passed: "Passed",
Failed: "Failed",
Invalid: "Invalid",
}
func (p ProposalOutcome) String() string {
return toString[p]
}
func (p ProposalOutcome) Marshal(cdc *codec.Codec) ([]byte, error) {
x, err := cdc.MarshalJSON(p.String())
if err != nil {
return []byte{}, err
}
return x[1 : len(x)-1], nil
}
func MatchMarshaledOutcome(value []byte, cdc *codec.Codec) (ProposalOutcome, error) {
passed, err := Passed.Marshal(cdc)
if err != nil {
return 0, err
}
if bytes.Compare(passed, value) == 0 {
return Passed, nil
}
failed, err := Failed.Marshal(cdc)
if err != nil {
return 0, err
}
if bytes.Compare(failed, value) == 0 {
return Failed, nil
}
invalid, err := Invalid.Marshal(cdc)
if err != nil {
return 0, err
}
if bytes.Compare(invalid, value) == 0 {
return Invalid, nil
}
return 0, nil
}
// ensure proposal types fulfill the PubProposal interface and the gov Content interface. // ensure proposal types fulfill the PubProposal interface and the gov Content interface.
var _, _ govtypes.Content = CommitteeChangeProposal{}, CommitteeDeleteProposal{} var _, _ govtypes.Content = CommitteeChangeProposal{}, CommitteeDeleteProposal{}
var _, _ PubProposal = CommitteeChangeProposal{}, CommitteeDeleteProposal{} var _, _ PubProposal = CommitteeChangeProposal{}, CommitteeDeleteProposal{}

View File

@ -60,3 +60,24 @@ func NewQueryRawParamsParams(subspace, key string) QueryRawParamsParams {
Key: key, Key: key,
} }
} }
type ProposalPollingStatus struct {
ProposalID uint64 `json:"proposal_id" yaml:"proposal_id"`
YesVotes sdk.Dec `json:"yes_votes" yaml:"yes_votes"`
CurrentVotes sdk.Dec `json:"current_votes" yaml:"current_votes"`
PossibleVotes sdk.Dec `json:"possible_votes" yaml:"possible_votes"`
VoteThreshold sdk.Dec `json:"vote_threshold" yaml:"vote_threshold"`
Quorum sdk.Dec `json:"quorum" yaml:"quorum"`
}
func NewProposalPollingStatus(proposalID uint64, yesVotes, currentVotes, possibleVotes,
voteThreshold, quorum sdk.Dec) ProposalPollingStatus {
return ProposalPollingStatus{
ProposalID: proposalID,
YesVotes: yesVotes,
CurrentVotes: currentVotes,
PossibleVotes: possibleVotes,
VoteThreshold: voteThreshold,
Quorum: quorum,
}
}

View File

@ -222,13 +222,16 @@ func (suite *KeeperTestSuite) SetupWithGenState() {
// Set up a god committee // Set up a god committee
committeeModKeeper := tApp.GetCommitteeKeeper() committeeModKeeper := tApp.GetCommitteeKeeper()
godCommittee := committeetypes.Committee{ godCommittee := committeetypes.MemberCommittee{
ID: 1, BaseCommittee: committeetypes.BaseCommittee{
Description: "This committee is for testing.", ID: 1,
Members: suite.addrs[:2], Description: "This committee is for testing.",
Permissions: []committeetypes.Permission{committeetypes.GodPermission{}}, Members: suite.addrs[:2],
VoteThreshold: d("0.667"), Permissions: []committeetypes.Permission{committeetypes.GodPermission{}},
ProposalDuration: time.Hour * 24 * 7, VoteThreshold: d("0.667"),
ProposalDuration: time.Hour * 24 * 7,
TallyOption: committeetypes.FirstPastThePost,
},
} }
committeeModKeeper.SetCommittee(ctx, godCommittee) committeeModKeeper.SetCommittee(ctx, godCommittee)

View File

@ -741,11 +741,13 @@ func (suite *KeeperTestSuite) TestSynchronizeHardBorrowReward() {
suite.Require().NoError(err) suite.Require().NoError(err)
// 5. Committee votes and passes proposal // 5. Committee votes and passes proposal
err = suite.committeeKeeper.AddVote(suite.ctx, proposalID, committeeMemberOne) err = suite.committeeKeeper.AddVote(suite.ctx, proposalID, committeeMemberOne, committee.Yes)
err = suite.committeeKeeper.AddVote(suite.ctx, proposalID, committeeMemberTwo) err = suite.committeeKeeper.AddVote(suite.ctx, proposalID, committeeMemberTwo, committee.Yes)
// 6. Check proposal passed // 6. Check proposal passed
proposalPasses, err := suite.committeeKeeper.GetProposalResult(suite.ctx, proposalID) com, found := suite.committeeKeeper.GetCommittee(suite.ctx, 1)
suite.Require().True(found)
proposalPasses := suite.committeeKeeper.GetProposalResult(suite.ctx, proposalID, com)
suite.Require().NoError(err) suite.Require().NoError(err)
suite.Require().True(proposalPasses) suite.Require().True(proposalPasses)

View File

@ -741,12 +741,13 @@ func (suite *KeeperTestSuite) TestSynchronizeHardSupplyReward() {
suite.Require().NoError(err) suite.Require().NoError(err)
// 5. Committee votes and passes proposal // 5. Committee votes and passes proposal
err = suite.committeeKeeper.AddVote(suite.ctx, proposalID, committeeMemberOne) err = suite.committeeKeeper.AddVote(suite.ctx, proposalID, committeeMemberOne, committee.Yes)
err = suite.committeeKeeper.AddVote(suite.ctx, proposalID, committeeMemberTwo) err = suite.committeeKeeper.AddVote(suite.ctx, proposalID, committeeMemberTwo, committee.Yes)
// 6. Check proposal passed // 6. Check proposal passed
proposalPasses, err := suite.committeeKeeper.GetProposalResult(suite.ctx, proposalID) com, found := suite.committeeKeeper.GetCommittee(suite.ctx, 1)
suite.Require().NoError(err) suite.Require().True(found)
proposalPasses := suite.committeeKeeper.GetProposalResult(suite.ctx, proposalID, com)
suite.Require().True(proposalPasses) suite.Require().True(proposalPasses)
// 7. Run committee module's begin blocker to enact proposal // 7. Run committee module's begin blocker to enact proposal