From 8f69dcf960a478a6aecd8fd8ac74c8f5b33f427a Mon Sep 17 00:00:00 2001 From: Kevin Davis Date: Thu, 1 Oct 2020 15:22:26 -0400 Subject: [PATCH] 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 --- migrate/v0_11/migrate.go | 196 ++++++ migrate/v0_11/migrate_test.go | 19 +- migrate/v0_11/testdata/committee-v09.json | 157 +++++ x/committee/alias.go | 6 + x/committee/legacy/v0_11/types.go | 798 +++++++++++++++++++++ x/committee/legacy/v0_9/types.go | 810 ++++++++++++++++++++++ x/committee/types/permissions.go | 20 + 7 files changed, 2005 insertions(+), 1 deletion(-) create mode 100644 migrate/v0_11/testdata/committee-v09.json create mode 100644 x/committee/legacy/v0_11/types.go create mode 100644 x/committee/legacy/v0_9/types.go diff --git a/migrate/v0_11/migrate.go b/migrate/v0_11/migrate.go index 7e8baffc..8062b83b 100644 --- a/migrate/v0_11/migrate.go +++ b/migrate/v0_11/migrate.go @@ -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 diff --git a/migrate/v0_11/migrate_test.go b/migrate/v0_11/migrate_test.go index fae6a0d6..efbe227d 100644 --- a/migrate/v0_11/migrate_test.go +++ b/migrate/v0_11/migrate_test.go @@ -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) diff --git a/migrate/v0_11/testdata/committee-v09.json b/migrate/v0_11/testdata/committee-v09.json new file mode 100644 index 00000000..7d43632a --- /dev/null +++ b/migrate/v0_11/testdata/committee-v09.json @@ -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": [] +} \ No newline at end of file diff --git a/x/committee/alias.go b/x/committee/alias.go index e9f2fa72..35889c8b 100644 --- a/x/committee/alias.go +++ b/x/committee/alias.go @@ -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 diff --git a/x/committee/legacy/v0_11/types.go b/x/committee/legacy/v0_11/types.go new file mode 100644 index 00000000..68c1e717 --- /dev/null +++ b/x/committee/legacy/v0_11/types.go @@ -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, ¤tCP) // 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, ¤tDP) // 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, ¤tAPs) // 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, ¤tMs) // 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 +} diff --git a/x/committee/legacy/v0_9/types.go b/x/committee/legacy/v0_9/types.go new file mode 100644 index 00000000..d1c5f566 --- /dev/null +++ b/x/committee/legacy/v0_9/types.go @@ -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, ¤tCP) // 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, ¤tDP) // 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, ¤tAPs) // 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, ¤tMs) // 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) +} diff --git a/x/committee/types/permissions.go b/x/committee/types/permissions.go index f6868570..3b5a3921 100644 --- a/x/committee/types/permissions.go +++ b/x/committee/types/permissions.go @@ -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) &&