Committee migration (#665)

* draft: kava-3 to kava-4 committee migrations

* migration: add busd, xrpb, btcb bep3 and cdp params to stability committee

* add new pricefeed markets to safety committee

* add harvest to committee
This commit is contained in:
Kevin Davis 2020-10-01 15:22:26 -04:00 committed by GitHub
parent 00f2068d1b
commit 8f69dcf960
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 2005 additions and 1 deletions

View File

@ -18,6 +18,8 @@ import (
v0_9bep3 "github.com/kava-labs/kava/x/bep3/legacy/v0_9"
v0_11cdp "github.com/kava-labs/kava/x/cdp"
v0_9cdp "github.com/kava-labs/kava/x/cdp/legacy/v0_9"
v0_11committee "github.com/kava-labs/kava/x/committee"
v0_9committee "github.com/kava-labs/kava/x/committee/legacy/v0_9"
v0_11harvest "github.com/kava-labs/kava/x/harvest"
v0_11incentive "github.com/kava-labs/kava/x/incentive"
v0_9incentive "github.com/kava-labs/kava/x/incentive/legacy/v0_9"
@ -164,6 +166,200 @@ func MigrateBep3(oldGenState v0_9bep3.GenesisState) v0_11bep3.GenesisState {
}
}
// MigrateCommittee migrates from a v0.9 (or v0.10) committee genesis state to a v0.11 committee genesis state
func MigrateCommittee(oldGenState v0_9committee.GenesisState) v0_11committee.GenesisState {
var newCommittees []v0_11committee.Committee
var newStabilityCommittee v0_11committee.Committee
var newSafetyCommittee v0_11committee.Committee
var newProposals []v0_11committee.Proposal
var newVotes []v0_11committee.Vote
for _, committee := range oldGenState.Committees {
if committee.ID == 1 {
newStabilityCommittee.Description = committee.Description
newStabilityCommittee.ID = committee.ID
newStabilityCommittee.Members = committee.Members
newStabilityCommittee.VoteThreshold = committee.VoteThreshold
newStabilityCommittee.ProposalDuration = committee.ProposalDuration
var newStabilityPermissions []v0_11committee.Permission
var newStabilitySubParamPermissions v0_11committee.SubParamChangePermission
for _, permission := range committee.Permissions {
subPermission, ok := permission.(v0_9committee.SubParamChangePermission)
if ok {
oldCollateralParam := subPermission.AllowedCollateralParams[0]
newCollateralParam := v0_11committee.AllowedCollateralParam{
Type: "bnb-a",
Denom: false,
AuctionSize: oldCollateralParam.AuctionSize,
ConversionFactor: oldCollateralParam.ConversionFactor,
DebtLimit: oldCollateralParam.DebtLimit,
LiquidationMarketID: oldCollateralParam.LiquidationMarketID,
SpotMarketID: oldCollateralParam.SpotMarketID,
LiquidationPenalty: oldCollateralParam.LiquidationPenalty,
LiquidationRatio: oldCollateralParam.LiquidationRatio,
Prefix: oldCollateralParam.Prefix,
StabilityFee: oldCollateralParam.StabilityFee,
}
oldDebtParam := subPermission.AllowedDebtParam
newDebtParam := v0_11committee.AllowedDebtParam{
ConversionFactor: oldDebtParam.ConversionFactor,
DebtFloor: oldDebtParam.DebtFloor,
Denom: oldDebtParam.Denom,
ReferenceAsset: oldDebtParam.ReferenceAsset,
SavingsRate: oldDebtParam.SavingsRate,
}
oldAssetParam := subPermission.AllowedAssetParams[0]
newAssetParam := v0_11committee.AllowedAssetParam{
Active: oldAssetParam.Active,
CoinID: oldAssetParam.CoinID,
Denom: oldAssetParam.Denom,
Limit: oldAssetParam.Limit,
MaxSwapAmount: true,
MinBlockLock: true,
}
oldMarketParams := subPermission.AllowedMarkets
var newMarketParams v0_11committee.AllowedMarkets
for _, oldMarketParam := range oldMarketParams {
newMarketParam := v0_11committee.AllowedMarket(oldMarketParam)
newMarketParams = append(newMarketParams, newMarketParam)
}
// add btc, xrp, busd markets to committee
btcMarketParam := v0_11committee.AllowedMarket{
MarketID: "btc:usd",
BaseAsset: false,
QuoteAsset: false,
Oracles: false,
Active: true,
}
btc30MarketParam := v0_11committee.AllowedMarket{
MarketID: "btc:usd:30",
BaseAsset: false,
QuoteAsset: false,
Oracles: false,
Active: true,
}
xrpMarketParam := v0_11committee.AllowedMarket{
MarketID: "xrp:usd",
BaseAsset: false,
QuoteAsset: false,
Oracles: false,
Active: true,
}
xrp30MarketParam := v0_11committee.AllowedMarket{
MarketID: "xrp:usd:30",
BaseAsset: false,
QuoteAsset: false,
Oracles: false,
Active: true,
}
busdMarketParam := v0_11committee.AllowedMarket{
MarketID: "busd:usd",
BaseAsset: false,
QuoteAsset: false,
Oracles: false,
Active: true,
}
busd30MarketParam := v0_11committee.AllowedMarket{
MarketID: "busd:usd:30",
BaseAsset: false,
QuoteAsset: false,
Oracles: false,
Active: true,
}
newMarketParams = append(newMarketParams, btcMarketParam, btc30MarketParam, xrpMarketParam, xrp30MarketParam, busdMarketParam, busd30MarketParam)
oldAllowedParams := subPermission.AllowedParams
var newAllowedParams v0_11committee.AllowedParams
for _, oldAllowedParam := range oldAllowedParams {
newAllowedParam := v0_11committee.AllowedParam(oldAllowedParam)
if oldAllowedParam.Subspace == "bep3" && oldAllowedParam.Key == "SupportedAssets" {
newAllowedParam.Key = "AssetParams"
}
harvestParam := v0_11committee.AllowedParam{Subspace: "harvest", Key: "Active"}
newAllowedParams = append(newAllowedParams, newAllowedParam, harvestParam)
}
// --------------- ADD BUSD, XRP-B, BTC-B BEP3 parameters to Stability Committee Permissions
busdAllowedAssetParam := v0_11committee.AllowedAssetParam{
Active: true,
CoinID: true, // allow busd coinID to be updated in case it gets its own slip-44
Denom: "busd",
Limit: true,
MaxSwapAmount: true,
MinBlockLock: true,
}
xrpbAllowedAssetParam := v0_11committee.AllowedAssetParam{
Active: true,
CoinID: false,
Denom: "xrpb",
Limit: true,
MaxSwapAmount: true,
MinBlockLock: true,
}
btcbAllowedAssetParam := v0_11committee.AllowedAssetParam{
Active: true,
CoinID: false,
Denom: "btcb",
Limit: true,
MaxSwapAmount: true,
MinBlockLock: true,
}
// --------- ADD BTC-B, XRP-B, BUSD(a), BUSD(b) cdp collateral params to stability committee
busdaAllowedCollateralParam := v0_11committee.NewAllowedCollateralParam(
"busd-a", false, false, true, true, true, false, false, false, false, false,
)
busdbAllowedCollateralParam := v0_11committee.NewAllowedCollateralParam(
"busd-b", false, false, true, true, true, false, false, false, false, false,
)
btcbAllowedCollateralParam := v0_11committee.NewAllowedCollateralParam(
"btcb-a", false, false, true, true, true, false, false, false, false, false,
)
xrpbAllowedCollateralParam := v0_11committee.NewAllowedCollateralParam(
"xrpb-a", false, false, true, true, true, false, false, false, false, false,
)
newStabilitySubParamPermissions.AllowedAssetParams = v0_11committee.AllowedAssetParams{
newAssetParam, busdAllowedAssetParam, btcbAllowedAssetParam, xrpbAllowedAssetParam}
newStabilitySubParamPermissions.AllowedCollateralParams = v0_11committee.AllowedCollateralParams{
newCollateralParam, busdaAllowedCollateralParam, busdbAllowedCollateralParam, btcbAllowedCollateralParam, xrpbAllowedCollateralParam}
newStabilitySubParamPermissions.AllowedDebtParam = newDebtParam
newStabilitySubParamPermissions.AllowedMarkets = newMarketParams
newStabilitySubParamPermissions.AllowedParams = newAllowedParams
newStabilityPermissions = append(newStabilityPermissions, newStabilitySubParamPermissions)
}
}
newStabilityPermissions = append(newStabilityPermissions, v0_11committee.TextPermission{})
newStabilityCommittee.Permissions = newStabilityPermissions
newCommittees = append(newCommittees, newStabilityCommittee)
} else {
newSafetyCommittee.ID = committee.ID
newSafetyCommittee.Description = committee.Description
newSafetyCommittee.Members = committee.Members
newSafetyCommittee.Permissions = []v0_11committee.Permission{v0_11committee.SoftwareUpgradePermission{}}
newSafetyCommittee.VoteThreshold = committee.VoteThreshold
newSafetyCommittee.ProposalDuration = committee.ProposalDuration
newCommittees = append(newCommittees, newSafetyCommittee)
}
}
for _, oldProp := range oldGenState.Proposals {
newPubProposal := v0_11committee.PubProposal(oldProp.PubProposal)
newProp := v0_11committee.NewProposal(newPubProposal, oldProp.ID, oldProp.CommitteeID, oldProp.Deadline)
newProposals = append(newProposals, newProp)
}
for _, oldVote := range oldGenState.Votes {
newVote := v0_11committee.NewVote(oldVote.ProposalID, oldVote.Voter)
newVotes = append(newVotes, newVote)
}
return v0_11committee.GenesisState{
NextProposalID: oldGenState.NextProposalID,
Committees: newCommittees,
Proposals: newProposals,
Votes: newVotes,
}
}
// MigrateAuth migrates from a v0.38.5 auth genesis state to a v0.39.1 auth genesis state
func MigrateAuth(oldGenState v38_5auth.GenesisState) v39_1auth.GenesisState {
var newAccounts v39_1authexported.GenesisAccounts

View File

@ -6,9 +6,9 @@ import (
"path/filepath"
"testing"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/stretchr/testify/require"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
v39_1auth "github.com/cosmos/cosmos-sdk/x/auth"
@ -20,6 +20,7 @@ import (
v38_5supply "github.com/kava-labs/kava/migrate/v0_11/legacy/cosmos-sdk/v0.38.5/supply"
v0_9bep3 "github.com/kava-labs/kava/x/bep3/legacy/v0_9"
v0_9cdp "github.com/kava-labs/kava/x/cdp/legacy/v0_9"
v0_9committee "github.com/kava-labs/kava/x/committee/legacy/v0_9"
v0_9incentive "github.com/kava-labs/kava/x/incentive/legacy/v0_9"
v0_9pricefeed "github.com/kava-labs/kava/x/pricefeed/legacy/v0_9"
v0_11validator_vesting "github.com/kava-labs/kava/x/validator-vesting"
@ -48,6 +49,22 @@ func TestMigrateBep3(t *testing.T) {
require.NoError(t, err)
}
func TestMigrateCommittee(t *testing.T) {
bz, err := ioutil.ReadFile(filepath.Join("testdata", "committee-v09.json"))
require.NoError(t, err)
var oldGenState v0_9committee.GenesisState
cdc := codec.New()
sdk.RegisterCodec(cdc)
v0_9committee.RegisterCodec(cdc)
require.NotPanics(t, func() {
cdc.MustUnmarshalJSON(bz, &oldGenState)
})
newGenState := MigrateCommittee(oldGenState)
err = newGenState.Validate()
require.NoError(t, err)
}
func TestMigrateAuth(t *testing.T) {
bz, err := ioutil.ReadFile(filepath.Join("testdata", "auth-v09.json"))
require.NoError(t, err)

View File

@ -0,0 +1,157 @@
{
"committees": [
{
"description": "Kava Stability Committee",
"id": "1",
"members": [
"kava1gru35up50ql2wxhegr880qy6ynl63ujlv8gum2",
"kava1sc3mh3pkas5e7xd269am4xm5mp6zweyzmhjagj",
"kava1c9ye54e3pzwm3e0zpdlel6pnavrj9qqv6e8r4h",
"kava1m7p6sjqrz6mylz776ct48wj6lpnpcd0z82209d",
"kava1a9pmkzk570egv3sflu3uwdf3gejl7qfy9hghzl"
],
"permissions": [
{
"type": "kava/SubParamChangePermission",
"value": {
"allowed_asset_params": [
{
"active": true,
"coin_id": false,
"denom": "bnb",
"limit": true
}
],
"allowed_collateral_params": [
{
"auction_size": true,
"conversion_factor": false,
"debt_limit": true,
"denom": "bnb",
"liquidation_market_id": false,
"liquidation_penalty": false,
"liquidation_ratio": false,
"prefix": false,
"spot_market_id": false,
"stability_fee": true
}
],
"allowed_debt_param": {
"conversion_factor": false,
"debt_floor": true,
"denom": false,
"reference_asset": false,
"savings_rate": true
},
"allowed_markets": [
{
"active": true,
"base_asset": false,
"market_id": "bnb:usd",
"oracles": false,
"quote_asset": false
},
{
"active": true,
"base_asset": false,
"market_id": "bnb:usd:30",
"oracles": false,
"quote_asset": false
}
],
"allowed_params": [
{
"key": "BidDuration",
"subspace": "auction"
},
{
"key": "IncrementSurplus",
"subspace": "auction"
},
{
"key": "IncrementDebt",
"subspace": "auction"
},
{
"key": "IncrementCollateral",
"subspace": "auction"
},
{
"key": "SupportedAssets",
"subspace": "bep3"
},
{
"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": "cdp"
},
{
"key": "Active",
"subspace": "incentive"
},
{
"key": "Active",
"subspace": "kavadist"
},
{
"key": "Markets",
"subspace": "pricefeed"
}
]
}
},
{
"type": "kava/TextPermission",
"value": {}
}
],
"proposal_duration": "604800000000000",
"vote_threshold": "0.500000000000000000"
},
{
"description": "Kava Safety Committee",
"id": "2",
"members": [
"kava1e0agyg6eug9r62fly9sls77ycjgw8ax6xk73es"
],
"permissions": [
{
"type": "kava/SoftwareUpgradePermission",
"value": {}
}
],
"proposal_duration": "604800000000000",
"vote_threshold": "0.500000000000000000"
}
],
"next_proposal_id": "27",
"proposals": [],
"votes": []
}

View File

@ -29,8 +29,10 @@ const (
QuerierRoute = types.QuerierRoute
QueryCommittee = types.QueryCommittee
QueryCommittees = types.QueryCommittees
QueryNextProposalID = types.QueryNextProposalID
QueryProposal = types.QueryProposal
QueryProposals = types.QueryProposals
QueryRawParams = types.QueryRawParams
QueryTally = types.QueryTally
QueryVote = types.QueryVote
QueryVotes = types.QueryVotes
@ -51,6 +53,7 @@ var (
DefaultGenesisState = types.DefaultGenesisState
GetKeyFromID = types.GetKeyFromID
GetVoteKey = types.GetVoteKey
NewAllowedCollateralParam = types.NewAllowedCollateralParam
NewCommittee = types.NewCommittee
NewCommitteeChangeProposal = types.NewCommitteeChangeProposal
NewCommitteeDeleteProposal = types.NewCommitteeDeleteProposal
@ -60,6 +63,7 @@ var (
NewProposal = types.NewProposal
NewQueryCommitteeParams = types.NewQueryCommitteeParams
NewQueryProposalParams = types.NewQueryProposalParams
NewQueryRawParamsParams = types.NewQueryRawParamsParams
NewQueryVoteParams = types.NewQueryVoteParams
NewVote = types.NewVote
RegisterCodec = types.RegisterCodec
@ -77,6 +81,7 @@ var (
ErrProposalExpired = types.ErrProposalExpired
ErrUnknownCommittee = types.ErrUnknownCommittee
ErrUnknownProposal = types.ErrUnknownProposal
ErrUnknownSubspace = types.ErrUnknownSubspace
ErrUnknownVote = types.ErrUnknownVote
ModuleCdc = types.ModuleCdc
NextProposalIDKey = types.NextProposalIDKey
@ -108,6 +113,7 @@ type (
PubProposal = types.PubProposal
QueryCommitteeParams = types.QueryCommitteeParams
QueryProposalParams = types.QueryProposalParams
QueryRawParamsParams = types.QueryRawParamsParams
QueryVoteParams = types.QueryVoteParams
SimpleParamChangePermission = types.SimpleParamChangePermission
SoftwareUpgradePermission = types.SoftwareUpgradePermission

View File

@ -0,0 +1,798 @@
package v0_11
import (
"fmt"
"time"
yaml "gopkg.in/yaml.v2"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
"github.com/cosmos/cosmos-sdk/x/params"
paramstypes "github.com/cosmos/cosmos-sdk/x/params/types"
upgrade "github.com/cosmos/cosmos-sdk/x/upgrade"
bep3types "github.com/kava-labs/kava/x/bep3/types"
cdptypes "github.com/kava-labs/kava/x/cdp/types"
"github.com/kava-labs/kava/x/pricefeed"
pricefeedtypes "github.com/kava-labs/kava/x/pricefeed/types"
)
const (
MaxCommitteeDescriptionLength int = 512
)
// Permission is anything with a method that validates whether a proposal is allowed by it or not.
type Permission interface {
Allows(sdk.Context, *codec.Codec, ParamKeeper, PubProposal) bool
}
type ParamKeeper interface {
GetSubspace(string) (params.Subspace, bool)
}
// A Committee is a collection of addresses that are allowed to vote and enact any governance proposal that passes their permissions.
type Committee struct {
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.
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.
}
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,
}
}
func (c Committee) HasMember(addr sdk.AccAddress) bool {
for _, m := range c.Members {
if m.Equals(addr) {
return true
}
}
return false
}
// 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 {
for _, p := range c.Permissions {
if p.Allows(ctx, appCdc, pk, proposal) {
return true
}
}
return false
}
func (c Committee) Validate() error {
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)
}
// check for valid addresses
if m.Empty() {
return fmt.Errorf("committee cannot have empty member address")
}
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)
}
return nil
}
// ------------------------------------------
// Proposals
// ------------------------------------------
// PubProposal is the interface that all proposals must fulfill to be submitted to a committee.
// Proposal types can be created external to this module. For example a ParamChangeProposal, or CommunityPoolSpendProposal.
// It is pinned to the equivalent type in the gov module to create compatibility between proposal types.
type PubProposal govtypes.Content
// Proposal is an internal record of a governance proposal submitted to a committee.
type Proposal struct {
PubProposal `json:"pub_proposal" yaml:"pub_proposal"`
ID uint64 `json:"id" yaml:"id"`
CommitteeID uint64 `json:"committee_id" yaml:"committee_id"`
Deadline time.Time `json:"deadline" yaml:"deadline"`
}
func NewProposal(pubProposal PubProposal, id uint64, committeeID uint64, deadline time.Time) Proposal {
return Proposal{
PubProposal: pubProposal,
ID: id,
CommitteeID: committeeID,
Deadline: deadline,
}
}
// HasExpiredBy calculates if the proposal will have expired by a certain time.
// All votes must be cast before deadline, those cast at time == deadline are not valid
func (p Proposal) HasExpiredBy(time time.Time) bool {
return !time.Before(p.Deadline)
}
// String implements the fmt.Stringer interface, and importantly overrides the String methods inherited from the embedded PubProposal type.
func (p Proposal) String() string {
bz, _ := yaml.Marshal(p)
return string(bz)
}
// ------------------------------------------
// Votes
// ------------------------------------------
type Vote struct {
ProposalID uint64 `json:"proposal_id" yaml:"proposal_id"`
Voter sdk.AccAddress `json:"voter" yaml:"voter"`
}
func NewVote(proposalID uint64, voter sdk.AccAddress) Vote {
return Vote{
ProposalID: proposalID,
Voter: voter,
}
}
func (v Vote) Validate() error {
if v.Voter.Empty() {
return fmt.Errorf("voter address cannot be empty")
}
return nil
}
// ------------------------------------------
// GodPermission
// ------------------------------------------
// GodPermission allows any governance proposal. It is used mainly for testing.
type GodPermission struct{}
var _ Permission = GodPermission{}
func (GodPermission) Allows(sdk.Context, *codec.Codec, ParamKeeper, PubProposal) bool { return true }
func (GodPermission) MarshalYAML() (interface{}, error) {
valueToMarshal := struct {
Type string `yaml:"type"`
}{
Type: "god_permission",
}
return valueToMarshal, nil
}
// ------------------------------------------
// SimpleParamChangePermission
// ------------------------------------------
// SimpleParamChangePermission only allows changes to certain params
type SimpleParamChangePermission struct {
AllowedParams AllowedParams `json:"allowed_params" yaml:"allowed_params"`
}
var _ Permission = SimpleParamChangePermission{}
func (perm SimpleParamChangePermission) Allows(_ sdk.Context, _ *codec.Codec, _ ParamKeeper, p PubProposal) bool {
proposal, ok := p.(paramstypes.ParameterChangeProposal)
if !ok {
return false
}
for _, change := range proposal.Changes {
if !perm.AllowedParams.Contains(change) {
return false
}
}
return true
}
func (perm SimpleParamChangePermission) MarshalYAML() (interface{}, error) {
valueToMarshal := struct {
Type string `yaml:"type"`
AllowedParams AllowedParams `yaml:"allowed_params"`
}{
Type: "param_change_permission",
AllowedParams: perm.AllowedParams,
}
return valueToMarshal, nil
}
type AllowedParam struct {
Subspace string `json:"subspace" yaml:"subspace"`
Key string `json:"key" yaml:"key"`
}
type AllowedParams []AllowedParam
func (allowed AllowedParams) Contains(paramChange paramstypes.ParamChange) bool {
for _, p := range allowed {
if paramChange.Subspace == p.Subspace && paramChange.Key == p.Key {
return true
}
}
return false
}
// ------------------------------------------
// TextPermission
// ------------------------------------------
// TextPermission allows any text governance proposal.
type TextPermission struct{}
var _ Permission = TextPermission{}
func (TextPermission) Allows(_ sdk.Context, _ *codec.Codec, _ ParamKeeper, p PubProposal) bool {
_, ok := p.(govtypes.TextProposal)
return ok
}
func (TextPermission) MarshalYAML() (interface{}, error) {
valueToMarshal := struct {
Type string `yaml:"type"`
}{
Type: "text_permission",
}
return valueToMarshal, nil
}
// ------------------------------------------
// SoftwareUpgradePermission
// ------------------------------------------
type SoftwareUpgradePermission struct{}
var _ Permission = SoftwareUpgradePermission{}
func (SoftwareUpgradePermission) Allows(_ sdk.Context, _ *codec.Codec, _ ParamKeeper, p PubProposal) bool {
_, ok := p.(upgrade.SoftwareUpgradeProposal)
return ok
}
func (SoftwareUpgradePermission) MarshalYAML() (interface{}, error) {
valueToMarshal := struct {
Type string `yaml:"type"`
}{
Type: "software_upgrade_permission",
}
return valueToMarshal, nil
}
// ------------------------------------------
// SubParamChangePermission
// ------------------------------------------
// ParamChangeProposal only allows changes to certain params
type SubParamChangePermission struct {
AllowedParams AllowedParams `json:"allowed_params" yaml:"allowed_params"`
AllowedCollateralParams AllowedCollateralParams `json:"allowed_collateral_params" yaml:"allowed_collateral_params"`
AllowedDebtParam AllowedDebtParam `json:"allowed_debt_param" yaml:"allowed_debt_param"`
AllowedAssetParams AllowedAssetParams `json:"allowed_asset_params" yaml:"allowed_asset_params"`
AllowedMarkets AllowedMarkets `json:"allowed_markets" yaml:"allowed_markets"`
}
var _ Permission = SubParamChangePermission{}
func (perm SubParamChangePermission) MarshalYAML() (interface{}, error) {
valueToMarshal := struct {
Type string `yaml:"type"`
AllowedParams AllowedParams `yaml:"allowed_params"`
AllowedCollateralParams AllowedCollateralParams `yaml:"allowed_collateral_params"`
AllowedDebtParam AllowedDebtParam `yaml:"allowed_debt_param"`
AllowedAssetParams AllowedAssetParams `yaml:"allowed_asset_params"`
AllowedMarkets AllowedMarkets `yaml:"allowed_markets"`
}{
Type: "param_change_permission",
AllowedParams: perm.AllowedParams,
AllowedCollateralParams: perm.AllowedCollateralParams,
AllowedDebtParam: perm.AllowedDebtParam,
AllowedAssetParams: perm.AllowedAssetParams,
AllowedMarkets: perm.AllowedMarkets,
}
return valueToMarshal, nil
}
func (perm SubParamChangePermission) Allows(ctx sdk.Context, appCdc *codec.Codec, pk ParamKeeper, p PubProposal) bool {
// Check pubproposal has correct type
proposal, ok := p.(paramstypes.ParameterChangeProposal)
if !ok {
return false
}
// Check the param changes match the allowed keys
for _, change := range proposal.Changes {
if !perm.AllowedParams.Contains(change) {
return false
}
}
// Check any CollateralParam changes are allowed
// Get the incoming CollaterParams value
var foundIncomingCP bool
var incomingCP cdptypes.CollateralParams
for _, change := range proposal.Changes {
if !(change.Subspace == cdptypes.ModuleName && change.Key == string(cdptypes.KeyCollateralParams)) {
continue
}
// note: in case of duplicates take the last value
foundIncomingCP = true
if err := appCdc.UnmarshalJSON([]byte(change.Value), &incomingCP); err != nil {
return false // invalid json value, so just disallow
}
}
// only check if there was a proposed change
if foundIncomingCP {
// Get the current value of the CollateralParams
cdpSubspace, found := pk.GetSubspace(cdptypes.ModuleName)
if !found {
return false // not using a panic to help avoid begin blocker panics
}
var currentCP cdptypes.CollateralParams
cdpSubspace.Get(ctx, cdptypes.KeyCollateralParams, &currentCP) // panics if something goes wrong
// Check all the incoming changes in the CollateralParams are allowed
collateralParamChangesAllowed := perm.AllowedCollateralParams.Allows(currentCP, incomingCP)
if !collateralParamChangesAllowed {
return false
}
}
// Check any DebtParam changes are allowed
// Get the incoming DebtParam value
var foundIncomingDP bool
var incomingDP cdptypes.DebtParam
for _, change := range proposal.Changes {
if !(change.Subspace == cdptypes.ModuleName && change.Key == string(cdptypes.KeyDebtParam)) {
continue
}
// note: in case of duplicates take the last value
foundIncomingDP = true
if err := appCdc.UnmarshalJSON([]byte(change.Value), &incomingDP); err != nil {
return false // invalid json value, so just disallow
}
}
// only check if there was a proposed change
if foundIncomingDP {
// Get the current value of the DebtParams
cdpSubspace, found := pk.GetSubspace(cdptypes.ModuleName)
if !found {
return false // not using a panic to help avoid begin blocker panics
}
var currentDP cdptypes.DebtParam
cdpSubspace.Get(ctx, cdptypes.KeyDebtParam, &currentDP) // panics if something goes wrong
// Check the incoming changes in the DebtParam are allowed
debtParamChangeAllowed := perm.AllowedDebtParam.Allows(currentDP, incomingDP)
if !debtParamChangeAllowed {
return false
}
}
// Check any AssetParams changes are allowed
// Get the incoming AssetParams value
var foundIncomingAPs bool
var incomingAPs bep3types.AssetParams
for _, change := range proposal.Changes {
if !(change.Subspace == bep3types.ModuleName && change.Key == string(bep3types.KeyAssetParams)) {
continue
}
// note: in case of duplicates take the last value
foundIncomingAPs = true
if err := appCdc.UnmarshalJSON([]byte(change.Value), &incomingAPs); err != nil {
return false // invalid json value, so just disallow
}
}
// only check if there was a proposed change
if foundIncomingAPs {
// Get the current value of the SupportedAssets
subspace, found := pk.GetSubspace(bep3types.ModuleName)
if !found {
return false // not using a panic to help avoid begin blocker panics
}
var currentAPs bep3types.AssetParams
subspace.Get(ctx, bep3types.KeyAssetParams, &currentAPs) // panics if something goes wrong
// Check all the incoming changes in the CollateralParams are allowed
assetParamsChangesAllowed := perm.AllowedAssetParams.Allows(currentAPs, incomingAPs)
if !assetParamsChangesAllowed {
return false
}
}
// Check any Markets changes are allowed
// Get the incoming Markets value
var foundIncomingMs bool
var incomingMs pricefeedtypes.Markets
for _, change := range proposal.Changes {
if !(change.Subspace == pricefeedtypes.ModuleName && change.Key == string(pricefeedtypes.KeyMarkets)) {
continue
}
// note: in case of duplicates take the last value
foundIncomingMs = true
if err := appCdc.UnmarshalJSON([]byte(change.Value), &incomingMs); err != nil {
return false // invalid json value, so just disallow
}
}
// only check if there was a proposed change
if foundIncomingMs {
// Get the current value of the Markets
subspace, found := pk.GetSubspace(pricefeedtypes.ModuleName)
if !found {
return false // not using a panic to help avoid begin blocker panics
}
var currentMs pricefeedtypes.Markets
subspace.Get(ctx, pricefeedtypes.KeyMarkets, &currentMs) // panics if something goes wrong
// Check all the incoming changes in the Markets are allowed
marketsChangesAllowed := perm.AllowedMarkets.Allows(currentMs, incomingMs)
if !marketsChangesAllowed {
return false
}
}
return true
}
// AllowedCollateralParam cdp.CollateralParam fields that can be subject to committee governance
type AllowedCollateralParam struct {
Type string `json:"type" yaml:"type"`
Denom bool `json:"denom" yaml:"denom"`
LiquidationRatio bool `json:"liquidation_ratio" yaml:"liquidation_ratio"`
DebtLimit bool `json:"debt_limit" yaml:"debt_limit"`
StabilityFee bool `json:"stability_fee" yaml:"stability_fee"`
AuctionSize bool `json:"auction_size" yaml:"auction_size"`
LiquidationPenalty bool `json:"liquidation_penalty" yaml:"liquidation_penalty"`
Prefix bool `json:"prefix" yaml:"prefix"`
SpotMarketID bool `json:"spot_market_id" yaml:"spot_market_id"`
LiquidationMarketID bool `json:"liquidation_market_id" yaml:"liquidation_market_id"`
ConversionFactor bool `json:"conversion_factor" yaml:"conversion_factor"`
}
type AllowedCollateralParams []AllowedCollateralParam
func (acps AllowedCollateralParams) Allows(current, incoming cdptypes.CollateralParams) bool {
allAllowed := true
// do not allow CollateralParams to be added or removed
// this checks both lists are the same size, then below checks each incoming matches a current
if len(incoming) != len(current) {
return false
}
// for each param struct, check it is allowed, and if it is not, check the value has not changed
for _, incomingCP := range incoming {
// 1) check incoming cp is in list of allowed cps
var foundAllowedCP bool
var allowedCP AllowedCollateralParam
for _, p := range acps {
if p.Type != incomingCP.Type {
continue
}
foundAllowedCP = true
allowedCP = p
}
if !foundAllowedCP {
// incoming had a CollateralParam that wasn't in the list of allowed ones
return false
}
// 2) Check incoming changes are individually allowed
// find existing CollateralParam
var foundCurrentCP bool
var currentCP cdptypes.CollateralParam
for _, p := range current {
if p.Denom != incomingCP.Denom {
continue
}
foundCurrentCP = true
currentCP = p
}
if !foundCurrentCP {
return false // not allowed to add param to list
}
// check changed values are all allowed
allowed := allowedCP.Allows(currentCP, incomingCP)
allAllowed = allAllowed && allowed
}
return allAllowed
}
func (acp AllowedCollateralParam) Allows(current, incoming cdptypes.CollateralParam) bool {
allowed := ((acp.Type == current.Type) && (acp.Type == incoming.Type)) && // require collatreral types to be all equal
(current.Denom == incoming.Denom || acp.Denom) &&
(current.LiquidationRatio.Equal(incoming.LiquidationRatio) || acp.LiquidationRatio) &&
(current.DebtLimit.IsEqual(incoming.DebtLimit) || acp.DebtLimit) &&
(current.StabilityFee.Equal(incoming.StabilityFee) || acp.StabilityFee) &&
(current.AuctionSize.Equal(incoming.AuctionSize) || acp.AuctionSize) &&
(current.LiquidationPenalty.Equal(incoming.LiquidationPenalty) || acp.LiquidationPenalty) &&
((current.Prefix == incoming.Prefix) || acp.Prefix) &&
((current.SpotMarketID == incoming.SpotMarketID) || acp.SpotMarketID) &&
((current.LiquidationMarketID == incoming.LiquidationMarketID) || acp.LiquidationMarketID) &&
(current.ConversionFactor.Equal(incoming.ConversionFactor) || acp.ConversionFactor)
return allowed
}
type AllowedDebtParam struct {
Denom bool `json:"denom" yaml:"denom"`
ReferenceAsset bool `json:"reference_asset" yaml:"reference_asset"`
ConversionFactor bool `json:"conversion_factor" yaml:"conversion_factor"`
DebtFloor bool `json:"debt_floor" yaml:"debt_floor"`
SavingsRate bool `json:"savings_rate" yaml:"savings_rate"`
}
func (adp AllowedDebtParam) Allows(current, incoming cdptypes.DebtParam) bool {
allowed := ((current.Denom == incoming.Denom) || adp.Denom) &&
((current.ReferenceAsset == incoming.ReferenceAsset) || adp.ReferenceAsset) &&
(current.ConversionFactor.Equal(incoming.ConversionFactor) || adp.ConversionFactor) &&
(current.DebtFloor.Equal(incoming.DebtFloor) || adp.DebtFloor) &&
(current.SavingsRate.Equal(incoming.SavingsRate) || adp.SavingsRate)
return allowed
}
type AllowedAssetParams []AllowedAssetParam
func (aaps AllowedAssetParams) Allows(current, incoming bep3types.AssetParams) bool {
allAllowed := true
// do not allow AssetParams to be added or removed
// this checks both lists are the same size, then below checks each incoming matches a current
if len(incoming) != len(current) {
return false
}
// for each asset struct, check it is allowed, and if it is not, check the value has not changed
for _, incomingAP := range incoming {
// 1) check incoming ap is in list of allowed aps
var foundAllowedAP bool
var allowedAP AllowedAssetParam
for _, p := range aaps {
if p.Denom != incomingAP.Denom {
continue
}
foundAllowedAP = true
allowedAP = p
}
if !foundAllowedAP {
// incoming had a AssetParam that wasn't in the list of allowed ones
return false
}
// 2) Check incoming changes are individually allowed
// find existing SupportedAsset
var foundCurrentAP bool
var currentAP bep3types.AssetParam
for _, p := range current {
if p.Denom != incomingAP.Denom {
continue
}
foundCurrentAP = true
currentAP = p
}
if !foundCurrentAP {
return false // not allowed to add asset to list
}
// check changed values are all allowed
allowed := allowedAP.Allows(currentAP, incomingAP)
allAllowed = allAllowed && allowed
}
return allAllowed
}
type AllowedAssetParam struct {
Denom string `json:"denom" yaml:"denom"`
CoinID bool `json:"coin_id" yaml:"coin_id"`
Limit bool `json:"limit" yaml:"limit"`
Active bool `json:"active" yaml:"active"`
}
func (aap AllowedAssetParam) Allows(current, incoming bep3types.AssetParam) bool {
allowed := ((aap.Denom == current.Denom) && (aap.Denom == incoming.Denom)) && // require denoms to be all equal
((current.CoinID == incoming.CoinID) || aap.CoinID) &&
(current.SupplyLimit.Equals(incoming.SupplyLimit) || aap.Limit) &&
((current.Active == incoming.Active) || aap.Active)
return allowed
}
type AllowedMarkets []AllowedMarket
func (ams AllowedMarkets) Allows(current, incoming pricefeedtypes.Markets) bool {
allAllowed := true
// do not allow Markets to be added or removed
// this checks both lists are the same size, then below checks each incoming matches a current
if len(incoming) != len(current) {
return false
}
// for each market struct, check it is allowed, and if it is not, check the value has not changed
for _, incomingM := range incoming {
// 1) check incoming market is in list of allowed markets
var foundAllowedM bool
var allowedM AllowedMarket
for _, p := range ams {
if p.MarketID != incomingM.MarketID {
continue
}
foundAllowedM = true
allowedM = p
}
if !foundAllowedM {
// incoming had a Market that wasn't in the list of allowed ones
return false
}
// 2) Check incoming changes are individually allowed
// find existing SupportedAsset
var foundCurrentM bool
var currentM pricefeed.Market
for _, p := range current {
if p.MarketID != incomingM.MarketID {
continue
}
foundCurrentM = true
currentM = p
}
if !foundCurrentM {
return false // not allowed to add market to list
}
// check changed values are all allowed
allowed := allowedM.Allows(currentM, incomingM)
allAllowed = allAllowed && allowed
}
return allAllowed
}
type AllowedMarket struct {
MarketID string `json:"market_id" yaml:"market_id"`
BaseAsset bool `json:"base_asset" yaml:"base_asset"`
QuoteAsset bool `json:"quote_asset" yaml:"quote_asset"`
Oracles bool `json:"oracles" yaml:"oracles"`
Active bool `json:"active" yaml:"active"`
}
func (am AllowedMarket) Allows(current, incoming pricefeedtypes.Market) bool {
allowed := ((am.MarketID == current.MarketID) && (am.MarketID == incoming.MarketID)) && // require denoms to be all equal
((current.BaseAsset == incoming.BaseAsset) || am.BaseAsset) &&
((current.QuoteAsset == incoming.QuoteAsset) || am.QuoteAsset) &&
(addressesEqual(current.Oracles, incoming.Oracles) || am.Oracles) &&
((current.Active == incoming.Active) || am.Active)
return allowed
}
// addressesEqual check if slices of addresses are equal, the order matters
func addressesEqual(addrs1, addrs2 []sdk.AccAddress) bool {
if len(addrs1) != len(addrs2) {
return false
}
areEqual := true
for i := range addrs1 {
areEqual = areEqual && addrs1[i].Equals(addrs2[i])
}
return areEqual
}
// DefaultNextProposalID is the starting poiint for proposal IDs.
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"`
}
// NewGenesisState returns a new genesis state object for the module.
func NewGenesisState(nextProposalID uint64, committees []Committee, proposals []Proposal, votes []Vote) GenesisState {
return GenesisState{
NextProposalID: nextProposalID,
Committees: committees,
Proposals: proposals,
Votes: votes,
}
}
// DefaultGenesisState returns the default genesis state for the module.
func DefaultGenesisState() GenesisState {
return NewGenesisState(
DefaultNextProposalID,
[]Committee{},
[]Proposal{},
[]Vote{},
)
}
// Validate performs basic validation of genesis data.
func (gs GenesisState) Validate() error {
// validate committees
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)
}
committeeMap[com.ID] = true
// validate committee
if err := com.Validate(); err != nil {
return err
}
}
// validate proposals
proposalMap := make(map[uint64]bool, len(gs.Proposals))
for _, p := range gs.Proposals {
// check there are no duplicate IDs
if _, ok := proposalMap[p.ID]; ok {
return fmt.Errorf("duplicate proposal ID found in genesis state; id: %d", p.ID)
}
proposalMap[p.ID] = true
// validate next proposal ID
if p.ID >= gs.NextProposalID {
return fmt.Errorf("NextProposalID is not greater than all proposal IDs; id: %d", p.ID)
}
// check committee exists
if !committeeMap[p.CommitteeID] {
return fmt.Errorf("proposal refers to non existent committee; proposal: %+v", p)
}
// validate pubProposal
if err := p.PubProposal.ValidateBasic(); err != nil {
return fmt.Errorf("proposal %d invalid: %w", p.ID, err)
}
}
// validate votes
for _, v := range gs.Votes {
// validate committee
if err := v.Validate(); err != nil {
return err
}
// check proposal exists
if !proposalMap[v.ProposalID] {
return fmt.Errorf("vote refers to non existent proposal; vote: %+v", v)
}
}
return nil
}

View File

@ -0,0 +1,810 @@
package v0_9
import (
"fmt"
"time"
yaml "gopkg.in/yaml.v2"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
"github.com/cosmos/cosmos-sdk/x/params"
paramstypes "github.com/cosmos/cosmos-sdk/x/params/types"
upgrade "github.com/cosmos/cosmos-sdk/x/upgrade"
bep3types "github.com/kava-labs/kava/x/bep3/types"
cdptypes "github.com/kava-labs/kava/x/cdp/types"
"github.com/kava-labs/kava/x/pricefeed"
pricefeedtypes "github.com/kava-labs/kava/x/pricefeed/types"
)
const (
MaxCommitteeDescriptionLength int = 512
)
// Permission is anything with a method that validates whether a proposal is allowed by it or not.
type Permission interface {
Allows(sdk.Context, *codec.Codec, ParamKeeper, PubProposal) bool
}
type ParamKeeper interface {
GetSubspace(string) (params.Subspace, bool)
}
// A Committee is a collection of addresses that are allowed to vote and enact any governance proposal that passes their permissions.
type Committee struct {
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.
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.
}
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,
}
}
func (c Committee) HasMember(addr sdk.AccAddress) bool {
for _, m := range c.Members {
if m.Equals(addr) {
return true
}
}
return false
}
// 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 {
for _, p := range c.Permissions {
if p.Allows(ctx, appCdc, pk, proposal) {
return true
}
}
return false
}
func (c Committee) Validate() error {
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)
}
// check for valid addresses
if m.Empty() {
return fmt.Errorf("committee cannot have empty member address")
}
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)
}
return nil
}
// ------------------------------------------
// Proposals
// ------------------------------------------
// PubProposal is the interface that all proposals must fulfill to be submitted to a committee.
// Proposal types can be created external to this module. For example a ParamChangeProposal, or CommunityPoolSpendProposal.
// It is pinned to the equivalent type in the gov module to create compatibility between proposal types.
type PubProposal govtypes.Content
// Proposal is an internal record of a governance proposal submitted to a committee.
type Proposal struct {
PubProposal `json:"pub_proposal" yaml:"pub_proposal"`
ID uint64 `json:"id" yaml:"id"`
CommitteeID uint64 `json:"committee_id" yaml:"committee_id"`
Deadline time.Time `json:"deadline" yaml:"deadline"`
}
func NewProposal(pubProposal PubProposal, id uint64, committeeID uint64, deadline time.Time) Proposal {
return Proposal{
PubProposal: pubProposal,
ID: id,
CommitteeID: committeeID,
Deadline: deadline,
}
}
// HasExpiredBy calculates if the proposal will have expired by a certain time.
// All votes must be cast before deadline, those cast at time == deadline are not valid
func (p Proposal) HasExpiredBy(time time.Time) bool {
return !time.Before(p.Deadline)
}
// String implements the fmt.Stringer interface, and importantly overrides the String methods inherited from the embedded PubProposal type.
func (p Proposal) String() string {
bz, _ := yaml.Marshal(p)
return string(bz)
}
// ------------------------------------------
// Votes
// ------------------------------------------
type Vote struct {
ProposalID uint64 `json:"proposal_id" yaml:"proposal_id"`
Voter sdk.AccAddress `json:"voter" yaml:"voter"`
}
func NewVote(proposalID uint64, voter sdk.AccAddress) Vote {
return Vote{
ProposalID: proposalID,
Voter: voter,
}
}
func (v Vote) Validate() error {
if v.Voter.Empty() {
return fmt.Errorf("voter address cannot be empty")
}
return nil
}
// ------------------------------------------
// GodPermission
// ------------------------------------------
// GodPermission allows any governance proposal. It is used mainly for testing.
type GodPermission struct{}
var _ Permission = GodPermission{}
func (GodPermission) Allows(sdk.Context, *codec.Codec, ParamKeeper, PubProposal) bool { return true }
func (GodPermission) MarshalYAML() (interface{}, error) {
valueToMarshal := struct {
Type string `yaml:"type"`
}{
Type: "god_permission",
}
return valueToMarshal, nil
}
// ------------------------------------------
// SimpleParamChangePermission
// ------------------------------------------
// SimpleParamChangePermission only allows changes to certain params
type SimpleParamChangePermission struct {
AllowedParams AllowedParams `json:"allowed_params" yaml:"allowed_params"`
}
var _ Permission = SimpleParamChangePermission{}
func (perm SimpleParamChangePermission) Allows(_ sdk.Context, _ *codec.Codec, _ ParamKeeper, p PubProposal) bool {
proposal, ok := p.(paramstypes.ParameterChangeProposal)
if !ok {
return false
}
for _, change := range proposal.Changes {
if !perm.AllowedParams.Contains(change) {
return false
}
}
return true
}
func (perm SimpleParamChangePermission) MarshalYAML() (interface{}, error) {
valueToMarshal := struct {
Type string `yaml:"type"`
AllowedParams AllowedParams `yaml:"allowed_params"`
}{
Type: "param_change_permission",
AllowedParams: perm.AllowedParams,
}
return valueToMarshal, nil
}
type AllowedParam struct {
Subspace string `json:"subspace" yaml:"subspace"`
Key string `json:"key" yaml:"key"`
}
type AllowedParams []AllowedParam
func (allowed AllowedParams) Contains(paramChange paramstypes.ParamChange) bool {
for _, p := range allowed {
if paramChange.Subspace == p.Subspace && paramChange.Key == p.Key {
return true
}
}
return false
}
// ------------------------------------------
// TextPermission
// ------------------------------------------
// TextPermission allows any text governance proposal.
type TextPermission struct{}
var _ Permission = TextPermission{}
func (TextPermission) Allows(_ sdk.Context, _ *codec.Codec, _ ParamKeeper, p PubProposal) bool {
_, ok := p.(govtypes.TextProposal)
return ok
}
func (TextPermission) MarshalYAML() (interface{}, error) {
valueToMarshal := struct {
Type string `yaml:"type"`
}{
Type: "text_permission",
}
return valueToMarshal, nil
}
// ------------------------------------------
// SoftwareUpgradePermission
// ------------------------------------------
type SoftwareUpgradePermission struct{}
var _ Permission = SoftwareUpgradePermission{}
func (SoftwareUpgradePermission) Allows(_ sdk.Context, _ *codec.Codec, _ ParamKeeper, p PubProposal) bool {
_, ok := p.(upgrade.SoftwareUpgradeProposal)
return ok
}
func (SoftwareUpgradePermission) MarshalYAML() (interface{}, error) {
valueToMarshal := struct {
Type string `yaml:"type"`
}{
Type: "software_upgrade_permission",
}
return valueToMarshal, nil
}
// ------------------------------------------
// SubParamChangePermission
// ------------------------------------------
// ParamChangeProposal only allows changes to certain params
type SubParamChangePermission struct {
AllowedParams AllowedParams `json:"allowed_params" yaml:"allowed_params"`
AllowedCollateralParams AllowedCollateralParams `json:"allowed_collateral_params" yaml:"allowed_collateral_params"`
AllowedDebtParam AllowedDebtParam `json:"allowed_debt_param" yaml:"allowed_debt_param"`
AllowedAssetParams AllowedAssetParams `json:"allowed_asset_params" yaml:"allowed_asset_params"`
AllowedMarkets AllowedMarkets `json:"allowed_markets" yaml:"allowed_markets"`
}
var _ Permission = SubParamChangePermission{}
func (perm SubParamChangePermission) MarshalYAML() (interface{}, error) {
valueToMarshal := struct {
Type string `yaml:"type"`
AllowedParams AllowedParams `yaml:"allowed_params"`
AllowedCollateralParams AllowedCollateralParams `yaml:"allowed_collateral_params"`
AllowedDebtParam AllowedDebtParam `yaml:"allowed_debt_param"`
AllowedAssetParams AllowedAssetParams `yaml:"allowed_asset_params"`
AllowedMarkets AllowedMarkets `yaml:"allowed_markets"`
}{
Type: "param_change_permission",
AllowedParams: perm.AllowedParams,
AllowedCollateralParams: perm.AllowedCollateralParams,
AllowedDebtParam: perm.AllowedDebtParam,
AllowedAssetParams: perm.AllowedAssetParams,
AllowedMarkets: perm.AllowedMarkets,
}
return valueToMarshal, nil
}
func (perm SubParamChangePermission) Allows(ctx sdk.Context, appCdc *codec.Codec, pk ParamKeeper, p PubProposal) bool {
// Check pubproposal has correct type
proposal, ok := p.(paramstypes.ParameterChangeProposal)
if !ok {
return false
}
// Check the param changes match the allowed keys
for _, change := range proposal.Changes {
if !perm.AllowedParams.Contains(change) {
return false
}
}
// Check any CollateralParam changes are allowed
// Get the incoming CollaterParams value
var foundIncomingCP bool
var incomingCP cdptypes.CollateralParams
for _, change := range proposal.Changes {
if !(change.Subspace == cdptypes.ModuleName && change.Key == string(cdptypes.KeyCollateralParams)) {
continue
}
// note: in case of duplicates take the last value
foundIncomingCP = true
if err := appCdc.UnmarshalJSON([]byte(change.Value), &incomingCP); err != nil {
return false // invalid json value, so just disallow
}
}
// only check if there was a proposed change
if foundIncomingCP {
// Get the current value of the CollateralParams
cdpSubspace, found := pk.GetSubspace(cdptypes.ModuleName)
if !found {
return false // not using a panic to help avoid begin blocker panics
}
var currentCP cdptypes.CollateralParams
cdpSubspace.Get(ctx, cdptypes.KeyCollateralParams, &currentCP) // panics if something goes wrong
// Check all the incoming changes in the CollateralParams are allowed
collateralParamChangesAllowed := perm.AllowedCollateralParams.Allows(currentCP, incomingCP)
if !collateralParamChangesAllowed {
return false
}
}
// Check any DebtParam changes are allowed
// Get the incoming DebtParam value
var foundIncomingDP bool
var incomingDP cdptypes.DebtParam
for _, change := range proposal.Changes {
if !(change.Subspace == cdptypes.ModuleName && change.Key == string(cdptypes.KeyDebtParam)) {
continue
}
// note: in case of duplicates take the last value
foundIncomingDP = true
if err := appCdc.UnmarshalJSON([]byte(change.Value), &incomingDP); err != nil {
return false // invalid json value, so just disallow
}
}
// only check if there was a proposed change
if foundIncomingDP {
// Get the current value of the DebtParams
cdpSubspace, found := pk.GetSubspace(cdptypes.ModuleName)
if !found {
return false // not using a panic to help avoid begin blocker panics
}
var currentDP cdptypes.DebtParam
cdpSubspace.Get(ctx, cdptypes.KeyDebtParam, &currentDP) // panics if something goes wrong
// Check the incoming changes in the DebtParam are allowed
debtParamChangeAllowed := perm.AllowedDebtParam.Allows(currentDP, incomingDP)
if !debtParamChangeAllowed {
return false
}
}
// Check any AssetParams changes are allowed
// Get the incoming AssetParams value
var foundIncomingAPs bool
var incomingAPs bep3types.AssetParams
for _, change := range proposal.Changes {
if !(change.Subspace == bep3types.ModuleName && change.Key == string(bep3types.KeyAssetParams)) {
continue
}
// note: in case of duplicates take the last value
foundIncomingAPs = true
if err := appCdc.UnmarshalJSON([]byte(change.Value), &incomingAPs); err != nil {
return false // invalid json value, so just disallow
}
}
// only check if there was a proposed change
if foundIncomingAPs {
// Get the current value of the SupportedAssets
subspace, found := pk.GetSubspace(bep3types.ModuleName)
if !found {
return false // not using a panic to help avoid begin blocker panics
}
var currentAPs bep3types.AssetParams
subspace.Get(ctx, bep3types.KeyAssetParams, &currentAPs) // panics if something goes wrong
// Check all the incoming changes in the CollateralParams are allowed
assetParamsChangesAllowed := perm.AllowedAssetParams.Allows(currentAPs, incomingAPs)
if !assetParamsChangesAllowed {
return false
}
}
// Check any Markets changes are allowed
// Get the incoming Markets value
var foundIncomingMs bool
var incomingMs pricefeedtypes.Markets
for _, change := range proposal.Changes {
if !(change.Subspace == pricefeedtypes.ModuleName && change.Key == string(pricefeedtypes.KeyMarkets)) {
continue
}
// note: in case of duplicates take the last value
foundIncomingMs = true
if err := appCdc.UnmarshalJSON([]byte(change.Value), &incomingMs); err != nil {
return false // invalid json value, so just disallow
}
}
// only check if there was a proposed change
if foundIncomingMs {
// Get the current value of the Markets
subspace, found := pk.GetSubspace(pricefeedtypes.ModuleName)
if !found {
return false // not using a panic to help avoid begin blocker panics
}
var currentMs pricefeedtypes.Markets
subspace.Get(ctx, pricefeedtypes.KeyMarkets, &currentMs) // panics if something goes wrong
// Check all the incoming changes in the Markets are allowed
marketsChangesAllowed := perm.AllowedMarkets.Allows(currentMs, incomingMs)
if !marketsChangesAllowed {
return false
}
}
return true
}
type AllowedCollateralParams []AllowedCollateralParam
func (acps AllowedCollateralParams) Allows(current, incoming cdptypes.CollateralParams) bool {
allAllowed := true
// do not allow CollateralParams to be added or removed
// this checks both lists are the same size, then below checks each incoming matches a current
if len(incoming) != len(current) {
return false
}
// for each param struct, check it is allowed, and if it is not, check the value has not changed
for _, incomingCP := range incoming {
// 1) check incoming cp is in list of allowed cps
var foundAllowedCP bool
var allowedCP AllowedCollateralParam
for _, p := range acps {
if p.Denom != incomingCP.Denom {
continue
}
foundAllowedCP = true
allowedCP = p
}
if !foundAllowedCP {
// incoming had a CollateralParam that wasn't in the list of allowed ones
return false
}
// 2) Check incoming changes are individually allowed
// find existing CollateralParam
var foundCurrentCP bool
var currentCP cdptypes.CollateralParam
for _, p := range current {
if p.Denom != incomingCP.Denom {
continue
}
foundCurrentCP = true
currentCP = p
}
if !foundCurrentCP {
return false // not allowed to add param to list
}
// check changed values are all allowed
allowed := allowedCP.Allows(currentCP, incomingCP)
allAllowed = allAllowed && allowed
}
return allAllowed
}
type AllowedCollateralParam struct {
Denom string `json:"denom" yaml:"denom"`
LiquidationRatio bool `json:"liquidation_ratio" yaml:"liquidation_ratio"`
DebtLimit bool `json:"debt_limit" yaml:"debt_limit"`
StabilityFee bool `json:"stability_fee" yaml:"stability_fee"`
AuctionSize bool `json:"auction_size" yaml:"auction_size"`
LiquidationPenalty bool `json:"liquidation_penalty" yaml:"liquidation_penalty"`
Prefix bool `json:"prefix" yaml:"prefix"`
SpotMarketID bool `json:"spot_market_id" yaml:"spot_market_id"`
LiquidationMarketID bool `json:"liquidation_market_id" yaml:"liquidation_market_id"`
ConversionFactor bool `json:"conversion_factor" yaml:"conversion_factor"`
}
func (acp AllowedCollateralParam) Allows(current, incoming cdptypes.CollateralParam) bool {
allowed := ((acp.Denom == current.Denom) && (acp.Denom == incoming.Denom)) && // require denoms to be all equal
(current.LiquidationRatio.Equal(incoming.LiquidationRatio) || acp.LiquidationRatio) &&
(current.DebtLimit.IsEqual(incoming.DebtLimit) || acp.DebtLimit) &&
(current.StabilityFee.Equal(incoming.StabilityFee) || acp.StabilityFee) &&
(current.AuctionSize.Equal(incoming.AuctionSize) || acp.AuctionSize) &&
(current.LiquidationPenalty.Equal(incoming.LiquidationPenalty) || acp.LiquidationPenalty) &&
((current.Prefix == incoming.Prefix) || acp.Prefix) &&
((current.SpotMarketID == incoming.SpotMarketID) || acp.SpotMarketID) &&
((current.LiquidationMarketID == incoming.LiquidationMarketID) || acp.LiquidationMarketID) &&
(current.ConversionFactor.Equal(incoming.ConversionFactor) || acp.ConversionFactor)
return allowed
}
type AllowedDebtParam struct {
Denom bool `json:"denom" yaml:"denom"`
ReferenceAsset bool `json:"reference_asset" yaml:"reference_asset"`
ConversionFactor bool `json:"conversion_factor" yaml:"conversion_factor"`
DebtFloor bool `json:"debt_floor" yaml:"debt_floor"`
SavingsRate bool `json:"savings_rate" yaml:"savings_rate"`
}
func (adp AllowedDebtParam) Allows(current, incoming cdptypes.DebtParam) bool {
allowed := ((current.Denom == incoming.Denom) || adp.Denom) &&
((current.ReferenceAsset == incoming.ReferenceAsset) || adp.ReferenceAsset) &&
(current.ConversionFactor.Equal(incoming.ConversionFactor) || adp.ConversionFactor) &&
(current.DebtFloor.Equal(incoming.DebtFloor) || adp.DebtFloor) &&
(current.SavingsRate.Equal(incoming.SavingsRate) || adp.SavingsRate)
return allowed
}
type AllowedAssetParams []AllowedAssetParam
func (aaps AllowedAssetParams) Allows(current, incoming bep3types.AssetParams) bool {
allAllowed := true
// do not allow AssetParams to be added or removed
// this checks both lists are the same size, then below checks each incoming matches a current
if len(incoming) != len(current) {
return false
}
// for each asset struct, check it is allowed, and if it is not, check the value has not changed
for _, incomingAP := range incoming {
// 1) check incoming ap is in list of allowed aps
var foundAllowedAP bool
var allowedAP AllowedAssetParam
for _, p := range aaps {
if p.Denom != incomingAP.Denom {
continue
}
foundAllowedAP = true
allowedAP = p
}
if !foundAllowedAP {
// incoming had a AssetParam that wasn't in the list of allowed ones
return false
}
// 2) Check incoming changes are individually allowed
// find existing SupportedAsset
var foundCurrentAP bool
var currentAP bep3types.AssetParam
for _, p := range current {
if p.Denom != incomingAP.Denom {
continue
}
foundCurrentAP = true
currentAP = p
}
if !foundCurrentAP {
return false // not allowed to add asset to list
}
// check changed values are all allowed
allowed := allowedAP.Allows(currentAP, incomingAP)
allAllowed = allAllowed && allowed
}
return allAllowed
}
type AllowedAssetParam struct {
Denom string `json:"denom" yaml:"denom"`
CoinID bool `json:"coin_id" yaml:"coin_id"`
Limit bool `json:"limit" yaml:"limit"`
Active bool `json:"active" yaml:"active"`
}
func (aap AllowedAssetParam) Allows(current, incoming bep3types.AssetParam) bool {
allowed := ((aap.Denom == current.Denom) && (aap.Denom == incoming.Denom)) && // require denoms to be all equal
((current.CoinID == incoming.CoinID) || aap.CoinID) &&
(current.SupplyLimit.Equals(incoming.SupplyLimit) || aap.Limit) &&
((current.Active == incoming.Active) || aap.Active)
return allowed
}
type AllowedMarkets []AllowedMarket
func (ams AllowedMarkets) Allows(current, incoming pricefeedtypes.Markets) bool {
allAllowed := true
// do not allow Markets to be added or removed
// this checks both lists are the same size, then below checks each incoming matches a current
if len(incoming) != len(current) {
return false
}
// for each market struct, check it is allowed, and if it is not, check the value has not changed
for _, incomingM := range incoming {
// 1) check incoming market is in list of allowed markets
var foundAllowedM bool
var allowedM AllowedMarket
for _, p := range ams {
if p.MarketID != incomingM.MarketID {
continue
}
foundAllowedM = true
allowedM = p
}
if !foundAllowedM {
// incoming had a Market that wasn't in the list of allowed ones
return false
}
// 2) Check incoming changes are individually allowed
// find existing SupportedAsset
var foundCurrentM bool
var currentM pricefeed.Market
for _, p := range current {
if p.MarketID != incomingM.MarketID {
continue
}
foundCurrentM = true
currentM = p
}
if !foundCurrentM {
return false // not allowed to add market to list
}
// check changed values are all allowed
allowed := allowedM.Allows(currentM, incomingM)
allAllowed = allAllowed && allowed
}
return allAllowed
}
type AllowedMarket struct {
MarketID string `json:"market_id" yaml:"market_id"`
BaseAsset bool `json:"base_asset" yaml:"base_asset"`
QuoteAsset bool `json:"quote_asset" yaml:"quote_asset"`
Oracles bool `json:"oracles" yaml:"oracles"`
Active bool `json:"active" yaml:"active"`
}
func (am AllowedMarket) Allows(current, incoming pricefeedtypes.Market) bool {
allowed := ((am.MarketID == current.MarketID) && (am.MarketID == incoming.MarketID)) && // require denoms to be all equal
((current.BaseAsset == incoming.BaseAsset) || am.BaseAsset) &&
((current.QuoteAsset == incoming.QuoteAsset) || am.QuoteAsset) &&
(addressesEqual(current.Oracles, incoming.Oracles) || am.Oracles) &&
((current.Active == incoming.Active) || am.Active)
return allowed
}
// addressesEqual check if slices of addresses are equal, the order matters
func addressesEqual(addrs1, addrs2 []sdk.AccAddress) bool {
if len(addrs1) != len(addrs2) {
return false
}
areEqual := true
for i := range addrs1 {
areEqual = areEqual && addrs1[i].Equals(addrs2[i])
}
return areEqual
}
// DefaultNextProposalID is the starting poiint for proposal IDs.
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"`
}
// NewGenesisState returns a new genesis state object for the module.
func NewGenesisState(nextProposalID uint64, committees []Committee, proposals []Proposal, votes []Vote) GenesisState {
return GenesisState{
NextProposalID: nextProposalID,
Committees: committees,
Proposals: proposals,
Votes: votes,
}
}
// DefaultGenesisState returns the default genesis state for the module.
func DefaultGenesisState() GenesisState {
return NewGenesisState(
DefaultNextProposalID,
[]Committee{},
[]Proposal{},
[]Vote{},
)
}
// Validate performs basic validation of genesis data.
func (gs GenesisState) Validate() error {
// validate committees
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)
}
committeeMap[com.ID] = true
// validate committee
if err := com.Validate(); err != nil {
return err
}
}
// validate proposals
proposalMap := make(map[uint64]bool, len(gs.Proposals))
for _, p := range gs.Proposals {
// check there are no duplicate IDs
if _, ok := proposalMap[p.ID]; ok {
return fmt.Errorf("duplicate proposal ID found in genesis state; id: %d", p.ID)
}
proposalMap[p.ID] = true
// validate next proposal ID
if p.ID >= gs.NextProposalID {
return fmt.Errorf("NextProposalID is not greater than all proposal IDs; id: %d", p.ID)
}
// check committee exists
if !committeeMap[p.CommitteeID] {
return fmt.Errorf("proposal refers to non existent committee; proposal: %+v", p)
}
// validate pubProposal
if err := p.PubProposal.ValidateBasic(); err != nil {
return fmt.Errorf("proposal %d invalid: %w", p.ID, err)
}
}
// validate votes
for _, v := range gs.Votes {
// validate committee
if err := v.Validate(); err != nil {
return err
}
// check proposal exists
if !proposalMap[v.ProposalID] {
return fmt.Errorf("vote refers to non existent proposal; vote: %+v", v)
}
}
return nil
}
// RegisterCodec registers the necessary types for the module
func RegisterCodec(cdc *codec.Codec) {
// Proposals
cdc.RegisterInterface((*PubProposal)(nil), nil)
// Permissions
cdc.RegisterInterface((*Permission)(nil), nil)
cdc.RegisterConcrete(GodPermission{}, "kava/GodPermission", nil)
cdc.RegisterConcrete(SimpleParamChangePermission{}, "kava/SimpleParamChangePermission", nil)
cdc.RegisterConcrete(TextPermission{}, "kava/TextPermission", nil)
cdc.RegisterConcrete(SoftwareUpgradePermission{}, "kava/SoftwareUpgradePermission", nil)
cdc.RegisterConcrete(SubParamChangePermission{}, "kava/SubParamChangePermission", nil)
}

View File

@ -385,6 +385,26 @@ type AllowedCollateralParam struct {
ConversionFactor bool `json:"conversion_factor" yaml:"conversion_factor"`
}
// NewAllowedCollateralParam return a new AllowedCollateralParam
func NewAllowedCollateralParam(
ctype string, denom, liqRatio, debtLimit,
stabilityFee, auctionSize, liquidationPenalty,
prefix, spotMarket, liquidationMarket, conversionFactor bool) AllowedCollateralParam {
return AllowedCollateralParam{
Type: ctype,
Denom: denom,
LiquidationRatio: liqRatio,
DebtLimit: debtLimit,
StabilityFee: stabilityFee,
AuctionSize: auctionSize,
LiquidationPenalty: liquidationPenalty,
Prefix: prefix,
SpotMarketID: spotMarket,
LiquidationMarketID: liquidationMarket,
ConversionFactor: conversionFactor,
}
}
func (acp AllowedCollateralParam) Allows(current, incoming cdptypes.CollateralParam) bool {
allowed := ((acp.Type == current.Type) && (acp.Type == incoming.Type)) && // require collateral types to be all equal
(current.Denom == incoming.Denom || acp.Denom) &&