Committtee audit revisions (#510)

* comments from review

Co-authored-by: Sunny Aggarwal <sunnya97@protonmail.ch>
Co-authored-by: jmahess <maheswaran@google.com>
Co-authored-by: Alexander Bezobchuk <alexanderbez@users.noreply.github.com>

* add vote methods

* add draft new param change permission

* add and update tests

* rename ParamChangePermission

* account for perms becoming invalid at a later time

* add debtParam to permission

* add bep3 AssetParam to permissions

* add pricefeed Markets to permission

* add upgrade permission

* move proposal passing to the begin blocker

* fix iteration bug

Co-authored-by: Federico Kunze <31522760+fedekunze@users.noreply.github.com>

* address todos and audit comments

* add proposal examples

* refactor handler to be easier to read

* address review comments

* update comments

Co-authored-by: Kevin Davis <kjydavis3@gmail.com>
Co-authored-by: Sunny Aggarwal <sunnya97@protonmail.ch>
Co-authored-by: jmahess <maheswaran@google.com>
Co-authored-by: Alexander Bezobchuk <alexanderbez@users.noreply.github.com>
Co-authored-by: Federico Kunze <31522760+fedekunze@users.noreply.github.com>
This commit is contained in:
Ruaridh 2020-05-15 20:25:49 +01:00 committed by GitHub
parent dd1d248be2
commit c28bc03248
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 2143 additions and 236 deletions

View File

@ -266,13 +266,15 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
committeeGovRouter.
AddRoute(gov.RouterKey, gov.ProposalHandler).
AddRoute(params.RouterKey, params.NewParamChangeProposalHandler(app.paramsKeeper)).
AddRoute(distr.RouterKey, distr.NewCommunityPoolSpendProposalHandler(app.distrKeeper))
AddRoute(distr.RouterKey, distr.NewCommunityPoolSpendProposalHandler(app.distrKeeper)).
AddRoute(upgrade.RouterKey, upgrade.NewSoftwareUpgradeProposalHandler(app.upgradeKeeper))
// Note: the committee proposal handler is not registered on the committee router. This means committees cannot create or update other committees.
// Adding the committee proposal handler to the router is possible but awkward as the handler depends on the keeper which depends on the handler.
app.committeeKeeper = committee.NewKeeper(
app.cdc,
keys[committee.StoreKey],
committeeGovRouter, // TODO blacklist module addresses?
app.paramsKeeper,
)
// create gov keeper with router

View File

@ -0,0 +1,123 @@
{
"type": "kava/CommitteeChangeProposal",
"value": {
"title": "Update Committee 0",
"description": "This is a proposal that updates committee with id 0",
"new_committee": {
"id": "0",
"description": "This committee is for adjusting parameters of the cdp system.",
"members": [
"kava1vysxvcttv5sxzerywfjhxucazsxj0",
"kava1v9hx7argv4ezqenpddjjqctyv3ex2umndpth74"
],
"permissions": [
{
"type": "kava/SubParamChangePermission",
"value": {
"allowed_params": [
{
"subspace": "cdp",
"key": "GlobalDebtLimit"
},
{
"subspace": "cdp",
"key": "SurplusThreshold"
},
{
"subspace": "cdp",
"key": "DebtThreshold"
},
{
"subspace": "cdp",
"key": "DistributionFrequency"
},
{
"subspace": "cdp",
"key": "CircuitBreaker"
},
{
"subspace": "cdp",
"key": "CollateralParams"
},
{
"subspace": "cdp",
"key": "DebtParam"
},
{
"subspace": "auction",
"key": "BidDuration"
},
{
"subspace": "auction",
"key": "IncrementSurplus"
},
{
"subspace": "auction",
"key": "IncrementDebt"
},
{
"subspace": "auction",
"key": "IncrementCollateral"
},
{
"subspace": "bep3",
"key": "SupportedAssets"
},
{
"subspace": "pricefeed",
"key": "Markets"
},
{
"subspace": "incentive",
"key": "Active"
},
{
"subspace": "kavadist",
"key": "Active"
}
],
"allowed_collateral_params": [
{
"denom": "bnb",
"liquidation_ratio": false,
"debt_limit": true,
"stability_fee": true,
"auction_size": true,
"liquidation_penalty": false,
"prefix": false,
"market_id": false,
"conversion_factor": false
}
],
"allowed_debt_param": {
"denom": false,
"reference_asset": false,
"conversion_factor": false,
"debt_floor": false,
"savings_rate": true
},
"allowed_asset_params": [
{
"denom": "bnb",
"coin_id": false,
"limit": true,
"active": true
}
],
"allowed_markets": [
{
"market_id": "bnb:usd",
"base_asset": false,
"quote_asset": false,
"oracles": false,
"active": true
}
]
}
}
],
"vote_threshold": "0.750000000000000000",
"proposal_duration": "604800000000000"
}
}
}

View File

@ -0,0 +1,12 @@
{
"type": "cosmos-sdk/SoftwareUpgradeProposal",
"value": {
"title": "Upgrade to v0.24.1",
"description": "This proposal will halt the chain after the time below, and restart when 2/3 validators have installed the newer version.",
"plan": {
"name": "Upgrade to v0.24.1",
"time": "2020-05-15T00:00:00Z",
"info": "[some additional information about the upgrade]"
}
}
}

View File

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

View File

@ -8,10 +8,14 @@ import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/gov"
"github.com/cosmos/cosmos-sdk/x/params"
"github.com/cosmos/cosmos-sdk/x/upgrade"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/cdp"
cdptypes "github.com/kava-labs/kava/x/cdp/types"
"github.com/kava-labs/kava/x/committee"
)
@ -32,7 +36,7 @@ func (suite *ModuleTestSuite) SetupTest() {
_, suite.addresses = app.GeneratePrivKeyAddressPairs(5)
}
func (suite *ModuleTestSuite) TestBeginBlock() {
func (suite *ModuleTestSuite) TestBeginBlock_ClosesExpired() {
suite.app.InitializeFromGenesisStates()
normalCom := committee.Committee{
@ -44,12 +48,12 @@ func (suite *ModuleTestSuite) TestBeginBlock() {
}
suite.keeper.SetCommittee(suite.ctx, normalCom)
pprop1 := gov.NewTextProposal("1A Title", "A description of this proposal.")
pprop1 := gov.NewTextProposal("Title 1", "A description of this proposal.")
id1, err := suite.keeper.SubmitProposal(suite.ctx, normalCom.Members[0], normalCom.ID, pprop1)
suite.NoError(err)
oneHrLaterCtx := suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(time.Hour))
pprop2 := gov.NewTextProposal("2A Title", "A description of this proposal.")
pprop2 := gov.NewTextProposal("Title 2", "A description of this proposal.")
id2, err := suite.keeper.SubmitProposal(oneHrLaterCtx, normalCom.Members[0], normalCom.ID, pprop2)
suite.NoError(err)
@ -66,6 +70,161 @@ func (suite *ModuleTestSuite) TestBeginBlock() {
suite.True(found, "expected non expired proposal to be not closed")
}
func (suite *ModuleTestSuite) TestBeginBlock_EnactsPassed() {
suite.app.InitializeFromGenesisStates()
// setup committee
normalCom := committee.Committee{
ID: 12,
Members: suite.addresses[:2],
Permissions: []committee.Permission{committee.GodPermission{}},
VoteThreshold: d("0.8"),
ProposalDuration: time.Hour * 24 * 7,
}
suite.keeper.SetCommittee(suite.ctx, normalCom)
// setup 2 proposals
previousCDPDebtThreshold := suite.app.GetCDPKeeper().GetParams(suite.ctx).DebtAuctionThreshold
newDebtThreshold := previousCDPDebtThreshold.Add(i(1000000))
evenNewerDebtThreshold := newDebtThreshold.Add(i(1000000))
pprop1 := params.NewParameterChangeProposal("Title 1", "A description of this proposal.",
[]params.ParamChange{{
Subspace: cdptypes.ModuleName,
Key: string(cdp.KeyDebtThreshold),
Value: string(cdp.ModuleCdc.MustMarshalJSON(newDebtThreshold)),
}},
)
id1, err := suite.keeper.SubmitProposal(suite.ctx, normalCom.Members[0], normalCom.ID, pprop1)
suite.NoError(err)
pprop2 := params.NewParameterChangeProposal("Title 2", "A description of this proposal.",
[]params.ParamChange{{
Subspace: cdptypes.ModuleName,
Key: string(cdp.KeyDebtThreshold),
Value: string(cdp.ModuleCdc.MustMarshalJSON(evenNewerDebtThreshold)),
}},
)
id2, err := suite.keeper.SubmitProposal(suite.ctx, normalCom.Members[0], normalCom.ID, pprop2)
suite.NoError(err)
// add enough votes to make the first proposal pass, but not the second
suite.NoError(suite.keeper.AddVote(suite.ctx, id1, suite.addresses[0]))
suite.NoError(suite.keeper.AddVote(suite.ctx, id1, suite.addresses[1]))
suite.NoError(suite.keeper.AddVote(suite.ctx, id2, suite.addresses[0]))
// Run BeginBlocker
suite.NotPanics(func() {
committee.BeginBlocker(suite.ctx, abci.RequestBeginBlock{}, suite.keeper)
})
// Check the param has been updated
suite.Equal(newDebtThreshold, suite.app.GetCDPKeeper().GetParams(suite.ctx).DebtAuctionThreshold)
// Check the passed proposal has gone
_, found := suite.keeper.GetProposal(suite.ctx, id1)
suite.False(found, "expected passed proposal to be enacted and closed")
_, found = suite.keeper.GetProposal(suite.ctx, id2)
suite.True(found, "expected non passed proposal to be not closed")
}
func (suite *ModuleTestSuite) TestBeginBlock_DoesntEnactFailed() {
suite.app.InitializeFromGenesisStates()
// setup committee
normalCom := committee.Committee{
ID: 12,
Members: suite.addresses[:1],
Permissions: []committee.Permission{committee.SoftwareUpgradePermission{}},
VoteThreshold: d("1.0"),
ProposalDuration: time.Hour * 24 * 7,
}
firstBlockTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
ctx := suite.ctx.WithBlockTime(firstBlockTime)
suite.keeper.SetCommittee(ctx, normalCom)
// setup an upgrade proposal
pprop1 := upgrade.NewSoftwareUpgradeProposal("Title 1", "A description of this proposal.",
upgrade.Plan{
Name: "upgrade-version-v0.23.1",
Time: firstBlockTime.Add(time.Second * 5),
Info: "some information about the upgrade",
},
)
id1, err := suite.keeper.SubmitProposal(ctx, normalCom.Members[0], normalCom.ID, pprop1)
suite.NoError(err)
// add enough votes to make the proposal pass
suite.NoError(suite.keeper.AddVote(ctx, id1, suite.addresses[0]))
// Run BeginBlocker 10 seconds later (5 seconds after upgrade expires)
tenSecLaterCtx := ctx.WithBlockTime(ctx.BlockTime().Add(time.Second * 10))
suite.NotPanics(func() {
suite.app.BeginBlocker(tenSecLaterCtx, abci.RequestBeginBlock{})
})
// Check the plan has not been stored
_, found := suite.app.GetUpgradeKeeper().GetUpgradePlan(tenSecLaterCtx)
suite.False(found)
// Check the passed proposal has gone
_, found = suite.keeper.GetProposal(tenSecLaterCtx, id1)
suite.False(found, "expected failed proposal to be not enacted and closed")
// Check the chain doesn't halt
oneMinLaterCtx := ctx.WithBlockTime(ctx.BlockTime().Add(time.Minute).Add(time.Second))
suite.NotPanics(func() {
suite.app.BeginBlocker(oneMinLaterCtx, abci.RequestBeginBlock{})
})
}
func (suite *ModuleTestSuite) TestBeginBlock_EnactsPassedUpgrade() {
suite.app.InitializeFromGenesisStates()
// setup committee
normalCom := committee.Committee{
ID: 12,
Members: suite.addresses[:1],
Permissions: []committee.Permission{committee.SoftwareUpgradePermission{}},
VoteThreshold: d("1.0"),
ProposalDuration: time.Hour * 24 * 7,
}
firstBlockTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
ctx := suite.ctx.WithBlockTime(firstBlockTime)
suite.keeper.SetCommittee(ctx, normalCom)
// setup an upgrade proposal
pprop1 := upgrade.NewSoftwareUpgradeProposal("Title 1", "A description of this proposal.",
upgrade.Plan{
Name: "upgrade-version-v0.23.1",
Time: firstBlockTime.Add(time.Minute * 1),
Info: "some information about the upgrade",
},
)
id1, err := suite.keeper.SubmitProposal(ctx, normalCom.Members[0], normalCom.ID, pprop1)
suite.NoError(err)
// add enough votes to make the proposal pass
suite.NoError(suite.keeper.AddVote(ctx, id1, suite.addresses[0]))
// Run BeginBlocker
fiveSecLaterCtx := ctx.WithBlockTime(ctx.BlockTime().Add(time.Second * 5))
suite.NotPanics(func() {
suite.app.BeginBlocker(fiveSecLaterCtx, abci.RequestBeginBlock{})
})
// Check the plan has been stored
_, found := suite.app.GetUpgradeKeeper().GetUpgradePlan(fiveSecLaterCtx)
suite.True(found)
// Check the passed proposal has gone
_, found = suite.keeper.GetProposal(fiveSecLaterCtx, id1)
suite.False(found, "expected passed proposal to be enacted and closed")
// Check the chain halts
oneMinLaterCtx := ctx.WithBlockTime(ctx.BlockTime().Add(time.Minute))
suite.Panics(func() {
suite.app.BeginBlocker(oneMinLaterCtx, abci.RequestBeginBlock{})
})
}
func TestModuleTestSuite(t *testing.T) {
suite.Run(t, new(ModuleTestSuite))
}

View File

@ -61,6 +61,7 @@ var (
NewQueryCommitteeParams = types.NewQueryCommitteeParams
NewQueryProposalParams = types.NewQueryProposalParams
NewQueryVoteParams = types.NewQueryVoteParams
NewVote = types.NewVote
RegisterCodec = types.RegisterCodec
RegisterPermissionTypeCodec = types.RegisterPermissionTypeCodec
RegisterProposalTypeCodec = types.RegisterProposalTypeCodec
@ -85,6 +86,13 @@ var (
type (
Keeper = keeper.Keeper
AllowedAssetParam = types.AllowedAssetParam
AllowedAssetParams = types.AllowedAssetParams
AllowedCollateralParam = types.AllowedCollateralParam
AllowedCollateralParams = types.AllowedCollateralParams
AllowedDebtParam = types.AllowedDebtParam
AllowedMarket = types.AllowedMarket
AllowedMarkets = types.AllowedMarkets
AllowedParam = types.AllowedParam
AllowedParams = types.AllowedParams
Committee = types.Committee
@ -94,13 +102,16 @@ type (
GodPermission = types.GodPermission
MsgSubmitProposal = types.MsgSubmitProposal
MsgVote = types.MsgVote
ParamChangePermission = types.ParamChangePermission
ParamKeeper = types.ParamKeeper
Permission = types.Permission
Proposal = types.Proposal
PubProposal = types.PubProposal
QueryCommitteeParams = types.QueryCommitteeParams
QueryProposalParams = types.QueryProposalParams
QueryVoteParams = types.QueryVoteParams
SimpleParamChangePermission = types.SimpleParamChangePermission
SoftwareUpgradePermission = types.SoftwareUpgradePermission
SubParamChangePermission = types.SubParamChangePermission
TextPermission = types.TextPermission
Vote = types.Vote
)

View File

@ -197,7 +197,7 @@ func MustGetExampleCommitteeChangeProposal(cdc *codec.Codec) string {
"The description of this committee.",
[]sdk.AccAddress{sdk.AccAddress(crypto.AddressHash([]byte("exampleAddress")))},
[]types.Permission{
types.ParamChangePermission{
types.SimpleParamChangePermission{
AllowedParams: types.AllowedParams{{Subspace: "cdp", Key: "CircuitBreaker"}},
},
},

View File

@ -1,8 +1,6 @@
package committee
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
@ -47,16 +45,11 @@ func handleMsgSubmitProposal(ctx sdk.Context, k keeper.Keeper, msg types.MsgSubm
}
func handleMsgVote(ctx sdk.Context, k keeper.Keeper, msg types.MsgVote) (*sdk.Result, error) {
// get the proposal just to add fields to the event
proposal, found := k.GetProposal(ctx, msg.ProposalID)
if !found {
return nil, sdkerrors.Wrapf(ErrUnknownProposal, "%d", msg.ProposalID)
}
err := k.AddVote(ctx, msg.ProposalID, msg.Voter)
if err != nil {
return nil, err
}
ctx.EventManager().EmitEvent(
sdk.NewEvent(
sdk.EventTypeMessage,
@ -65,31 +58,5 @@ func handleMsgVote(ctx sdk.Context, k keeper.Keeper, msg types.MsgVote) (*sdk.Re
),
)
// Enact a proposal if it has enough votes
passes, err := k.GetProposalResult(ctx, msg.ProposalID)
if err != nil {
return nil, err
}
if !passes {
return &sdk.Result{Events: ctx.EventManager().Events()}, nil
}
err = k.EnactProposal(ctx, msg.ProposalID)
outcome := types.AttributeValueProposalPassed
if err != nil {
outcome = types.AttributeValueProposalFailed
}
k.DeleteProposalAndVotes(ctx, msg.ProposalID)
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeProposalClose,
sdk.NewAttribute(types.AttributeKeyCommitteeID, fmt.Sprintf("%d", proposal.CommitteeID)),
sdk.NewAttribute(types.AttributeKeyProposalID, fmt.Sprintf("%d", proposal.ID)),
sdk.NewAttribute(types.AttributeKeyProposalCloseStatus, outcome),
),
)
return &sdk.Result{Events: ctx.EventManager().Events()}, nil
}

View File

@ -9,6 +9,7 @@ import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/distribution"
"github.com/cosmos/cosmos-sdk/x/params"
"github.com/cosmos/cosmos-sdk/x/upgrade"
abci "github.com/tendermint/tendermint/abci/types"
@ -116,6 +117,28 @@ func (suite *HandlerTestSuite) TestSubmitProposalMsg_Invalid() {
}
func (suite *HandlerTestSuite) TestSubmitProposalMsg_ValidUpgrade() {
msg := committee.NewMsgSubmitProposal(
upgrade.NewSoftwareUpgradeProposal(
"A Title",
"A description of this proposal.",
upgrade.Plan{
Name: "emergency-shutdown-1", // identifier for the upgrade
Time: suite.ctx.BlockTime().Add(time.Minute * 10), // time after which to implement plan
Info: "Some information about the shutdown.",
},
),
suite.addresses[0],
1,
)
res, err := suite.handler(suite.ctx, msg)
suite.NoError(err)
_, found := suite.keeper.GetProposal(suite.ctx, types.Uint64FromBytes(res.Data))
suite.True(found)
}
func (suite *HandlerTestSuite) TestSubmitProposalMsg_Unregistered() {
var committeeID uint64 = 1
msg := types.NewMsgSubmitProposal(
@ -133,80 +156,6 @@ func (suite *HandlerTestSuite) TestSubmitProposalMsg_Unregistered() {
)
}
func (suite *HandlerTestSuite) TestMsgAddVote_ProposalPass() {
previousCDPDebtThreshold := suite.app.GetCDPKeeper().GetParams(suite.ctx).DebtAuctionThreshold
newDebtThreshold := previousCDPDebtThreshold.Add(i(1000000))
msg := types.NewMsgSubmitProposal(
params.NewParameterChangeProposal(
"A Title",
"A description of this proposal.",
[]params.ParamChange{{
Subspace: cdptypes.ModuleName,
Key: string(cdptypes.KeyDebtThreshold),
Value: string(types.ModuleCdc.MustMarshalJSON(newDebtThreshold)),
}},
),
suite.addresses[0],
1,
)
res, err := suite.handler(suite.ctx, msg)
suite.NoError(err)
proposalID := types.Uint64FromBytes(res.Data)
_, err = suite.handler(suite.ctx, types.NewMsgVote(suite.addresses[0], proposalID))
suite.NoError(err)
// Add a vote to make the proposal pass
_, err = suite.handler(suite.ctx, types.NewMsgVote(suite.addresses[1], proposalID))
suite.NoError(err)
// Check the param has been updated
suite.Equal(newDebtThreshold, suite.app.GetCDPKeeper().GetParams(suite.ctx).DebtAuctionThreshold)
// Check proposal and votes are gone
_, found := suite.keeper.GetProposal(suite.ctx, proposalID)
suite.False(found)
suite.Empty(
suite.keeper.GetVotesByProposal(suite.ctx, proposalID),
"vote found when there should be none",
)
}
func (suite *HandlerTestSuite) TestMsgAddVote_ProposalFail() {
recipient := suite.addresses[4]
recipientCoins := suite.app.GetBankKeeper().GetCoins(suite.ctx, recipient)
msg := types.NewMsgSubmitProposal(
distribution.NewCommunityPoolSpendProposal(
"A Title",
"A description of this proposal.",
recipient,
cs(c("ukava", 500)),
),
suite.addresses[0],
1,
)
res, err := suite.handler(suite.ctx, msg)
suite.NoError(err)
proposalID := types.Uint64FromBytes(res.Data)
_, err = suite.handler(suite.ctx, types.NewMsgVote(suite.addresses[0], proposalID))
suite.NoError(err)
// invalidate the proposal by emptying community pool
suite.app.GetDistrKeeper().DistributeFromFeePool(suite.ctx, suite.communityPoolAmt, suite.addresses[0])
// Add a vote to make the proposal pass
_, err = suite.handler(suite.ctx, types.NewMsgVote(suite.addresses[1], proposalID))
suite.NoError(err)
// Check the proposal was not enacted
suite.Equal(recipientCoins, suite.app.GetBankKeeper().GetCoins(suite.ctx, recipient))
// Check proposal and votes are gone
_, found := suite.keeper.GetProposal(suite.ctx, proposalID)
suite.False(found)
suite.Empty(
suite.keeper.GetVotesByProposal(suite.ctx, proposalID),
"vote found when there should be none",
)
}
func TestHandlerTestSuite(t *testing.T) {
suite.Run(t, new(HandlerTestSuite))
}

View File

@ -1,24 +1,19 @@
package types
package keeper_test
import (
"testing"
"time"
"github.com/stretchr/testify/suite"
abci "github.com/tendermint/tendermint/abci/types"
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
paramstypes "github.com/cosmos/cosmos-sdk/x/params/types"
"github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/committee/types"
)
var _ PubProposal = UnregisteredPubProposal{}
type UnregisteredPubProposal struct {
govtypes.TextProposal
}
func (UnregisteredPubProposal) ProposalRoute() string { return "unregistered" }
func (UnregisteredPubProposal) ProposalType() string { return "unregistered" }
type TypesTestSuite struct {
suite.Suite
}
@ -27,14 +22,14 @@ func (suite *TypesTestSuite) TestCommittee_HasPermissionsFor() {
testcases := []struct {
name string
permissions []Permission
pubProposal PubProposal
permissions []types.Permission
pubProposal types.PubProposal
expectHasPermissions bool
}{
{
name: "normal (single permission)",
permissions: []Permission{ParamChangePermission{
AllowedParams: AllowedParams{
permissions: []types.Permission{types.SimpleParamChangePermission{
AllowedParams: types.AllowedParams{
{
Subspace: "cdp",
Key: "DebtThreshold",
@ -56,30 +51,30 @@ func (suite *TypesTestSuite) TestCommittee_HasPermissionsFor() {
},
{
name: "normal (multiple permissions)",
permissions: []Permission{
ParamChangePermission{
AllowedParams: AllowedParams{
permissions: []types.Permission{
types.SimpleParamChangePermission{
AllowedParams: types.AllowedParams{
{
Subspace: "cdp",
Key: "DebtThreshold",
},
}},
TextPermission{},
types.TextPermission{},
},
pubProposal: govtypes.NewTextProposal("A Proposal Title", "A description of this proposal"),
expectHasPermissions: true,
},
{
name: "overruling permission",
permissions: []Permission{
ParamChangePermission{
AllowedParams: AllowedParams{
permissions: []types.Permission{
types.SimpleParamChangePermission{
AllowedParams: types.AllowedParams{
{
Subspace: "cdp",
Key: "DebtThreshold",
},
}},
GodPermission{},
types.GodPermission{},
},
pubProposal: paramstypes.NewParameterChangeProposal(
"A Title",
@ -115,16 +110,16 @@ func (suite *TypesTestSuite) TestCommittee_HasPermissionsFor() {
{
name: "split permissions",
// These permissions looks like they allow the param change proposal, however a proposal must pass a single permission independently of others.
permissions: []Permission{
ParamChangePermission{
AllowedParams: AllowedParams{
permissions: []types.Permission{
types.SimpleParamChangePermission{
AllowedParams: types.AllowedParams{
{
Subspace: "cdp",
Key: "DebtThreshold",
},
}},
ParamChangePermission{
AllowedParams: AllowedParams{
types.SimpleParamChangePermission{
AllowedParams: types.AllowedParams{
{
Subspace: "cdp",
Key: "DebtParams",
@ -153,9 +148,9 @@ func (suite *TypesTestSuite) TestCommittee_HasPermissionsFor() {
},
{
name: "unregistered proposal",
permissions: []Permission{
ParamChangePermission{
AllowedParams: AllowedParams{
permissions: []types.Permission{
types.SimpleParamChangePermission{
AllowedParams: types.AllowedParams{
{
Subspace: "cdp",
Key: "DebtThreshold",
@ -169,7 +164,10 @@ func (suite *TypesTestSuite) TestCommittee_HasPermissionsFor() {
for _, tc := range testcases {
suite.Run(tc.name, func() {
com := NewCommittee(
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{})
tApp.InitializeFromGenesisStates()
com := types.NewCommittee(
12,
"a description of this committee",
nil,
@ -179,7 +177,7 @@ func (suite *TypesTestSuite) TestCommittee_HasPermissionsFor() {
)
suite.Equal(
tc.expectHasPermissions,
com.HasPermissionsFor(tc.pubProposal),
com.HasPermissionsFor(ctx, tApp.Codec(), tApp.GetParamsKeeper(), tc.pubProposal),
)
})
}

View File

@ -71,17 +71,12 @@ func ValidProposalsInvariant(k Keeper) sdk.Invariant {
}
}
com, found := k.GetCommittee(ctx, proposal.CommitteeID)
_, found := k.GetCommittee(ctx, proposal.CommitteeID)
if !found {
validationErr = fmt.Errorf("proposal has no committee %d", proposal.CommitteeID)
return true
}
if !com.HasPermissionsFor(proposal.PubProposal) {
validationErr = fmt.Errorf("proposal not permitted for committee %+v", com)
return true
}
return false
})
@ -107,8 +102,8 @@ func ValidVotesInvariant(k Keeper) sdk.Invariant {
k.IterateVotes(ctx, func(vote types.Vote) bool {
invalidVote = vote
if vote.Voter.Empty() {
validationErr = fmt.Errorf("empty voter address")
if err := vote.Validate(); err != nil {
validationErr = err
return true
}

View File

@ -16,11 +16,13 @@ type Keeper struct {
cdc *codec.Codec
storeKey sdk.StoreKey
ParamKeeper types.ParamKeeper // TODO ideally don't export, only sims need it exported
// Proposal router
router govtypes.Router
}
func NewKeeper(cdc *codec.Codec, storeKey sdk.StoreKey, router govtypes.Router) Keeper {
func NewKeeper(cdc *codec.Codec, storeKey sdk.StoreKey, router govtypes.Router, paramKeeper types.ParamKeeper) Keeper {
// Logic in the keeper methods assume the set of gov handlers is fixed.
// So the gov router must be sealed so no handlers can be added or removed after the keeper is created.
router.Seal()
@ -28,6 +30,7 @@ func NewKeeper(cdc *codec.Codec, storeKey sdk.StoreKey, router govtypes.Router)
return Keeper{
cdc: cdc,
storeKey: storeKey,
ParamKeeper: paramKeeper,
router: router,
}
}
@ -271,11 +274,14 @@ func (k Keeper) GetVotes(ctx sdk.Context) []types.Vote {
// GetVotesByProposal returns all votes for one proposal.
func (k Keeper) GetVotesByProposal(ctx sdk.Context, proposalID uint64) []types.Vote {
results := []types.Vote{}
k.IterateVotes(ctx, func(vote types.Vote) bool {
if vote.ProposalID == proposalID {
iterator := sdk.KVStorePrefixIterator(ctx.KVStore(k.storeKey), append(types.VoteKeyPrefix, types.GetKeyFromID(proposalID)...))
defer iterator.Close()
for ; iterator.Valid(); iterator.Next() {
var vote types.Vote
k.cdc.MustUnmarshalBinaryBare(iterator.Value(), &vote)
results = append(results, vote)
}
return false
})
return results
}

View File

@ -0,0 +1,239 @@
package keeper_test
import (
"testing"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
paramstypes "github.com/cosmos/cosmos-sdk/x/params/types"
"github.com/stretchr/testify/suite"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/kava-labs/kava/app"
bep3types "github.com/kava-labs/kava/x/bep3/types"
cdptypes "github.com/kava-labs/kava/x/cdp/types"
"github.com/kava-labs/kava/x/committee/types"
pricefeedtypes "github.com/kava-labs/kava/x/pricefeed/types"
)
type PermissionTestSuite struct {
suite.Suite
cdc *codec.Codec
}
func (suite *PermissionTestSuite) SetupTest() {
app := app.NewTestApp()
suite.cdc = app.Codec()
}
func (suite *PermissionTestSuite) TestSubParamChangePermission_Allows() {
// cdp CollateralParams
testCPs := cdptypes.CollateralParams{
{
Denom: "bnb",
LiquidationRatio: d("2.0"),
DebtLimit: c("usdx", 1000000000000),
StabilityFee: d("1.000000001547125958"),
LiquidationPenalty: d("0.05"),
AuctionSize: i(100),
Prefix: 0x20,
ConversionFactor: i(6),
MarketID: "bnb:usd",
},
{
Denom: "btc",
LiquidationRatio: d("1.5"),
DebtLimit: c("usdx", 1000000000),
StabilityFee: d("1.000000001547125958"),
LiquidationPenalty: d("0.10"),
AuctionSize: i(1000),
Prefix: 0x30,
ConversionFactor: i(8),
MarketID: "btc:usd",
},
}
testCPUpdatedDebtLimit := make(cdptypes.CollateralParams, len(testCPs))
copy(testCPUpdatedDebtLimit, testCPs)
testCPUpdatedDebtLimit[0].DebtLimit = c("usdx", 5000000)
// cdp DebtParam
testDP := cdptypes.DebtParam{
Denom: "usdx",
ReferenceAsset: "usd",
ConversionFactor: i(6),
DebtFloor: i(10000000),
SavingsRate: d("0.95"),
}
testDPUpdatedDebtFloor := testDP
testDPUpdatedDebtFloor.DebtFloor = i(1000)
// cdp Genesis
testCDPParams := cdptypes.DefaultParams()
testCDPParams.CollateralParams = testCPs
testCDPParams.DebtParam = testDP
testCDPParams.GlobalDebtLimit = testCPs[0].DebtLimit.Add(testCPs[0].DebtLimit) // correct global debt limit to pass genesis validation
// bep3 Asset Params
testAPs := bep3types.AssetParams{
{
Denom: "bnb",
CoinID: 714,
Limit: i(100000000000),
Active: true,
},
{
Denom: "inc",
CoinID: 9999,
Limit: i(100),
Active: false,
},
}
testAPsUpdatedActive := make(bep3types.AssetParams, len(testAPs))
copy(testAPsUpdatedActive, testAPs)
testAPsUpdatedActive[1].Active = true
// bep3 Genesis
testBep3Params := bep3types.DefaultParams()
testBep3Params.SupportedAssets = testAPs
// pricefeed Markets
testMs := pricefeedtypes.Markets{
{
MarketID: "bnb:usd",
BaseAsset: "bnb",
QuoteAsset: "usd",
Oracles: []sdk.AccAddress{},
Active: true,
},
{
MarketID: "btc:usd",
BaseAsset: "btc",
QuoteAsset: "usd",
Oracles: []sdk.AccAddress{},
Active: true,
},
}
testMsUpdatedActive := make(pricefeedtypes.Markets, len(testMs))
copy(testMsUpdatedActive, testMs)
testMsUpdatedActive[1].Active = true
testcases := []struct {
name string
genState []app.GenesisState
permission types.SubParamChangePermission
pubProposal types.PubProposal
expectAllowed bool
}{
{
name: "normal",
genState: []app.GenesisState{
newPricefeedGenState([]string{"bnb", "btc"}, []sdk.Dec{d("15.01"), d("9500")}),
newCDPGenesisState(testCDPParams),
newBep3GenesisState(testBep3Params),
},
permission: types.SubParamChangePermission{
AllowedParams: types.AllowedParams{
{Subspace: cdptypes.ModuleName, Key: string(cdptypes.KeyDebtThreshold)},
{Subspace: cdptypes.ModuleName, Key: string(cdptypes.KeyCollateralParams)},
{Subspace: cdptypes.ModuleName, Key: string(cdptypes.KeyDebtParam)},
{Subspace: bep3types.ModuleName, Key: string(bep3types.KeySupportedAssets)},
{Subspace: pricefeedtypes.ModuleName, Key: string(pricefeedtypes.KeyMarkets)},
},
AllowedCollateralParams: types.AllowedCollateralParams{
{
Denom: "bnb",
DebtLimit: true,
StabilityFee: true,
},
{ // TODO currently even if a perm doesn't allow a change in one element it must still be present in list
Denom: "btc",
},
},
AllowedDebtParam: types.AllowedDebtParam{
DebtFloor: true,
},
AllowedAssetParams: types.AllowedAssetParams{
{
Denom: "bnb",
},
{
Denom: "inc",
Active: true,
},
},
AllowedMarkets: types.AllowedMarkets{
{
MarketID: "bnb:usd",
},
{
MarketID: "btc:usd",
Active: true,
},
},
},
pubProposal: paramstypes.NewParameterChangeProposal(
"A Title",
"A description for this proposal.",
[]paramstypes.ParamChange{
{
Subspace: cdptypes.ModuleName,
Key: string(cdptypes.KeyDebtThreshold),
Value: string(suite.cdc.MustMarshalJSON(i(1234))),
},
{
Subspace: cdptypes.ModuleName,
Key: string(cdptypes.KeyCollateralParams),
Value: string(suite.cdc.MustMarshalJSON(testCPUpdatedDebtLimit)),
},
{
Subspace: cdptypes.ModuleName,
Key: string(cdptypes.KeyDebtParam),
Value: string(suite.cdc.MustMarshalJSON(testDPUpdatedDebtFloor)),
},
{
Subspace: bep3types.ModuleName,
Key: string(bep3types.KeySupportedAssets),
Value: string(suite.cdc.MustMarshalJSON(testAPsUpdatedActive)),
},
{
Subspace: pricefeedtypes.ModuleName,
Key: string(pricefeedtypes.KeyMarkets),
Value: string(suite.cdc.MustMarshalJSON(testMsUpdatedActive)),
},
},
),
expectAllowed: true,
},
{
name: "not allowed (wrong pubproposal type)",
permission: types.SubParamChangePermission{},
pubProposal: govtypes.NewTextProposal("A Title", "A description for this proposal."),
expectAllowed: false,
},
{
name: "not allowed (nil pubproposal)",
permission: types.SubParamChangePermission{},
pubProposal: nil,
expectAllowed: false,
},
// TODO more cases
}
for _, tc := range testcases {
suite.Run(tc.name, func() {
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{})
tApp.InitializeFromGenesisStates(tc.genState...)
suite.Equal(
tc.expectAllowed,
tc.permission.Allows(ctx, tApp.Codec(), tApp.GetParamsKeeper(), tc.pubProposal),
)
})
}
}
func TestPermissionTestSuite(t *testing.T) {
suite.Run(t, new(PermissionTestSuite))
}

View File

@ -21,7 +21,7 @@ func (k Keeper) SubmitProposal(ctx sdk.Context, proposer sdk.AccAddress, committ
}
// Check committee has permissions to enact proposal.
if !com.HasPermissionsFor(pubProposal) {
if !com.HasPermissionsFor(ctx, k.cdc, k.ParamKeeper, pubProposal) {
return 0, sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "committee does not have permissions to enact proposal")
}
@ -67,7 +67,7 @@ func (k Keeper) AddVote(ctx sdk.Context, proposalID uint64, voter sdk.AccAddress
}
// Store vote, overwriting any prior vote
k.SetVote(ctx, types.Vote{ProposalID: proposalID, Voter: voter})
k.SetVote(ctx, types.NewVote(proposalID, voter))
ctx.EventManager().EmitEvent(
sdk.NewEvent(
@ -107,23 +107,61 @@ func (k Keeper) TallyVotes(ctx sdk.Context, proposalID uint64) int64 {
}
// EnactProposal makes the changes proposed in a proposal.
func (k Keeper) EnactProposal(ctx sdk.Context, proposalID uint64) error {
pr, found := k.GetProposal(ctx, proposalID)
func (k Keeper) EnactProposal(ctx sdk.Context, proposal types.Proposal) error {
// Check committee still has permissions for the proposal
// Since the proposal was submitted params could have changed, invalidating the permission of the committee.
com, found := k.GetCommittee(ctx, proposal.CommitteeID)
if !found {
return sdkerrors.Wrapf(types.ErrUnknownProposal, "%d", proposalID)
return sdkerrors.Wrapf(types.ErrUnknownCommittee, "%d", proposal.CommitteeID)
}
if !com.HasPermissionsFor(ctx, k.cdc, k.ParamKeeper, proposal.PubProposal) {
return sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "committee does not have permissions to enact proposal")
}
if err := k.ValidatePubProposal(ctx, pr.PubProposal); err != nil {
if err := k.ValidatePubProposal(ctx, proposal.PubProposal); err != nil {
return err
}
handler := k.router.GetRoute(pr.ProposalRoute())
if err := handler(ctx, pr.PubProposal); err != nil {
// enact the proposal
handler := k.router.GetRoute(proposal.ProposalRoute())
if err := handler(ctx, proposal.PubProposal); err != nil {
// the handler should not error as it was checked in ValidatePubProposal
panic(fmt.Sprintf("unexpected handler error: %s", err))
}
return nil
}
// EnactPassedProposals puts in place the changes proposed in any proposal that has enough votes
func (k Keeper) EnactPassedProposals(ctx sdk.Context) {
k.IterateProposals(ctx, func(proposal types.Proposal) bool {
passes, err := k.GetProposalResult(ctx, proposal.ID)
if err != nil {
panic(err)
}
if !passes {
return false
}
err = k.EnactProposal(ctx, proposal)
outcome := types.AttributeValueProposalPassed
if err != nil {
outcome = types.AttributeValueProposalFailed
}
k.DeleteProposalAndVotes(ctx, proposal.ID)
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeProposalClose,
sdk.NewAttribute(types.AttributeKeyCommitteeID, fmt.Sprintf("%d", proposal.CommitteeID)),
sdk.NewAttribute(types.AttributeKeyProposalID, fmt.Sprintf("%d", proposal.ID)),
sdk.NewAttribute(types.AttributeKeyProposalCloseStatus, outcome),
),
)
return false
})
}
// CloseExpiredProposals removes proposals (and associated votes) that have past their deadline.
func (k Keeper) CloseExpiredProposals(ctx sdk.Context) {

View File

@ -12,11 +12,49 @@ import (
abci "github.com/tendermint/tendermint/abci/types"
"github.com/kava-labs/kava/app"
bep3types "github.com/kava-labs/kava/x/bep3/types"
cdptypes "github.com/kava-labs/kava/x/cdp/types"
"github.com/kava-labs/kava/x/committee"
"github.com/kava-labs/kava/x/committee/types"
"github.com/kava-labs/kava/x/pricefeed"
)
func newCDPGenesisState(params cdptypes.Params) app.GenesisState {
genesis := cdptypes.DefaultGenesisState()
genesis.Params = params
return app.GenesisState{cdptypes.ModuleName: cdptypes.ModuleCdc.MustMarshalJSON(genesis)}
}
func newBep3GenesisState(params bep3types.Params) app.GenesisState {
genesis := bep3types.DefaultGenesisState()
genesis.Params = params
return app.GenesisState{bep3types.ModuleName: bep3types.ModuleCdc.MustMarshalJSON(genesis)}
}
func newPricefeedGenState(assets []string, prices []sdk.Dec) app.GenesisState {
if len(assets) != len(prices) {
panic("assets and prices must be the same length")
}
pfGenesis := pricefeed.DefaultGenesisState()
for i := range assets {
pfGenesis.Params.Markets = append(
pfGenesis.Params.Markets,
pricefeed.Market{
MarketID: assets[i] + ":usd", BaseAsset: assets[i], QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true,
})
pfGenesis.PostedPrices = append(
pfGenesis.PostedPrices,
pricefeed.PostedPrice{
MarketID: assets[i] + ":usd",
OracleAddress: sdk.AccAddress{},
Price: prices[i],
Expiry: time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC),
})
}
return app.GenesisState{pricefeed.ModuleName: pricefeed.ModuleCdc.MustMarshalJSON(pfGenesis)}
}
func (suite *KeeperTestSuite) TestSubmitProposal() {
normalCom := types.Committee{
ID: 12,
@ -26,9 +64,50 @@ func (suite *KeeperTestSuite) TestSubmitProposal() {
VoteThreshold: d("0.667"),
ProposalDuration: time.Hour * 24 * 7,
}
noPermissionsCom := normalCom
noPermissionsCom.Permissions = []types.Permission{}
paramChangePermissionsCom := normalCom
paramChangePermissionsCom.Permissions = []types.Permission{
types.SubParamChangePermission{
AllowedParams: types.AllowedParams{
{Subspace: cdptypes.ModuleName, Key: string(cdptypes.KeyDebtThreshold)},
{Subspace: cdptypes.ModuleName, Key: string(cdptypes.KeyCollateralParams)},
},
AllowedCollateralParams: types.AllowedCollateralParams{
types.AllowedCollateralParam{
Denom: "bnb",
DebtLimit: true,
StabilityFee: true,
},
},
},
}
testCP := cdptypes.CollateralParams{{
Denom: "bnb",
LiquidationRatio: d("1.5"),
DebtLimit: c("usdx", 1000000000000),
StabilityFee: d("1.000000001547125958"), // %5 apr
LiquidationPenalty: d("0.05"),
AuctionSize: i(100),
Prefix: 0x20,
ConversionFactor: i(6),
MarketID: "bnb:usd",
}}
testCDPParams := cdptypes.DefaultParams()
testCDPParams.CollateralParams = testCP
testCDPParams.GlobalDebtLimit = testCP[0].DebtLimit
newValidCP := make(cdptypes.CollateralParams, len(testCP))
copy(newValidCP, testCP)
newValidCP[0].DebtLimit = c("usdx", 500000000000)
newInvalidCP := make(cdptypes.CollateralParams, len(testCP))
copy(newInvalidCP, testCP)
newInvalidCP[0].MarketID = "btc:usd"
testcases := []struct {
name string
committee types.Committee
@ -38,13 +117,28 @@ func (suite *KeeperTestSuite) TestSubmitProposal() {
expectErr bool
}{
{
name: "normal",
name: "normal text proposal",
committee: normalCom,
pubProposal: gov.NewTextProposal("A Title", "A description of this proposal."),
proposer: normalCom.Members[0],
committeeID: normalCom.ID,
expectErr: false,
},
{
name: "normal param change proposal",
committee: normalCom,
pubProposal: params.NewParameterChangeProposal(
"A Title", "A description of this proposal.",
[]params.ParamChange{
{
Subspace: "cdp", Key: string(cdptypes.KeyDebtThreshold), Value: string(suite.app.Codec().MustMarshalJSON(i(1000000))),
},
},
),
proposer: normalCom.Members[0],
committeeID: normalCom.ID,
expectErr: false,
},
{
name: "invalid proposal",
committee: normalCom,
@ -77,6 +171,42 @@ func (suite *KeeperTestSuite) TestSubmitProposal() {
committeeID: noPermissionsCom.ID,
expectErr: true,
},
{
name: "valid sub param change",
committee: paramChangePermissionsCom,
pubProposal: params.NewParameterChangeProposal(
"A Title", "A description of this proposal.",
[]params.ParamChange{
{
"cdp", string(cdptypes.KeyDebtThreshold), string(suite.app.Codec().MustMarshalJSON(i(1000000000))),
},
{
"cdp", string(cdptypes.KeyCollateralParams), string(suite.app.Codec().MustMarshalJSON(newValidCP)),
},
},
),
proposer: paramChangePermissionsCom.Members[0],
committeeID: paramChangePermissionsCom.ID,
expectErr: false,
},
{
name: "invalid sub param change permission",
committee: paramChangePermissionsCom,
pubProposal: params.NewParameterChangeProposal(
"A Title", "A description of this proposal.",
[]params.ParamChange{
{
"cdp", string(cdptypes.KeyDebtThreshold), string(suite.app.Codec().MustMarshalJSON(i(1000000000))),
},
{
"cdp", string(cdptypes.KeyCollateralParams), string(suite.app.Codec().MustMarshalJSON(newInvalidCP)),
},
},
),
proposer: paramChangePermissionsCom.Members[0],
committeeID: paramChangePermissionsCom.ID,
expectErr: true,
},
}
for _, tc := range testcases {
@ -85,7 +215,10 @@ func (suite *KeeperTestSuite) TestSubmitProposal() {
tApp := app.NewTestApp()
keeper := tApp.GetCommitteeKeeper()
ctx := tApp.NewContext(true, abci.Header{})
tApp.InitializeFromGenesisStates()
tApp.InitializeFromGenesisStates(
newPricefeedGenState([]string{"bnb"}, []sdk.Dec{d("15.01")}),
newCDPGenesisState(testCDPParams),
)
// setup committee (if required)
if !(reflect.DeepEqual(tc.committee, types.Committee{})) {
keeper.SetCommittee(ctx, tc.committee)

View File

@ -26,13 +26,10 @@ func handleCommitteeChangeProposal(ctx sdk.Context, k Keeper, committeeProposal
}
// Remove all committee's ongoing proposals
k.IterateProposals(ctx, func(p Proposal) bool {
if p.CommitteeID != committeeProposal.NewCommittee.ID {
return false
}
proposals := k.GetProposalsByCommittee(ctx, committeeProposal.NewCommittee.ID)
for _, p := range proposals {
k.DeleteProposalAndVotes(ctx, p.ID)
return false
})
}
// update/create the committee
k.SetCommittee(ctx, committeeProposal.NewCommittee)
@ -45,13 +42,10 @@ func handleCommitteeDeleteProposal(ctx sdk.Context, k Keeper, committeeProposal
}
// Remove all committee's ongoing proposals
k.IterateProposals(ctx, func(p Proposal) bool {
if p.CommitteeID != committeeProposal.CommitteeID {
return false
}
proposals := k.GetProposalsByCommittee(ctx, committeeProposal.CommitteeID)
for _, p := range proposals {
k.DeleteProposalAndVotes(ctx, p.ID)
return false
})
}
k.DeleteCommittee(ctx, committeeProposal.CommitteeID)
return nil

View File

@ -101,7 +101,7 @@ func RandomPermissions(r *rand.Rand, allowedParams []types.AllowedParam) []types
allowedParams[i], allowedParams[j] = allowedParams[j], allowedParams[i]
})
permissions = append(permissions,
types.ParamChangePermission{
types.SimpleParamChangePermission{
AllowedParams: allowedParams[:r.Intn(len(allowedParams)+1)],
})
}

View File

@ -45,7 +45,7 @@ func WeightedOperations(appParams simulation.AppParams, cdc *codec.Codec, ak Acc
wops,
simulation.NewWeightedOperation(
weight,
SimulateMsgSubmitProposal(ak, k, wContent.ContentSimulatorFn),
SimulateMsgSubmitProposal(cdc, ak, k, wContent.ContentSimulatorFn),
),
)
}
@ -55,7 +55,7 @@ func WeightedOperations(appParams simulation.AppParams, cdc *codec.Codec, ak Acc
// SimulateMsgSubmitProposal creates a proposal using the passed contentSimulatorFn and tries to find a committee that has permissions for it. If it can't then it uses the fallback committee.
// If the fallback committee isn't there (eg when using an non-generated genesis) and no committee can be found this emits a no-op msg and doesn't do anything.
// For each submit proposal msg, future ops for the vote messages are generated. Sometimes it doesn't run enough votes to allow the proposal to timeout - the likelihood of this happening is controlled by a parameter.
func SimulateMsgSubmitProposal(ak AccountKeeper, k keeper.Keeper, contentSim simulation.ContentSimulatorFn) simulation.Operation {
func SimulateMsgSubmitProposal(cdc *codec.Codec, ak AccountKeeper, k keeper.Keeper, contentSim simulation.ContentSimulatorFn) simulation.Operation {
return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simulation.Account, chainID string) (simulation.OperationMsg, []simulation.FutureOperation, error) {
// 1) Send a submit proposal msg
@ -77,7 +77,7 @@ func SimulateMsgSubmitProposal(ak AccountKeeper, k keeper.Keeper, contentSim sim
var selectedCommittee types.Committee
var found bool
for _, c := range committees {
if c.HasPermissionsFor(pp) {
if c.HasPermissionsFor(ctx, cdc, k.ParamKeeper, pp) {
selectedCommittee = c
found = true
break

View File

@ -125,9 +125,9 @@ func SimulateCommitteeChangeProposalContent(k keeper.Keeper, paramChanges []simu
}
}
/*
// Example custom ParamChangeProposal generator to only generate change to interesting cdp params.
// This allows more control over what params are changed within a simulation.
/*
func SimulateCDPParamChangeProposalContent(cdpKeeper cdpkeeper.Keeper, paramChangePool []simulation.ParamChange) simulation.ContentSimulatorFn {
return func(r *rand.Rand, ctx sdk.Context, _ []simulation.Account) govtypes.Content {
@ -138,7 +138,7 @@ func SimulateCDPParamChangeProposalContent(cdpKeeper cdpkeeper.Keeper, paramChan
if len(cp) == 0 {
return nil
}
cp[0].StabilityFee = sdk.MustNewDecFromStr("0.000001") // TODO generate
cp[0].StabilityFee = sdk.MustNewDecFromStr("0.000001")
paramChanges = append(
paramChanges,
paramstypes.NewParamChange(cdptypes.ModuleName, "?", string(cdptypes.ModuleCdc.MustMarshalJSON(cp))),

View File

@ -18,9 +18,6 @@ The `x/committee` module emits the following events:
| proposal_vote | committee_id | {committee ID} |
| proposal_vote | proposal_id | {proposal ID} |
| proposal_vote | voter | {voter address} |
| proposal_close | committee_id | {committee ID} |
| proposal_close | proposal_id | {proposal ID} |
| proposal_close | status | {outcome} |
| message | module | committee |
| message | sender | {sender address} |
@ -30,4 +27,4 @@ The `x/committee` module emits the following events:
|----------------------|---------------------|--------------------|
| proposal_close | committee_id | {committee ID} |
| proposal_close | proposal_id | {proposal ID} |
| proposal_close | status | proposal_timeout |
| proposal_close | status | {outcome} |

View File

@ -5,6 +5,7 @@ import (
distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types"
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
paramstypes "github.com/cosmos/cosmos-sdk/x/params/types"
upgrade "github.com/cosmos/cosmos-sdk/x/upgrade"
)
// ModuleCdc is a generic codec to be used throughout module
@ -21,6 +22,8 @@ func init() {
RegisterProposalTypeCodec(distrtypes.CommunityPoolSpendProposal{}, "cosmos-sdk/CommunityPoolSpendProposal")
RegisterProposalTypeCodec(paramstypes.ParameterChangeProposal{}, "cosmos-sdk/ParameterChangeProposal")
RegisterProposalTypeCodec(govtypes.TextProposal{}, "cosmos-sdk/TextProposal")
RegisterProposalTypeCodec(upgrade.SoftwareUpgradeProposal{}, "cosmos-sdk/SoftwareUpgradeProposal")
RegisterProposalTypeCodec(upgrade.CancelSoftwareUpgradeProposal{}, "cosmos-sdk/CancelSoftwareUpgradeProposal")
}
// RegisterCodec registers the necessary types for the module
@ -34,8 +37,10 @@ func RegisterCodec(cdc *codec.Codec) {
// Permissions
cdc.RegisterInterface((*Permission)(nil), nil)
cdc.RegisterConcrete(GodPermission{}, "kava/GodPermission", nil)
cdc.RegisterConcrete(ParamChangePermission{}, "kava/ParamChangePermission", 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)
// Msgs
cdc.RegisterConcrete(MsgSubmitProposal{}, "kava/MsgSubmitProposal", nil)

View File

@ -6,6 +6,7 @@ import (
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"
)
@ -48,9 +49,9 @@ func (c Committee) HasMember(addr sdk.AccAddress) bool {
// 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(proposal PubProposal) bool {
func (c Committee) HasPermissionsFor(ctx sdk.Context, appCdc *codec.Codec, pk ParamKeeper, proposal PubProposal) bool {
for _, p := range c.Permissions {
if p.Allows(proposal) {
if p.Allows(ctx, appCdc, pk, proposal) {
return true
}
}
@ -80,6 +81,12 @@ func (c Committee) Validate() error {
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)
@ -138,3 +145,17 @@ 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
}

View File

@ -0,0 +1,9 @@
package types
import (
"github.com/cosmos/cosmos-sdk/x/params"
)
type ParamKeeper interface {
GetSubspace(string) (params.Subspace, bool)
}

View File

@ -92,14 +92,15 @@ func (gs GenesisState) Validate() error {
// 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)
}
// validate address
if v.Voter.Empty() {
return fmt.Errorf("found empty voter address; vote: %+v", v)
}
}
return nil
}

View File

@ -12,7 +12,6 @@ import (
"github.com/tendermint/tendermint/crypto"
)
func d(s string) sdk.Dec { return sdk.MustNewDecFromStr(s) }
func TestGenesisState_Validate(t *testing.T) {
testTime := time.Date(1998, time.January, 1, 0, 0, 0, 0, time.UTC)
addresses := []sdk.AccAddress{

View File

@ -0,0 +1,741 @@
package types
import (
sdk "github.com/cosmos/cosmos-sdk/types"
bep3types "github.com/kava-labs/kava/x/bep3/types"
cdptypes "github.com/kava-labs/kava/x/cdp/types"
pricefeedtypes "github.com/kava-labs/kava/x/pricefeed/types"
)
// Avoid cluttering test cases with long function names
func i(in int64) sdk.Int { return sdk.NewInt(in) }
func d(str string) sdk.Dec { return sdk.MustNewDecFromStr(str) }
func c(denom string, amount int64) sdk.Coin { return sdk.NewInt64Coin(denom, amount) }
func cs(coins ...sdk.Coin) sdk.Coins { return sdk.NewCoins(coins...) }
func (suite *PermissionsTestSuite) TestAllowedCollateralParams_Allows() {
testCPs := cdptypes.CollateralParams{
{
Denom: "bnb",
LiquidationRatio: d("2.0"),
DebtLimit: c("usdx", 1000000000000),
StabilityFee: d("1.000000001547125958"),
LiquidationPenalty: d("0.05"),
AuctionSize: i(100),
Prefix: 0x20,
ConversionFactor: i(6),
MarketID: "bnb:usd",
},
{
Denom: "btc",
LiquidationRatio: d("1.5"),
DebtLimit: c("usdx", 1000000000),
StabilityFee: d("1.000000001547125958"),
LiquidationPenalty: d("0.10"),
AuctionSize: i(1000),
Prefix: 0x30,
ConversionFactor: i(8),
MarketID: "btc:usd",
},
{
Denom: "atom",
LiquidationRatio: d("2.0"),
DebtLimit: c("usdx", 1000000000),
StabilityFee: d("1.000000001547125958"),
LiquidationPenalty: d("0.07"),
AuctionSize: i(100),
Prefix: 0x40,
ConversionFactor: i(6),
MarketID: "atom:usd",
},
}
updatedTestCPs := make(cdptypes.CollateralParams, len(testCPs))
updatedTestCPs[0] = testCPs[1]
updatedTestCPs[1] = testCPs[0]
updatedTestCPs[2] = testCPs[2]
updatedTestCPs[0].DebtLimit = c("usdx", 1000) // btc
updatedTestCPs[1].LiquidationPenalty = d("0.15") // bnb
updatedTestCPs[2].DebtLimit = c("usdx", 1000) // atom
updatedTestCPs[2].LiquidationPenalty = d("0.15") // atom
testcases := []struct {
name string
allowed AllowedCollateralParams
current cdptypes.CollateralParams
incoming cdptypes.CollateralParams
expectAllowed bool
}{
{
name: "disallowed add",
allowed: AllowedCollateralParams{
{
Denom: "bnb",
AuctionSize: true,
},
{
Denom: "btc",
StabilityFee: true,
},
{ // allow all fields
Denom: "atom",
LiquidationRatio: true,
DebtLimit: true,
StabilityFee: true,
AuctionSize: true,
LiquidationPenalty: true,
Prefix: true,
MarketID: true,
ConversionFactor: true,
},
},
current: testCPs[:2],
incoming: testCPs[:3],
expectAllowed: false,
},
{
name: "disallowed remove",
allowed: AllowedCollateralParams{
{
Denom: "bnb",
AuctionSize: true,
},
{
// allow all fields
Denom: "btc",
LiquidationRatio: true,
DebtLimit: true,
StabilityFee: true,
AuctionSize: true,
LiquidationPenalty: true,
Prefix: true,
MarketID: true,
ConversionFactor: true,
},
},
current: testCPs[:2],
incoming: testCPs[:1], // removes btc
expectAllowed: false,
},
{
name: "allowed change with different order",
allowed: AllowedCollateralParams{
{
Denom: "bnb",
LiquidationPenalty: true,
},
{
Denom: "btc",
DebtLimit: true,
},
{
Denom: "atom",
DebtLimit: true,
LiquidationPenalty: true,
},
},
current: testCPs,
incoming: updatedTestCPs,
expectAllowed: true,
},
}
for _, tc := range testcases {
suite.Run(tc.name, func() {
suite.Require().Equal(
tc.expectAllowed,
tc.allowed.Allows(tc.current, tc.incoming),
)
})
}
}
func (suite *PermissionsTestSuite) TestAllowedAssetParams_Allows() {
testAPs := bep3types.AssetParams{
{
Denom: "bnb",
CoinID: 714,
Limit: i(1000000000000),
Active: true,
},
{
Denom: "btc",
CoinID: 0,
Limit: i(1000000000000),
Active: true,
},
{
Denom: "xrp",
CoinID: 144,
Limit: i(1000000000000),
Active: true,
},
}
updatedTestAPs := make(bep3types.AssetParams, len(testAPs))
updatedTestAPs[0] = testAPs[1]
updatedTestAPs[1] = testAPs[0]
updatedTestAPs[2] = testAPs[2]
updatedTestAPs[0].Limit = i(1000) // btc
updatedTestAPs[1].Active = false // bnb
updatedTestAPs[2].Limit = i(1000) // xrp
updatedTestAPs[2].Active = false // xrp
testcases := []struct {
name string
allowed AllowedAssetParams
current bep3types.AssetParams
incoming bep3types.AssetParams
expectAllowed bool
}{
{
name: "disallowed add",
allowed: AllowedAssetParams{
{
Denom: "bnb",
Active: true,
},
{
Denom: "btc",
Limit: true,
},
{ // allow all fields
Denom: "xrp",
CoinID: true,
Limit: true,
Active: true,
},
},
current: testAPs[:2],
incoming: testAPs[:3],
expectAllowed: false,
},
{
name: "disallowed remove",
allowed: AllowedAssetParams{
{
Denom: "bnb",
Active: true,
},
{ // allow all fields
Denom: "btc",
CoinID: true,
Limit: true,
Active: true,
},
},
current: testAPs[:2],
incoming: testAPs[:1], // removes btc
expectAllowed: false,
},
{
name: "allowed change with different order",
allowed: AllowedAssetParams{
{
Denom: "bnb",
Active: true,
},
{
Denom: "btc",
Limit: true,
},
{
Denom: "xrp",
Limit: true,
Active: true,
},
},
current: testAPs,
incoming: updatedTestAPs,
expectAllowed: true,
},
}
for _, tc := range testcases {
suite.Run(tc.name, func() {
suite.Require().Equal(
tc.expectAllowed,
tc.allowed.Allows(tc.current, tc.incoming),
)
})
}
}
func (suite *PermissionsTestSuite) TestAllowedMarkets_Allows() {
testMs := pricefeedtypes.Markets{
{
MarketID: "bnb:usd",
BaseAsset: "bnb",
QuoteAsset: "usd",
Oracles: []sdk.AccAddress{},
Active: true,
},
{
MarketID: "btc:usd",
BaseAsset: "btc",
QuoteAsset: "usd",
Oracles: []sdk.AccAddress{},
Active: true,
},
{
MarketID: "atom:usd",
BaseAsset: "atom",
QuoteAsset: "usd",
Oracles: []sdk.AccAddress{},
Active: true,
},
}
updatedTestMs := make(pricefeedtypes.Markets, len(testMs))
updatedTestMs[0] = testMs[1]
updatedTestMs[1] = testMs[0]
updatedTestMs[2] = testMs[2]
updatedTestMs[0].Oracles = []sdk.AccAddress{[]byte("a test address")} // btc
updatedTestMs[1].Active = false // bnb
updatedTestMs[2].Oracles = []sdk.AccAddress{[]byte("a test address")} // atom
updatedTestMs[2].Active = false // atom
testcases := []struct {
name string
allowed AllowedMarkets
current pricefeedtypes.Markets
incoming pricefeedtypes.Markets
expectAllowed bool
}{
{
name: "disallowed add",
allowed: AllowedMarkets{
{
MarketID: "bnb:usd",
Active: true,
},
{
MarketID: "btc:usd",
Oracles: true,
},
{ // allow all fields
MarketID: "atom:usd",
BaseAsset: true,
QuoteAsset: true,
Oracles: true,
Active: true,
},
},
current: testMs[:2],
incoming: testMs[:3],
expectAllowed: false,
},
{
name: "disallowed remove",
allowed: AllowedMarkets{
{
MarketID: "bnb:usd",
Active: true,
},
{ // allow all fields
MarketID: "btc:usd",
BaseAsset: true,
QuoteAsset: true,
Oracles: true,
Active: true,
},
},
current: testMs[:2],
incoming: testMs[:1], // removes btc
expectAllowed: false,
},
{
name: "allowed change with different order",
allowed: AllowedMarkets{
{
MarketID: "bnb:usd",
Active: true,
},
{
MarketID: "btc:usd",
Oracles: true,
},
{
MarketID: "atom:usd",
Oracles: true,
Active: true,
},
},
current: testMs,
incoming: updatedTestMs,
expectAllowed: true,
},
}
for _, tc := range testcases {
suite.Run(tc.name, func() {
suite.Require().Equal(
tc.expectAllowed,
tc.allowed.Allows(tc.current, tc.incoming),
)
})
}
}
func (suite *PermissionsTestSuite) TestAllowedCollateralParam_Allows() {
testCP := cdptypes.CollateralParam{
Denom: "bnb",
LiquidationRatio: d("1.5"),
DebtLimit: c("usdx", 1000000000000),
StabilityFee: d("1.000000001547125958"), // %5 apr
LiquidationPenalty: d("0.05"),
AuctionSize: i(100),
Prefix: 0x20,
ConversionFactor: i(6),
MarketID: "bnb:usd",
}
newMarketIDCP := testCP
newMarketIDCP.MarketID = "btc:usd"
newDebtLimitCP := testCP
newDebtLimitCP.DebtLimit = c("usdx", 1000)
newMarketIDAndDebtLimitCP := testCP
newMarketIDCP.MarketID = "btc:usd"
newDebtLimitCP.DebtLimit = c("usdx", 1000)
testcases := []struct {
name string
allowed AllowedCollateralParam
current cdptypes.CollateralParam
incoming cdptypes.CollateralParam
expectAllowed bool
}{
{
name: "allowed change",
allowed: AllowedCollateralParam{
Denom: "bnb",
DebtLimit: true,
StabilityFee: true,
AuctionSize: true,
},
current: testCP,
incoming: newDebtLimitCP,
expectAllowed: true,
},
{
name: "un-allowed change",
allowed: AllowedCollateralParam{
Denom: "bnb",
DebtLimit: true,
StabilityFee: true,
AuctionSize: true,
},
current: testCP,
incoming: newMarketIDCP,
expectAllowed: false,
},
{
name: "un-allowed mismatching denom",
allowed: AllowedCollateralParam{
Denom: "btc",
DebtLimit: true,
},
current: testCP,
incoming: newDebtLimitCP,
expectAllowed: false,
},
{
name: "allowed no change",
allowed: AllowedCollateralParam{
Denom: "bnb",
DebtLimit: true,
},
current: testCP,
incoming: testCP, // no change
expectAllowed: true,
},
{
name: "un-allowed change with allowed change",
allowed: AllowedCollateralParam{
Denom: "btc",
DebtLimit: true,
},
current: testCP,
incoming: newMarketIDAndDebtLimitCP,
expectAllowed: false,
},
// TODO {
// name: "nil Int values",
// allowed: AllowedCollateralParam{
// Denom: "btc",
// DebtLimit: true,
// },
// incoming: cdptypes.CollateralParam{}, // nil sdk.Int types
// current: testCP,
// expectAllowed: false,
// },
}
for _, tc := range testcases {
suite.Run(tc.name, func() {
suite.Require().Equal(
tc.expectAllowed,
tc.allowed.Allows(tc.current, tc.incoming),
)
})
}
}
func (suite *PermissionsTestSuite) TestAllowedDebtParam_Allows() {
testDP := cdptypes.DebtParam{
Denom: "usdx",
ReferenceAsset: "usd",
ConversionFactor: i(6),
DebtFloor: i(10000000),
SavingsRate: d("0.95"),
}
newDenomDP := testDP
newDenomDP.Denom = "usdz"
newDebtFloorDP := testDP
newDebtFloorDP.DebtFloor = i(1000)
newDenomAndDebtFloorDP := testDP
newDenomAndDebtFloorDP.Denom = "usdz"
newDenomAndDebtFloorDP.DebtFloor = i(1000)
testcases := []struct {
name string
allowed AllowedDebtParam
current cdptypes.DebtParam
incoming cdptypes.DebtParam
expectAllowed bool
}{
{
name: "allowed change",
allowed: AllowedDebtParam{
DebtFloor: true,
SavingsRate: true,
},
current: testDP,
incoming: newDebtFloorDP,
expectAllowed: true,
},
{
name: "un-allowed change",
allowed: AllowedDebtParam{
DebtFloor: true,
SavingsRate: true,
},
current: testDP,
incoming: newDenomDP,
expectAllowed: false,
},
{
name: "allowed no change",
allowed: AllowedDebtParam{
DebtFloor: true,
SavingsRate: true,
},
current: testDP,
incoming: testDP, // no change
expectAllowed: true,
},
{
name: "un-allowed change with allowed change",
allowed: AllowedDebtParam{
DebtFloor: true,
SavingsRate: true,
},
current: testDP,
incoming: newDenomAndDebtFloorDP,
expectAllowed: false,
},
// TODO {
// name: "nil Int values",
// allowed: AllowedCollateralParam{
// Denom: "btc",
// DebtLimit: true,
// },
// incoming: cdptypes.CollateralParam{}, // nil sdk.Int types
// current: testCP,
// expectAllowed: false,
// },
}
for _, tc := range testcases {
suite.Run(tc.name, func() {
suite.Require().Equal(
tc.expectAllowed,
tc.allowed.Allows(tc.current, tc.incoming),
)
})
}
}
func (suite *PermissionsTestSuite) TestAllowedAssetParam_Allows() {
testAP := bep3types.AssetParam{
Denom: "usdx",
CoinID: 999,
Limit: i(1000000000),
Active: true,
}
newCoinidAP := testAP
newCoinidAP.CoinID = 0
newLimitAP := testAP
newLimitAP.Limit = i(1000)
newCoinidAndLimitAP := testAP
newCoinidAndLimitAP.CoinID = 0
newCoinidAndLimitAP.Limit = i(1000)
testcases := []struct {
name string
allowed AllowedAssetParam
current bep3types.AssetParam
incoming bep3types.AssetParam
expectAllowed bool
}{
{
name: "allowed change",
allowed: AllowedAssetParam{
Denom: "usdx",
Limit: true,
},
current: testAP,
incoming: newLimitAP,
expectAllowed: true,
},
{
name: "un-allowed change",
allowed: AllowedAssetParam{
Denom: "usdx",
Limit: true,
},
current: testAP,
incoming: newCoinidAP,
expectAllowed: false,
},
{
name: "allowed no change",
allowed: AllowedAssetParam{
Denom: "usdx",
Limit: true,
},
current: testAP,
incoming: testAP, // no change
expectAllowed: true,
},
{
name: "un-allowed change with allowed change",
allowed: AllowedAssetParam{
Denom: "usdx",
Limit: true,
},
current: testAP,
incoming: newCoinidAndLimitAP,
expectAllowed: false,
},
// TODO {
// name: "nil Int values",
// allowed: AllowedCollateralParam{
// Denom: "btc",
// DebtLimit: true,
// },
// incoming: cdptypes.CollateralParam{}, // nil sdk.Int types
// current: testCP,
// expectAllowed: false,
// },
}
for _, tc := range testcases {
suite.Run(tc.name, func() {
suite.Require().Equal(
tc.expectAllowed,
tc.allowed.Allows(tc.current, tc.incoming),
)
})
}
}
func (suite *PermissionsTestSuite) TestAllowedMarket_Allows() {
testM := pricefeedtypes.Market{
MarketID: "bnb:usd",
BaseAsset: "bnb",
QuoteAsset: "usd",
Oracles: []sdk.AccAddress{[]byte("a test address")},
Active: true,
}
newOraclesM := testM
newOraclesM.Oracles = nil
newActiveM := testM
newActiveM.Active = false
newOraclesAndActiveM := testM
newOraclesAndActiveM.Oracles = nil
newOraclesAndActiveM.Active = false
testcases := []struct {
name string
allowed AllowedMarket
current pricefeedtypes.Market
incoming pricefeedtypes.Market
expectAllowed bool
}{
{
name: "allowed change",
allowed: AllowedMarket{
MarketID: "bnb:usd",
Active: true,
},
current: testM,
incoming: newActiveM,
expectAllowed: true,
},
{
name: "un-allowed change",
allowed: AllowedMarket{
MarketID: "bnb:usd",
Active: true,
},
current: testM,
incoming: newOraclesM,
expectAllowed: false,
},
{
name: "allowed no change",
allowed: AllowedMarket{
MarketID: "bnb:usd",
Active: true,
},
current: testM,
incoming: testM, // no change
expectAllowed: true,
},
{
name: "un-allowed change with allowed change",
allowed: AllowedMarket{
MarketID: "bnb:usd",
Active: true,
},
current: testM,
incoming: newOraclesAndActiveM,
expectAllowed: false,
},
// TODO {
// name: "nil Int values",
// allowed: AllowedCollateralParam{
// Denom: "btc",
// DebtLimit: true,
// },
// incoming: cdptypes.CollateralParam{}, // nil sdk.Int types
// current: testCP,
// expectAllowed: false,
// },
}
for _, tc := range testcases {
suite.Run(tc.name, func() {
suite.Require().Equal(
tc.expectAllowed,
tc.allowed.Allows(tc.current, tc.incoming),
)
})
}
}

View File

@ -1,8 +1,16 @@
package types
import (
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
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"
)
func init() {
@ -10,13 +18,15 @@ func init() {
// But since these proposals contain Permissions, these types also need registering:
govtypes.ModuleCdc.RegisterInterface((*Permission)(nil), nil)
govtypes.RegisterProposalTypeCodec(GodPermission{}, "kava/GodPermission")
govtypes.RegisterProposalTypeCodec(ParamChangePermission{}, "kava/ParamChangePermission")
govtypes.RegisterProposalTypeCodec(SimpleParamChangePermission{}, "kava/SimpleParamChangePermission")
govtypes.RegisterProposalTypeCodec(TextPermission{}, "kava/TextPermission")
govtypes.RegisterProposalTypeCodec(SoftwareUpgradePermission{}, "kava/SoftwareUpgradePermission")
govtypes.RegisterProposalTypeCodec(SubParamChangePermission{}, "kava/SubParamChangePermission")
}
// Permission is anything with a method that validates whether a proposal is allowed by it or not.
type Permission interface {
Allows(PubProposal) bool
Allows(sdk.Context, *codec.Codec, ParamKeeper, PubProposal) bool
}
// ------------------------------------------
@ -28,7 +38,7 @@ type GodPermission struct{}
var _ Permission = GodPermission{}
func (GodPermission) Allows(PubProposal) bool { return true }
func (GodPermission) Allows(sdk.Context, *codec.Codec, ParamKeeper, PubProposal) bool { return true }
func (GodPermission) MarshalYAML() (interface{}, error) {
valueToMarshal := struct {
@ -40,17 +50,17 @@ func (GodPermission) MarshalYAML() (interface{}, error) {
}
// ------------------------------------------
// ParamChangePermission
// SimpleParamChangePermission
// ------------------------------------------
// ParamChangeProposal only allows changes to certain params
type ParamChangePermission struct {
// SimpleParamChangePermission only allows changes to certain params
type SimpleParamChangePermission struct {
AllowedParams AllowedParams `json:"allowed_params" yaml:"allowed_params"`
}
var _ Permission = ParamChangePermission{}
var _ Permission = SimpleParamChangePermission{}
func (perm ParamChangePermission) Allows(p PubProposal) bool {
func (perm SimpleParamChangePermission) Allows(_ sdk.Context, _ *codec.Codec, _ ParamKeeper, p PubProposal) bool {
proposal, ok := p.(paramstypes.ParameterChangeProposal)
if !ok {
return false
@ -63,7 +73,7 @@ func (perm ParamChangePermission) Allows(p PubProposal) bool {
return true
}
func (perm ParamChangePermission) MarshalYAML() (interface{}, error) {
func (perm SimpleParamChangePermission) MarshalYAML() (interface{}, error) {
valueToMarshal := struct {
Type string `yaml:"type"`
AllowedParams AllowedParams `yaml:"allowed_params"`
@ -98,7 +108,7 @@ type TextPermission struct{}
var _ Permission = TextPermission{}
func (TextPermission) Allows(p PubProposal) bool {
func (TextPermission) Allows(_ sdk.Context, _ *codec.Codec, _ ParamKeeper, p PubProposal) bool {
_, ok := p.(govtypes.TextProposal)
return ok
}
@ -111,3 +121,438 @@ func (TextPermission) MarshalYAML() (interface{}, error) {
}
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.KeySupportedAssets)) {
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.KeySupportedAssets, &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"`
MarketID bool `json:"market_id" yaml:"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.MarketID == incoming.MarketID) || acp.MarketID) &&
(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.Limit.Equal(incoming.Limit) || 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
}

View File

@ -2,11 +2,14 @@ package types
import (
"testing"
"time"
"github.com/stretchr/testify/suite"
sdk "github.com/cosmos/cosmos-sdk/types"
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
paramstypes "github.com/cosmos/cosmos-sdk/x/params/types"
upgrade "github.com/cosmos/cosmos-sdk/x/upgrade"
)
type PermissionsTestSuite struct {
@ -36,7 +39,7 @@ func (suite *PermissionsTestSuite) SetupTest() {
}
}
func (suite *PermissionsTestSuite) TestParamChangePermission_Allows() {
func (suite *PermissionsTestSuite) TestSimpleParamChangePermission_Allows() {
testcases := []struct {
name string
allowedParams AllowedParams
@ -133,12 +136,12 @@ func (suite *PermissionsTestSuite) TestParamChangePermission_Allows() {
for _, tc := range testcases {
suite.Run(tc.name, func() {
permission := ParamChangePermission{
permission := SimpleParamChangePermission{
AllowedParams: tc.allowedParams,
}
suite.Equal(
tc.expectAllowed,
permission.Allows(tc.pubProposal),
permission.Allows(sdk.Context{}, nil, nil, tc.pubProposal),
)
})
}
@ -276,7 +279,64 @@ func (suite *PermissionsTestSuite) TestTextPermission_Allows() {
permission := TextPermission{}
suite.Equal(
tc.expectAllowed,
permission.Allows(tc.pubProposal),
permission.Allows(sdk.Context{}, nil, nil, tc.pubProposal),
)
})
}
}
func (suite *PermissionsTestSuite) TestSoftwareUpgradePermission_Allows() {
testcases := []struct {
name string
pubProposal PubProposal
expectAllowed bool
}{
{
name: "normal",
pubProposal: upgrade.NewSoftwareUpgradeProposal(
"A Title",
"A description for this proposal.",
upgrade.Plan{
Name: "upgrade v0.12.1",
Time: time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC),
Info: "some information",
},
),
expectAllowed: true,
},
{
name: "not allowed (wrong pubproposal type)",
pubProposal: paramstypes.NewParameterChangeProposal(
"A Title",
"A description for this proposal.",
[]paramstypes.ParamChange{
{
Subspace: "cdp",
Key: "DebtThreshold",
Value: `{"denom": "usdx", "amount": "1000000"}`,
},
{
Subspace: "cdp",
Key: "CollateralParams",
Value: `[]`,
},
},
),
expectAllowed: false,
},
{
name: "not allowed (nil pubproposal)",
pubProposal: nil,
expectAllowed: false,
},
}
for _, tc := range testcases {
suite.Run(tc.name, func() {
permission := SoftwareUpgradePermission{}
suite.Equal(
tc.expectAllowed,
permission.Allows(sdk.Context{}, nil, nil, tc.pubProposal),
)
})
}