mirror of
https://github.com/0glabs/0g-chain.git
synced 2024-12-25 07:45:18 +00:00
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:
parent
4d6f6aab3c
commit
cae7503f7b
@ -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
1
go.mod
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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
193
migrate/v0_15/migrate.go
Normal 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)
|
||||
}
|
56
migrate/v0_15/migrate_test.go
Normal file
56
migrate/v0_15/migrate_test.go
Normal 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))
|
||||
}
|
402
migrate/v0_15/testdata/kava-7-committee-state.json
vendored
Normal file
402
migrate/v0_15/testdata/kava-7-committee-state.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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{
|
||||
ID: 12,
|
||||
Members: suite.addresses[:2],
|
||||
Permissions: []committee.Permission{committee.GodPermission{}},
|
||||
VoteThreshold: d("0.8"),
|
||||
ProposalDuration: time.Hour * 24 * 7,
|
||||
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{
|
||||
ID: 12,
|
||||
Members: suite.addresses[:1],
|
||||
Permissions: []committee.Permission{committee.SoftwareUpgradePermission{}},
|
||||
VoteThreshold: d("1.0"),
|
||||
ProposalDuration: time.Hour * 24 * 7,
|
||||
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))
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -2,6 +2,7 @@ package committee_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
@ -17,12 +18,49 @@ import (
|
||||
type GenesisTestSuite struct {
|
||||
suite.Suite
|
||||
|
||||
app app.TestApp
|
||||
ctx sdk.Context
|
||||
keeper committee.Keeper
|
||||
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() {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -49,13 +49,16 @@ func (suite *HandlerTestSuite) SetupTest() {
|
||||
testGenesis := types.NewGenesisState(
|
||||
3,
|
||||
[]types.Committee{
|
||||
{
|
||||
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,
|
||||
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{},
|
||||
|
@ -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,
|
||||
|
@ -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)}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -16,22 +16,27 @@ type Keeper struct {
|
||||
cdc *codec.Codec
|
||||
storeKey sdk.StoreKey
|
||||
|
||||
ParamKeeper types.ParamKeeper // TODO ideally don't export, only sims need it exported
|
||||
ParamKeeper types.ParamKeeper // TODO ideally don't export, only sims need it exported
|
||||
accountKeeper types.AccountKeeper
|
||||
supplyKeeper types.SupplyKeeper
|
||||
|
||||
// Proposal router
|
||||
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()
|
||||
|
||||
return Keeper{
|
||||
cdc: cdc,
|
||||
storeKey: storeKey,
|
||||
ParamKeeper: paramKeeper,
|
||||
router: 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.
|
||||
|
@ -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"
|
||||
|
||||
@ -19,9 +20,10 @@ import (
|
||||
type KeeperTestSuite struct {
|
||||
suite.Suite
|
||||
|
||||
keeper keeper.Keeper
|
||||
app app.TestApp
|
||||
ctx sdk.Context
|
||||
keeper keeper.Keeper
|
||||
supplyKeeper supply.Keeper
|
||||
app app.TestApp
|
||||
ctx sdk.Context
|
||||
|
||||
addresses []sdk.AccAddress
|
||||
}
|
||||
@ -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{
|
||||
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,
|
||||
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
|
||||
|
@ -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 !com.HasMember(voter) {
|
||||
return sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "voter must be a member of committee")
|
||||
|
||||
if _, ok := com.(types.MemberCommittee); ok {
|
||||
if !com.HasMember(voter) {
|
||||
return sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "voter must be a member of committee")
|
||||
}
|
||||
if voteType != types.Yes {
|
||||
return sdkerrors.Wrap(types.ErrInvalidVoteType, "member committees only accept yes votes")
|
||||
}
|
||||
}
|
||||
|
||||
// Store vote, overwriting any prior vote
|
||||
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
@ -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())
|
||||
}
|
||||
|
@ -51,20 +51,26 @@ func (suite *QuerierTestSuite) SetupTest() {
|
||||
suite.testGenesis = types.NewGenesisState(
|
||||
3,
|
||||
[]types.Committee{
|
||||
{
|
||||
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,
|
||||
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,
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: 2,
|
||||
Members: suite.addresses[2:],
|
||||
Permissions: nil,
|
||||
VoteThreshold: d("0.667"),
|
||||
ProposalDuration: time.Hour * 24 * 7,
|
||||
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"
|
||||
@ -277,9 +293,12 @@ func (suite *QuerierTestSuite) TestQueryRawParams() {
|
||||
subspace = subspace.WithKeyTable(params.NewKeyTable().RegisterParamSet(&TestParams{}))
|
||||
|
||||
paramValue := TestSubParam{
|
||||
Some: "test",
|
||||
Test: d("1000000000000.000000000000000001"),
|
||||
Params: []types.Vote{{1, suite.addresses[0]}, {12, suite.addresses[1]}},
|
||||
Some: "test",
|
||||
Test: d("1000000000000.000000000000000001"),
|
||||
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)
|
||||
|
||||
|
1094
x/committee/legacy/v0_14/types.go
Normal file
1094
x/committee/legacy/v0_14/types.go
Normal file
File diff suppressed because it is too large
Load Diff
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -39,27 +39,33 @@ func (suite *ProposalHandlerTestSuite) SetupTest() {
|
||||
suite.testGenesis = committee.NewGenesisState(
|
||||
2,
|
||||
[]committee.Committee{
|
||||
{
|
||||
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,
|
||||
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,
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: 2,
|
||||
Members: suite.addresses[2:],
|
||||
Permissions: nil,
|
||||
VoteThreshold: d("0.667"),
|
||||
ProposalDuration: time.Hour * 24 * 7,
|
||||
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{
|
||||
ID: 34,
|
||||
Members: suite.addresses[:1],
|
||||
VoteThreshold: d("1"),
|
||||
ProposalDuration: time.Hour * 24,
|
||||
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,
|
||||
Members: suite.addresses, // add new members
|
||||
Permissions: suite.testGenesis.Committees[0].Permissions,
|
||||
VoteThreshold: suite.testGenesis.Committees[0].VoteThreshold,
|
||||
ProposalDuration: suite.testGenesis.Committees[0].ProposalDuration,
|
||||
committee.MemberCommittee{
|
||||
BaseCommittee: committee.BaseCommittee{
|
||||
ID: suite.testGenesis.Committees[0].GetID(),
|
||||
Members: suite.addresses, // add new members
|
||||
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,
|
||||
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,
|
||||
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].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,
|
||||
},
|
||||
|
@ -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,
|
||||
|
@ -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(
|
||||
r.Uint64(), // could collide with other committees, but unlikely
|
||||
simulation.RandStringOfLength(r, r.Intn(types.MaxCommitteeDescriptionLength+1)),
|
||||
members,
|
||||
RandomPermissions(r, allowedParams),
|
||||
threshold,
|
||||
dur,
|
||||
), nil
|
||||
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,
|
||||
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 {
|
||||
|
@ -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()))
|
||||
|
@ -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(),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
}
|
||||
|
342
x/committee/types/committee_test.go
Normal file
342
x/committee/types/committee_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -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")
|
||||
)
|
||||
|
@ -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"
|
||||
)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -10,14 +10,14 @@ 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"`
|
||||
Proposals []Proposal `json:"proposals" yaml:"proposals"`
|
||||
Votes []Vote `json:"votes" yaml:"votes"`
|
||||
NextProposalID uint64 `json:"next_proposal_id" yaml:"next_proposal_id"`
|
||||
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 {
|
||||
|
@ -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{
|
||||
{
|
||||
ID: 1,
|
||||
Description: "This committee is for testing.",
|
||||
Members: addresses[:3],
|
||||
Permissions: []Permission{GodPermission{}},
|
||||
VoteThreshold: d("0.667"),
|
||||
ProposalDuration: time.Hour * 24 * 7,
|
||||
Committees: Committees{
|
||||
MemberCommittee{
|
||||
BaseCommittee: BaseCommittee{
|
||||
ID: 1,
|
||||
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,
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: 2,
|
||||
Description: "This committee is also for testing.",
|
||||
Members: addresses[2:],
|
||||
Permissions: nil,
|
||||
VoteThreshold: d("0.8"),
|
||||
ProposalDuration: time.Hour * 24 * 21,
|
||||
MemberCommittee{
|
||||
BaseCommittee: BaseCommittee{
|
||||
ID: 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,
|
||||
},
|
||||
|
@ -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.
|
||||
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
@ -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{}
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
@ -222,13 +222,16 @@ func (suite *KeeperTestSuite) SetupWithGenState() {
|
||||
|
||||
// Set up a god committee
|
||||
committeeModKeeper := tApp.GetCommitteeKeeper()
|
||||
godCommittee := committeetypes.Committee{
|
||||
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,
|
||||
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)
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user