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],
committeeGovRouter,
app.paramsKeeper,
app.accountKeeper,
app.supplyKeeper,
)
app.kavadistKeeper = kavadist.NewKeeper(
app.cdc,

1
go.mod
View File

@ -8,6 +8,7 @@ require (
github.com/spf13/cobra v1.0.0
github.com/spf13/viper v1.6.3
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/tm-db v0.5.1
gopkg.in/yaml.v2 v2.3.0

View File

@ -19,8 +19,8 @@ import (
"github.com/kava-labs/kava/x/bep3"
v0_14cdp "github.com/kava-labs/kava/x/cdp"
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_14committee "github.com/kava-labs/kava/x/committee/legacy/v0_14"
v0_14hard "github.com/kava-labs/kava/x/hard"
v0_11hard "github.com/kava-labs/kava/x/hard/legacy/v0_11"
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/x/bep3"
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_14committee "github.com/kava-labs/kava/x/committee/legacy/v0_14"
v0_14hard "github.com/kava-labs/kava/x/hard"
v0_11hard "github.com/kava-labs/kava/x/hard/legacy/v0_11"
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.
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.EnactPassedProposals(ctx)
k.CloseExpiredProposals(ctx)
k.ProcessProposals(ctx)
}

View File

@ -17,6 +17,7 @@ import (
"github.com/kava-labs/kava/x/cdp"
cdptypes "github.com/kava-labs/kava/x/cdp/types"
"github.com/kava-labs/kava/x/committee"
"github.com/kava-labs/kava/x/committee/types"
)
type ModuleTestSuite struct {
@ -39,26 +40,28 @@ func (suite *ModuleTestSuite) SetupTest() {
func (suite *ModuleTestSuite) TestBeginBlock_ClosesExpired() {
suite.app.InitializeFromGenesisStates()
normalCom := committee.Committee{
memberCom := committee.MemberCommittee{
BaseCommittee: committee.BaseCommittee{
ID: 12,
Members: suite.addresses[:2],
Permissions: []committee.Permission{committee.GodPermission{}},
VoteThreshold: d("0.8"),
ProposalDuration: time.Hour * 24 * 7,
},
}
suite.keeper.SetCommittee(suite.ctx, normalCom)
suite.keeper.SetCommittee(suite.ctx, memberCom)
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)
oneHrLaterCtx := suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(time.Hour))
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)
// 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() {
committee.BeginBlocker(proposalDurationLaterCtx, abci.RequestBeginBlock{}, suite.keeper)
})
@ -74,13 +77,9 @@ func (suite *ModuleTestSuite) TestBeginBlock_EnactsPassed() {
suite.app.InitializeFromGenesisStates()
// setup committee
normalCom := committee.Committee{
ID: 12,
Members: suite.addresses[:2],
Permissions: []committee.Permission{committee.GodPermission{}},
VoteThreshold: d("0.8"),
ProposalDuration: time.Hour * 24 * 7,
}
normalCom := committee.NewMemberCommittee(12, "committee description", suite.addresses[:2],
[]committee.Permission{committee.GodPermission{}}, d("0.8"), time.Hour*24*7, types.FirstPastThePost)
suite.keeper.SetCommittee(suite.ctx, normalCom)
// setup 2 proposals
@ -109,9 +108,9 @@ func (suite *ModuleTestSuite) TestBeginBlock_EnactsPassed() {
suite.NoError(err)
// 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[1]))
suite.NoError(suite.keeper.AddVote(suite.ctx, id2, 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], types.Yes))
suite.NoError(suite.keeper.AddVote(suite.ctx, id2, suite.addresses[0], types.Yes))
// Run BeginBlocker
suite.NotPanics(func() {
@ -131,16 +130,12 @@ func (suite *ModuleTestSuite) TestBeginBlock_DoesntEnactFailed() {
suite.app.InitializeFromGenesisStates()
// setup committee
normalCom := committee.Committee{
ID: 12,
Members: suite.addresses[:1],
Permissions: []committee.Permission{committee.SoftwareUpgradePermission{}},
VoteThreshold: d("1.0"),
ProposalDuration: time.Hour * 24 * 7,
}
memberCom := committee.NewMemberCommittee(12, "committee description", suite.addresses[:1],
[]committee.Permission{committee.SoftwareUpgradePermission{}}, d("1.0"), time.Hour*24*7, types.FirstPastThePost)
firstBlockTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
ctx := suite.ctx.WithBlockTime(firstBlockTime)
suite.keeper.SetCommittee(ctx, normalCom)
suite.keeper.SetCommittee(ctx, memberCom)
// setup an upgrade 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",
},
)
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)
// 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)
tenSecLaterCtx := ctx.WithBlockTime(ctx.BlockTime().Add(time.Second * 10))
@ -180,16 +175,20 @@ func (suite *ModuleTestSuite) TestBeginBlock_EnactsPassedUpgrade() {
suite.app.InitializeFromGenesisStates()
// setup committee
normalCom := committee.Committee{
memberCom := committee.MemberCommittee{
BaseCommittee: committee.BaseCommittee{
ID: 12,
Members: suite.addresses[:1],
Permissions: []committee.Permission{committee.SoftwareUpgradePermission{}},
VoteThreshold: d("1.0"),
ProposalDuration: time.Hour * 24 * 7,
TallyOption: types.FirstPastThePost,
},
}
firstBlockTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
ctx := suite.ctx.WithBlockTime(firstBlockTime)
suite.keeper.SetCommittee(ctx, normalCom)
suite.keeper.SetCommittee(ctx, memberCom)
// setup an upgrade 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",
},
)
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)
// 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
fiveSecLaterCtx := ctx.WithBlockTime(ctx.BlockTime().Add(time.Second * 5))

View File

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

View File

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

View File

@ -5,6 +5,7 @@ import (
"fmt"
"io/ioutil"
"strconv"
"strings"
"time"
"github.com/spf13/cobra"
@ -100,11 +101,11 @@ For example:
// GetCmdVote returns the command to vote on a proposal.
func GetCmdVote(cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "vote [proposal-id]",
Args: cobra.ExactArgs(1),
Use: "vote [proposal-id] [vote]",
Args: cobra.ExactArgs(2),
Short: "Vote for an active proposal",
Long: "Submit a yes vote for the proposal with id [proposal-id].",
Example: fmt.Sprintf("%s tx %s vote 2", version.ClientName, types.ModuleName),
Long: "Submit a [yes/no/abstain] vote for the proposal with id [proposal-id].",
Example: fmt.Sprintf("%s tx %s vote 2 yes", version.ClientName, types.ModuleName),
RunE: func(cmd *cobra.Command, args []string) error {
inBuf := bufio.NewReader(cmd.InOrStdin())
txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc))
@ -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])
}
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
msg := types.NewMsgVote(from, proposalID)
msg := types.NewMsgVote(from, proposalID, vote)
err = msg.ValidateBasic()
if err != nil {
return err
@ -192,7 +210,7 @@ func MustGetExampleCommitteeChangeProposal(cdc *codec.Codec) string {
exampleChangeProposal := types.NewCommitteeChangeProposal(
"A Title",
"A description of this proposal.",
types.NewCommittee(
types.NewMemberCommittee(
1,
"The description of this committee.",
[]sdk.AccAddress{sdk.AccAddress(crypto.AddressHash([]byte("exampleAddress")))},
@ -203,6 +221,7 @@ func MustGetExampleCommitteeChangeProposal(cdc *codec.Codec) string {
},
sdk.MustNewDecFromStr("0.8"),
time.Hour*24*7,
types.FirstPastThePost,
),
)
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
func MustGetExampleParameterChangeProposal(cdc *codec.Codec) string {
value := fmt.Sprintf("\"%d\"", 1000000000)
exampleParameterChangeProposal := params.NewParameterChangeProposal(
"A Title",
"A description of this proposal.",
[]params.ParamChange{params.NewParamChange("cdp", "SurplusAuctionThreshold", "1000000000")},
[]params.ParamChange{params.NewParamChange("cdp", "SurplusAuctionThreshold", value)},
)
exampleParameterChangeProposalBz, err := cdc.MarshalJSONIndent(exampleParameterChangeProposal, "", " ")
if err != nil {

View File

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

View File

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

View File

@ -3,6 +3,7 @@ package rest
import (
"fmt"
"net/http"
"strings"
"github.com/gorilla/mux"
@ -70,6 +71,7 @@ func postProposalHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
type PostVoteReq struct {
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
Voter sdk.AccAddress `json:"voter" yaml:"voter"`
Vote types.VoteType `json:"vote" yaml:"vote"`
}
func postVoteHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
@ -86,6 +88,28 @@ func postVoteHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
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
var req PostVoteReq
if !rest.ReadRESTReq(w, r, cliCtx.Codec, &req) {
@ -97,7 +121,7 @@ func postVoteHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
}
// Create and return a StdTx
msg := types.NewMsgVote(req.Voter, proposalID)
msg := types.NewMsgVote(req.Voter, proposalID, vote)
if err := msg.ValidateBasic(); err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return

View File

@ -2,6 +2,7 @@ package committee_test
import (
"testing"
"time"
"github.com/stretchr/testify/suite"
@ -20,9 +21,46 @@ type GenesisTestSuite struct {
app app.TestApp
ctx sdk.Context
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 {
name string
genState types.GenesisState
@ -34,7 +72,37 @@ func (suite *GenesisTestSuite) TestGenesis() {
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(
2,
[]types.Committee{},
@ -43,6 +111,26 @@ func (suite *GenesisTestSuite) TestGenesis() {
),
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 {
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) {
err := k.AddVote(ctx, msg.ProposalID, msg.Voter)
err := k.AddVote(ctx, msg.ProposalID, msg.Voter, msg.VoteType)
if err != nil {
return nil, err
}

View File

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

View File

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

View File

@ -3,6 +3,7 @@ package keeper_test
import (
"github.com/cosmos/cosmos-sdk/codec"
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/x/committee"
@ -28,6 +29,11 @@ func getProposalVoteMap(k keeper.Keeper, ctx sdk.Context) map[uint64]([]types.Vo
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.
func NewCommitteeGenesisState(cdc *codec.Codec, gs committee.GenesisState) app.GenesisState {
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

@ -17,12 +17,15 @@ type Keeper struct {
storeKey sdk.StoreKey
ParamKeeper types.ParamKeeper // TODO ideally don't export, only sims need it exported
accountKeeper types.AccountKeeper
supplyKeeper types.SupplyKeeper
// Proposal 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.
// So the gov router must be sealed so no handlers can be added or removed after the keeper is created.
router.Seal()
@ -31,6 +34,8 @@ func NewKeeper(cdc *codec.Codec, storeKey sdk.StoreKey, router govtypes.Router,
cdc: cdc,
storeKey: storeKey,
ParamKeeper: paramKeeper,
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.
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)
bz := store.Get(types.GetKeyFromID(committeeID))
if bz == nil {
return types.Committee{}, false
return committee, false
}
var committee types.Committee
k.cdc.MustUnmarshalBinaryBare(bz, &committee)
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) {
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.CommitteeKeyPrefix)
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.

View File

@ -8,6 +8,7 @@ import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/gov"
"github.com/cosmos/cosmos-sdk/x/supply"
abci "github.com/tendermint/tendermint/abci/types"
@ -20,6 +21,7 @@ type KeeperTestSuite struct {
suite.Suite
keeper keeper.Keeper
supplyKeeper supply.Keeper
app app.TestApp
ctx sdk.Context
@ -29,19 +31,23 @@ type KeeperTestSuite struct {
func (suite *KeeperTestSuite) SetupTest() {
suite.app = app.NewTestApp()
suite.keeper = suite.app.GetCommitteeKeeper()
suite.supplyKeeper = suite.app.GetSupplyKeeper()
suite.ctx = suite.app.NewContext(true, abci.Header{})
_, suite.addresses = app.GeneratePrivKeyAddressPairs(5)
_, suite.addresses = app.GeneratePrivKeyAddressPairs(10)
}
func (suite *KeeperTestSuite) TestGetSetDeleteCommittee() {
// setup test
com := types.Committee{
com := types.MemberCommittee{
BaseCommittee: types.BaseCommittee{
ID: 12,
Description: "This committee is for testing.",
Members: suite.addresses,
Permissions: []types.Permission{types.GodPermission{}},
VoteThreshold: d("0.667"),
ProposalDuration: time.Hour * 24 * 7,
TallyOption: types.FirstPastThePost,
},
}
// 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
deadline := ctx.BlockTime().Add(com.ProposalDuration)
deadline := ctx.BlockTime().Add(com.GetProposalDuration())
proposalID, err := k.StoreNewProposal(ctx, pubProposal, committeeID, deadline)
if err != nil {
return 0, err
@ -40,15 +40,16 @@ func (k Keeper) SubmitProposal(ctx sdk.Context, proposer sdk.AccAddress, committ
ctx.EventManager().EmitEvent(
sdk.NewEvent(
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.AttributeKeyDeadline, deadline.String()),
),
)
return proposalID, nil
}
// 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
pr, found := k.GetProposal(ctx, proposalID)
if !found {
@ -62,129 +63,31 @@ func (k Keeper) AddVote(ctx sdk.Context, proposalID uint64, voter sdk.AccAddress
if !found {
return sdkerrors.Wrapf(types.ErrUnknownCommittee, "%d", pr.CommitteeID)
}
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
k.SetVote(ctx, types.NewVote(proposalID, voter))
k.SetVote(ctx, types.NewVote(proposalID, voter, voteType))
ctx.EventManager().EmitEvent(
sdk.NewEvent(
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.AttributeKeyVoter, voter.String()),
sdk.NewAttribute(types.AttributeKeyVote, fmt.Sprintf("%d", voteType)),
),
)
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.
func (k Keeper) ValidatePubProposal(ctx sdk.Context, pubProposal types.PubProposal) (returnErr error) {
if pubProposal == nil {
@ -217,3 +120,145 @@ func (k Keeper) ValidatePubProposal(ctx sdk.Context, pubProposal types.PubPropos
}
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())
}
_, found := keeper.GetProposal(ctx, params.ProposalID)
proposal, found := keeper.GetProposal(ctx, params.ProposalID)
if !found {
return nil, sdkerrors.Wrapf(types.ErrUnknownProposal, "%d", params.ProposalID)
}
numVotes := keeper.TallyVotes(ctx, params.ProposalID)
bz, err := codec.MarshalJSONIndent(keeper.cdc, numVotes)
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 {
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
}

View File

@ -51,20 +51,26 @@ func (suite *QuerierTestSuite) SetupTest() {
suite.testGenesis = types.NewGenesisState(
3,
[]types.Committee{
{
types.MemberCommittee{
BaseCommittee: types.BaseCommittee{
ID: 1,
Description: "This committee is for testing.",
Members: suite.addresses[:3],
Permissions: []types.Permission{types.GodPermission{}},
VoteThreshold: d("0.667"),
ProposalDuration: time.Hour * 24 * 7,
TallyOption: types.FirstPastThePost,
},
{
},
types.MemberCommittee{
BaseCommittee: types.BaseCommittee{
ID: 2,
Members: suite.addresses[2:],
Permissions: nil,
VoteThreshold: d("0.667"),
ProposalDuration: time.Hour * 24 * 7,
TallyOption: types.FirstPastThePost,
},
},
},
[]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)},
},
[]types.Vote{
{ProposalID: 1, Voter: suite.addresses[0]},
{ProposalID: 1, Voter: suite.addresses[1]},
{ProposalID: 2, Voter: suite.addresses[2]},
{ProposalID: 1, Voter: suite.addresses[0], VoteType: types.Yes},
{ProposalID: 1, Voter: suite.addresses[1], VoteType: types.Yes},
{ProposalID: 2, Voter: suite.addresses[2], VoteType: types.Yes},
},
)
suite.app.InitializeFromGenesisStates(
@ -97,7 +103,7 @@ func (suite *QuerierTestSuite) TestQueryCommittees() {
suite.NotNil(bz)
// Unmarshal the bytes
var committees []types.Committee
var committees types.Committees
suite.NoError(suite.cdc.UnmarshalJSON(bz, &committees))
// Check
@ -105,11 +111,11 @@ func (suite *QuerierTestSuite) TestQueryCommittees() {
}
func (suite *QuerierTestSuite) TestQueryCommittee() {
ctx := suite.ctx.WithIsCheckTx(false) // ?
ctx := suite.ctx.WithIsCheckTx(false)
// Set up request query
query := abci.RequestQuery{
Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryCommittee}, "/"),
Data: suite.cdc.MustMarshalJSON(types.NewQueryCommitteeParams(suite.testGenesis.Committees[0].ID)),
Data: suite.cdc.MustMarshalJSON(types.NewQueryCommitteeParams(suite.testGenesis.Committees[0].GetID())),
}
// Execute query and check the []byte result
@ -154,7 +160,7 @@ func (suite *QuerierTestSuite) TestQueryProposals() {
}
func (suite *QuerierTestSuite) TestQueryProposal() {
ctx := suite.ctx.WithIsCheckTx(false) // ?
ctx := suite.ctx.WithIsCheckTx(false)
// Set up request query
query := abci.RequestQuery{
Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryProposal}, "/"),
@ -209,7 +215,7 @@ func (suite *QuerierTestSuite) TestQueryVotes() {
}
func (suite *QuerierTestSuite) TestQueryVote() {
ctx := suite.ctx.WithIsCheckTx(false) // ?
ctx := suite.ctx.WithIsCheckTx(false)
// Set up request query
propID := suite.testGenesis.Proposals[0].ID
query := abci.RequestQuery{
@ -231,9 +237,21 @@ func (suite *QuerierTestSuite) TestQueryVote() {
}
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
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{
Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryTally}, "/"),
Data: suite.cdc.MustMarshalJSON(types.NewQueryProposalParams(propID)),
@ -245,11 +263,9 @@ func (suite *QuerierTestSuite) TestQueryTally() {
suite.NotNil(bz)
// Unmarshal the bytes
var tally int64
suite.NoError(suite.cdc.UnmarshalJSON(bz, &tally))
// Check
suite.Equal(int64(len(suite.votes[propID])), tally)
var propPollingStatus types.ProposalPollingStatus
suite.NoError(suite.cdc.UnmarshalJSON(bz, &propPollingStatus))
suite.Equal(expectedPollingStatus, propPollingStatus)
}
type TestSubParam struct {
@ -269,7 +285,7 @@ func (p *TestParams) ParamSetPairs() params.ParamSetPairs {
}
}
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.
subspaceName := "test"
@ -279,7 +295,10 @@ func (suite *QuerierTestSuite) TestQueryRawParams() {
paramValue := TestSubParam{
Some: "test",
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)

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

@ -3,6 +3,7 @@ package simulation
import (
"fmt"
"math/rand"
"strings"
"time"
"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.
// 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.
fallbackCommittee := types.NewCommittee(
fallbackCommittee := types.NewMemberCommittee(
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.",
RandomAddresses(r, simState.Accounts),
[]types.Permission{types.GodPermission{}},
sdk.MustNewDecFromStr("0.5"),
AverageBlockTime*10,
types.FirstPastThePost,
)
// 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) {
// pick committee members
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
for len(members) < 1 {
@ -76,20 +78,40 @@ func RandomCommittee(r *rand.Rand, availableAccs []simulation.Account, allowedPa
// pick proposal duration
dur, err := RandomPositiveDuration(r, 0, AverageBlockTime*10)
if err != nil {
return types.Committee{}, err
return types.MemberCommittee{}, err
}
// pick committee vote threshold, must be in interval (0,1]
threshold := simulation.RandomDecAmount(r, sdk.MustNewDecFromStr("1").Sub(sdk.SmallestDec())).Add(sdk.SmallestDec())
return types.NewCommittee(
var committee types.Committee
if r.Uint64()%2 == 0 {
committee = types.NewMemberCommittee(
r.Uint64(), // could collide with other committees, but unlikely
simulation.RandStringOfLength(r, r.Intn(types.MaxCommitteeDescriptionLength+1)),
members,
RandomPermissions(r, allowedParams),
threshold,
dur,
), nil
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 {

View File

@ -69,7 +69,7 @@ func SimulateMsgSubmitProposal(cdc *codec.Codec, ak AccountKeeper, k keeper.Keep
})
// move fallback committee to the end of slice
for i, c := range committees {
if c.ID == FallbackCommitteeID {
if c.GetID() == FallbackCommitteeID {
// switch places with last element
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
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(
pp,
proposer,
selectedCommittee.ID,
selectedCommittee.GetID(),
)
account := ak.GetAccount(ctx, proposer)
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
// num voters determined by whether the proposal should pass or not
numMembers := int64(len(selectedCommittee.Members))
majority := selectedCommittee.VoteThreshold.Mul(sdk.NewInt(numMembers).ToDec()).Ceil().TruncateInt64()
numMembers := int64(len(selectedCommittee.GetMembers()))
majority := selectedCommittee.GetVoteThreshold().Mul(sdk.NewInt(numMembers).ToDec()).Ceil().TruncateInt64()
numVoters := r.Int63n(majority) // in interval [0, majority)
shouldPass := r.Float64() < proposalPassPercentage
if shouldPass {
numVoters = majority + r.Int63n(numMembers-majority+1) // in interval [majority, numMembers]
}
voters := selectedCommittee.Members[:numVoters]
voters := selectedCommittee.GetMembers()[:numVoters]
// schedule vote operations
var futureOps []simulation.FutureOperation
@ -155,9 +155,17 @@ func SimulateMsgSubmitProposal(cdc *codec.Codec, ak AccountKeeper, k keeper.Keep
if err != nil {
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{
BlockTime: voteTime,
Op: SimulateMsgVote(k, ak, v, proposal.ID),
Op: SimulateMsgVote(k, ak, v, proposal.ID, voteType),
}
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) (
opMsg simulation.OperationMsg, fOps []simulation.FutureOperation, err error) {
msg := types.NewMsgVote(voter, proposalID)
msg := types.NewMsgVote(voter, proposalID, voteType)
account := ak.GetAccount(ctx, voter)
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
var committees []types.Committee
k.IterateCommittees(ctx, func(com types.Committee) bool {
if com.ID != FallbackCommitteeID {
if com.GetID() != FallbackCommitteeID {
committees = append(committees, com)
}
return false
@ -85,11 +85,11 @@ func SimulateCommitteeChangeProposalContent(k keeper.Keeper, paramChanges []simu
for len(members) < 1 {
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
if r.Intn(100) < 50 {
com.Permissions = RandomPermissions(r, allowedParams)
com.SetPermissions(RandomPermissions(r, allowedParams))
}
// update proposal duration
if r.Intn(100) < 50 {
@ -97,12 +97,12 @@ func SimulateCommitteeChangeProposalContent(k keeper.Keeper, paramChanges []simu
if err != nil {
panic(err)
}
com.ProposalDuration = dur
com.SetProposalDuration(dur)
}
// update vote threshold
if r.Intn(100) < 50 {
// 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(
@ -117,7 +117,7 @@ func SimulateCommitteeChangeProposalContent(k keeper.Keeper, paramChanges []simu
content = types.NewCommitteeDeleteProposal(
simulation.RandStringOfLength(r, 10),
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(CommitteeDeleteProposal{}, "kava/CommitteeDeleteProposal", nil)
// Committees
cdc.RegisterInterface((*Committee)(nil), nil)
cdc.RegisterConcrete(MemberCommittee{}, "kava/MemberCommittee", nil)
cdc.RegisterConcrete(TokenCommittee{}, "kava/TokenCommittee", nil)
// Permissions
cdc.RegisterInterface((*Permission)(nil), nil)
cdc.RegisterConcrete(GodPermission{}, "kava/GodPermission", nil)

View File

@ -1,7 +1,9 @@
package types
import (
"encoding/json"
"fmt"
"strings"
"time"
yaml "gopkg.in/yaml.v2"
@ -13,32 +15,169 @@ import (
const MaxCommitteeDescriptionLength int = 512
// ------------------------------------------
// Committees
// ------------------------------------------
type TallyOption uint64
// A Committee is a collection of addresses that are allowed to vote and enact any governance proposal that passes their permissions.
type Committee struct {
const (
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"`
Description string `json:"description" yaml:"description"`
Members []sdk.AccAddress `json:"members" yaml:"members"`
Permissions []Permission `json:"permissions" yaml:"permissions"`
VoteThreshold sdk.Dec `json:"vote_threshold" yaml:"vote_threshold"` // Smallest percentage of members that must vote for a proposal to pass.
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.
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 {
return Committee{
ID: id,
Description: description,
Members: members,
Permissions: permissions,
VoteThreshold: threshold,
ProposalDuration: duration,
}
// GetType is a getter for committee type
func (c BaseCommittee) GetType() string { return c.Type }
// GetID is a getter for committee ID
func (c BaseCommittee) GetID() uint64 { return c.ID }
// GetDescription is a getter for committee description
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 {
if m.Equals(addr) {
return true
@ -47,9 +186,18 @@ func (c Committee) HasMember(addr sdk.AccAddress) bool {
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.
// 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 {
if p.Allows(ctx, appCdc, pk, proposal) {
return true
@ -58,13 +206,42 @@ func (c Committee) HasPermissionsFor(ctx sdk.Context, appCdc *codec.Codec, pk Pa
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))
for _, m := range c.Members {
// check there are no duplicate members
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
if m.Empty() {
@ -73,32 +250,138 @@ func (c Committee) Validate() error {
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 {
if p == nil {
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 {
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
}
// 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
// ------------------------------------------
@ -144,12 +427,14 @@ func (p Proposal) String() string {
type Vote struct {
ProposalID uint64 `json:"proposal_id" yaml:"proposal_id"`
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{
ProposalID: proposalID,
Voter: voter,
VoteType: voteType,
}
}
@ -157,5 +442,6 @@ func (v Vote) Validate() error {
if v.Voter.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")
ErrNoProposalHandlerExists = sdkerrors.Register(ModuleName, 9, "pubproposal has no corresponding handler")
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"
AttributeKeyCommitteeID = "committee_id"
AttributeKeyProposalID = "proposal_id"
AttributeKeyDeadline = "deadline"
AttributeKeyProposalCloseStatus = "status"
AttributeKeyVoter = "voter"
AttributeValueProposalPassed = "proposal_passed"
AttributeValueProposalTimeout = "proposal_timeout"
AttributeValueProposalFailed = "proposal_failed"
AttributeKeyVote = "vote"
AttributeKeyProposalOutcome = "proposal_outcome"
)

View File

@ -1,9 +1,22 @@
package types
import (
sdk "github.com/cosmos/cosmos-sdk/types"
authexported "github.com/cosmos/cosmos-sdk/x/auth/exported"
"github.com/cosmos/cosmos-sdk/x/params"
supplyexported "github.com/cosmos/cosmos-sdk/x/supply/exported"
)
type ParamKeeper interface {
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

@ -11,13 +11,13 @@ const DefaultNextProposalID uint64 = 1
// GenesisState is state that must be provided at chain genesis.
type GenesisState struct {
NextProposalID uint64 `json:"next_proposal_id" yaml:"next_proposal_id"`
Committees []Committee `json:"committees" yaml:"committees"`
Committees Committees `json:"committees" yaml:"committees"`
Proposals []Proposal `json:"proposals" yaml:"proposals"`
Votes []Vote `json:"votes" yaml:"votes"`
}
// NewGenesisState returns a new genesis state object for the module.
func NewGenesisState(nextProposalID uint64, committees []Committee, proposals []Proposal, votes []Vote) GenesisState {
func NewGenesisState(nextProposalID uint64, committees Committees, proposals []Proposal, votes []Vote) GenesisState {
return GenesisState{
NextProposalID: nextProposalID,
Committees: committees,
@ -30,7 +30,7 @@ func NewGenesisState(nextProposalID uint64, committees []Committee, proposals []
func DefaultGenesisState() GenesisState {
return NewGenesisState(
DefaultNextProposalID,
[]Committee{},
Committees{},
[]Proposal{},
[]Vote{},
)
@ -54,10 +54,10 @@ func (gs GenesisState) Validate() error {
committeeMap := make(map[uint64]bool, len(gs.Committees))
for _, com := range gs.Committees {
// check there are no duplicate IDs
if _, ok := committeeMap[com.ID]; ok {
return fmt.Errorf("duplicate committee ID found in genesis state; id: %d", com.ID)
if _, ok := committeeMap[com.GetID()]; ok {
return fmt.Errorf("duplicate committee ID found in genesis state; id: %d", com.GetID())
}
committeeMap[com.ID] = true
committeeMap[com.GetID()] = true
// validate committee
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("KavaTest5"))),
}
testGenesis := GenesisState{
NextProposalID: 2,
Committees: []Committee{
{
Committees: Committees{
MemberCommittee{
BaseCommittee: BaseCommittee{
ID: 1,
Description: "This committee is for testing.",
Description: "This members committee is for testing.",
Members: addresses[:3],
Permissions: []Permission{GodPermission{}},
VoteThreshold: d("0.667"),
ProposalDuration: time.Hour * 24 * 7,
Type: MemberCommitteeType,
TallyOption: FirstPastThePost,
},
{
},
MemberCommittee{
BaseCommittee: BaseCommittee{
ID: 2,
Description: "This committee is also for testing.",
Members: addresses[2:],
Description: "This members committee is also for testing.",
Members: addresses[:3],
Permissions: nil,
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{
{ID: 1, CommitteeID: 1, PubProposal: govtypes.NewTextProposal("A Title", "A description of this proposal."), Deadline: testTime.Add(7 * 24 * time.Hour)},
},
Votes: []Vote{
{ProposalID: 1, Voter: addresses[0]},
{ProposalID: 1, Voter: addresses[1]},
{ProposalID: 1, Voter: addresses[0], VoteType: Yes},
{ProposalID: 1, Voter: addresses[1], VoteType: Yes},
},
}
@ -79,7 +102,7 @@ func TestGenesisState_Validate(t *testing.T) {
name: "invalid committee",
genState: GenesisState{
NextProposalID: testGenesis.NextProposalID,
Committees: append(testGenesis.Committees, Committee{}),
Committees: append(testGenesis.Committees, MemberCommittee{}),
Proposals: testGenesis.Proposals,
Votes: testGenesis.Votes,
},

View File

@ -1,8 +1,13 @@
package types
import (
"encoding/json"
"fmt"
"strings"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
yaml "gopkg.in/yaml.v2"
)
const (
@ -57,15 +62,119 @@ func (msg MsgSubmitProposal) GetSigners() []sdk.AccAddress {
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.
type MsgVote struct {
ProposalID uint64 `json:"proposal_id" yaml:"proposal_id"`
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
func NewMsgVote(voter sdk.AccAddress, proposalID uint64) MsgVote {
return MsgVote{proposalID, voter}
func NewMsgVote(voter sdk.AccAddress, proposalID uint64, voteType VoteType) MsgVote {
return MsgVote{proposalID, voter, voteType}
}
// Route return the message type used for routing the message.
@ -79,7 +188,8 @@ func (msg MsgVote) ValidateBasic() error {
if msg.Voter.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.

View File

@ -56,12 +56,32 @@ func TestMsgVote_ValidateBasic(t *testing.T) {
}{
{
name: "normal",
msg: MsgVote{5, addr},
msg: MsgVote{5, addr, Yes},
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",
msg: MsgVote{5, nil},
msg: MsgVote{5, nil, Yes},
expectPass: false,
},
{
name: "invalid vote (greater)",
msg: MsgVote{5, addr, 4},
expectPass: false,
},
}

View File

@ -1,8 +1,11 @@
package types
import (
"bytes"
yaml "gopkg.in/yaml.v2"
"github.com/cosmos/cosmos-sdk/codec"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
)
@ -12,6 +15,61 @@ const (
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.
var _, _ govtypes.Content = CommitteeChangeProposal{}, CommitteeDeleteProposal{}
var _, _ PubProposal = CommitteeChangeProposal{}, CommitteeDeleteProposal{}

View File

@ -60,3 +60,24 @@ func NewQueryRawParamsParams(subspace, key string) QueryRawParamsParams {
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
committeeModKeeper := tApp.GetCommitteeKeeper()
godCommittee := committeetypes.Committee{
godCommittee := committeetypes.MemberCommittee{
BaseCommittee: committeetypes.BaseCommittee{
ID: 1,
Description: "This committee is for testing.",
Members: suite.addrs[:2],
Permissions: []committeetypes.Permission{committeetypes.GodPermission{}},
VoteThreshold: d("0.667"),
ProposalDuration: time.Hour * 24 * 7,
TallyOption: committeetypes.FirstPastThePost,
},
}
committeeModKeeper.SetCommittee(ctx, godCommittee)

View File

@ -741,11 +741,13 @@ func (suite *KeeperTestSuite) TestSynchronizeHardBorrowReward() {
suite.Require().NoError(err)
// 5. Committee votes and passes proposal
err = suite.committeeKeeper.AddVote(suite.ctx, proposalID, committeeMemberOne)
err = suite.committeeKeeper.AddVote(suite.ctx, proposalID, committeeMemberTwo)
err = suite.committeeKeeper.AddVote(suite.ctx, proposalID, committeeMemberOne, committee.Yes)
err = suite.committeeKeeper.AddVote(suite.ctx, proposalID, committeeMemberTwo, committee.Yes)
// 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().True(proposalPasses)

View File

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