From c5f414e6a5de1b77b5f89dfde030f4aae7729203 Mon Sep 17 00:00:00 2001 From: Robert Pirtle Date: Thu, 20 Apr 2023 14:13:16 -0700 Subject: [PATCH] feat: add community cdp repay debt proposal (#1565) * add CommunityPoolCDPRepayDebtProposal proto & type * add cdpKeeper to x/community's keeper * add handler for CommunityPoolCDPRepayDebtProposal * more test cases * rename CommunityPoolCDP -> CommunityCDP the proposal does not interact with the vanilla community pool * fix & test stringer * update changelog --- CHANGELOG.md | 2 + app/app.go | 3 +- docs/core/proto-docs.md | 20 ++ proto/kava/community/v1beta1/proposal.proto | 12 + x/community/handler.go | 2 + x/community/keeper/keeper.go | 4 +- x/community/keeper/proposal_handler.go | 6 + x/community/keeper/proposal_handler_test.go | 118 +++++++- x/community/testutil/cdp_genesis.go | 56 ++++ x/community/types/codec.go | 2 + x/community/types/expected_keepers.go | 5 + x/community/types/proposal.go | 71 +++++ x/community/types/proposal.pb.go | 314 +++++++++++++++++++- x/community/types/proposal_test.go | 97 ++++++ 14 files changed, 699 insertions(+), 13 deletions(-) create mode 100644 x/community/testutil/cdp_genesis.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 4edfa924..c5ae4539 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ - (x/committee) [#1562] Add CommunityPoolLendWithdrawPermission - (x/community) [#1563] Include x/community module pool balance in x/distribution community_pool query response. +- (x/community) [#1565] Add CommunityCDPRepayDebtProposal ### Deprecated @@ -232,6 +233,7 @@ the [changelog](https://github.com/cosmos/cosmos-sdk/blob/v0.38.4/CHANGELOG.md). large-scale simulations remotely using aws-batch +[#1565]: https://github.com/Kava-Labs/kava/pull/1565 [#1563]: https://github.com/Kava-Labs/kava/pull/1563 [#1562]: https://github.com/Kava-Labs/kava/pull/1562 [#1550]: https://github.com/Kava-Labs/kava/pull/1550 diff --git a/app/app.go b/app/app.go index ce8e28f3..a89437c3 100644 --- a/app/app.go +++ b/app/app.go @@ -635,8 +635,9 @@ func NewApp( app.communityKeeper = communitykeeper.NewKeeper( app.accountKeeper, app.bankKeeper, + &cdpKeeper, app.distrKeeper, - hardKeeper, + &hardKeeper, ) app.kavadistKeeper = kavadistkeeper.NewKeeper( appCodec, diff --git a/docs/core/proto-docs.md b/docs/core/proto-docs.md index 4d60fbdd..f1e09d79 100644 --- a/docs/core/proto-docs.md +++ b/docs/core/proto-docs.md @@ -180,6 +180,7 @@ - [Msg](#kava.committee.v1beta1.Msg) - [kava/community/v1beta1/proposal.proto](#kava/community/v1beta1/proposal.proto) + - [CommunityCDPRepayDebtProposal](#kava.community.v1beta1.CommunityCDPRepayDebtProposal) - [CommunityPoolLendDepositProposal](#kava.community.v1beta1.CommunityPoolLendDepositProposal) - [CommunityPoolLendWithdrawProposal](#kava.community.v1beta1.CommunityPoolLendWithdrawProposal) @@ -2857,6 +2858,25 @@ Msg defines the committee Msg service + + +### CommunityCDPRepayDebtProposal +CommunityCDPRepayDebtProposal repays a cdp debt position owned by the community module +This proposal exists primarily to allow committees to repay community module cdp debts. + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| `title` | [string](#string) | | | +| `description` | [string](#string) | | | +| `collateral_type` | [string](#string) | | | +| `payment` | [cosmos.base.v1beta1.Coin](#cosmos.base.v1beta1.Coin) | | | + + + + + + ### CommunityPoolLendDepositProposal diff --git a/proto/kava/community/v1beta1/proposal.proto b/proto/kava/community/v1beta1/proposal.proto index e1e899ea..16f128db 100644 --- a/proto/kava/community/v1beta1/proposal.proto +++ b/proto/kava/community/v1beta1/proposal.proto @@ -31,3 +31,15 @@ message CommunityPoolLendWithdrawProposal { (gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins" ]; } + +// CommunityCDPRepayDebtProposal repays a cdp debt position owned by the community module +// This proposal exists primarily to allow committees to repay community module cdp debts. +message CommunityCDPRepayDebtProposal { + option (gogoproto.goproto_stringer) = false; + option (gogoproto.goproto_getters) = false; + + string title = 1; + string description = 2; + string collateral_type = 3; + cosmos.base.v1beta1.Coin payment = 4 [(gogoproto.nullable) = false]; +} diff --git a/x/community/handler.go b/x/community/handler.go index 808c24fe..42362547 100644 --- a/x/community/handler.go +++ b/x/community/handler.go @@ -14,6 +14,8 @@ import ( func NewCommunityPoolProposalHandler(k keeper.Keeper) govv1beta1.Handler { return func(ctx sdk.Context, content govv1beta1.Content) error { switch c := content.(type) { + case *types.CommunityCDPRepayDebtProposal: + return keeper.HandleCommunityCDPRepayDebtProposal(ctx, k, c) case *types.CommunityPoolLendDepositProposal: return keeper.HandleCommunityPoolLendDepositProposal(ctx, k, c) case *types.CommunityPoolLendWithdrawProposal: diff --git a/x/community/keeper/keeper.go b/x/community/keeper/keeper.go index 88ef3627..1148d9d6 100644 --- a/x/community/keeper/keeper.go +++ b/x/community/keeper/keeper.go @@ -12,6 +12,7 @@ import ( // Keeper of the community store type Keeper struct { bankKeeper types.BankKeeper + cdpKeeper types.CdpKeeper distrKeeper types.DistributionKeeper hardKeeper types.HardKeeper moduleAddress sdk.AccAddress @@ -20,7 +21,7 @@ type Keeper struct { } // NewKeeper creates a new community Keeper instance -func NewKeeper(ak types.AccountKeeper, bk types.BankKeeper, dk types.DistributionKeeper, hk types.HardKeeper) Keeper { +func NewKeeper(ak types.AccountKeeper, bk types.BankKeeper, ck types.CdpKeeper, dk types.DistributionKeeper, hk types.HardKeeper) Keeper { // ensure community module account is set addr := ak.GetModuleAddress(types.ModuleAccountName) if addr == nil { @@ -33,6 +34,7 @@ func NewKeeper(ak types.AccountKeeper, bk types.BankKeeper, dk types.Distributio return Keeper{ bankKeeper: bk, + cdpKeeper: ck, distrKeeper: dk, hardKeeper: hk, moduleAddress: addr, diff --git a/x/community/keeper/proposal_handler.go b/x/community/keeper/proposal_handler.go index f724a8e5..eb24458c 100644 --- a/x/community/keeper/proposal_handler.go +++ b/x/community/keeper/proposal_handler.go @@ -36,3 +36,9 @@ func HandleCommunityPoolLendWithdrawProposal(ctx sdk.Context, k Keeper, p *types // send all withdrawn coins back to community pool return k.distrKeeper.FundCommunityPool(ctx, totalWithdrawn, k.moduleAddress) } + +// HandleCommunityCDPRepayDebtProposal is a handler for executing a passed community pool cdp repay debt proposal. +func HandleCommunityCDPRepayDebtProposal(ctx sdk.Context, k Keeper, p *types.CommunityCDPRepayDebtProposal) error { + // make debt repayment + return k.cdpKeeper.RepayPrincipal(ctx, k.moduleAddress, p.CollateralType, p.Payment) +} diff --git a/x/community/keeper/proposal_handler_test.go b/x/community/keeper/proposal_handler_test.go index ecf56f80..1e2c074a 100644 --- a/x/community/keeper/proposal_handler_test.go +++ b/x/community/keeper/proposal_handler_test.go @@ -12,6 +12,7 @@ import ( tmtime "github.com/tendermint/tendermint/types/time" "github.com/kava-labs/kava/app" + cdpkeeper "github.com/kava-labs/kava/x/cdp/keeper" "github.com/kava-labs/kava/x/community/keeper" "github.com/kava-labs/kava/x/community/testutil" "github.com/kava-labs/kava/x/community/types" @@ -22,14 +23,15 @@ import ( const chainID = "kavatest_2221-1" +func c(denom string, amount int64) sdk.Coin { return sdk.NewInt64Coin(denom, amount) } func ukava(amt int64) sdk.Coins { - return sdk.NewCoins(sdk.NewInt64Coin("ukava", amt)) + return sdk.NewCoins(c("ukava", amt)) } func usdx(amt int64) sdk.Coins { - return sdk.NewCoins(sdk.NewInt64Coin("usdx", amt)) + return sdk.NewCoins(c("usdx", amt)) } func otherdenom(amt int64) sdk.Coins { - return sdk.NewCoins(sdk.NewInt64Coin("other-denom", amt)) + return sdk.NewCoins(c("other-denom", amt)) } type proposalTestSuite struct { @@ -40,6 +42,7 @@ type proposalTestSuite struct { Keeper keeper.Keeper MaccAddress sdk.AccAddress + cdpKeeper cdpkeeper.Keeper hardKeeper hardkeeper.Keeper } @@ -68,19 +71,21 @@ func (suite *proposalTestSuite) SetupTest() { genTime, chainID, app.GenesisState{hardtypes.ModuleName: tApp.AppCodec().MustMarshalJSON(&hardGS)}, app.GenesisState{pricefeedtypes.ModuleName: tApp.AppCodec().MustMarshalJSON(&pricefeedGS)}, + testutil.NewCDPGenState(tApp.AppCodec(), "ukava", "kava", sdk.NewDec(2)), ) suite.App = tApp suite.Ctx = ctx suite.Keeper = tApp.GetCommunityKeeper() suite.MaccAddress = tApp.GetAccountKeeper().GetModuleAddress(types.ModuleAccountName) + suite.cdpKeeper = suite.App.GetCDPKeeper() suite.hardKeeper = suite.App.GetHardKeeper() // give the community pool some funds // ukava - suite.FundCommunityPool(ukava(1e10)) + suite.FundCommunityPool(ukava(2e10)) // usdx - suite.FundCommunityPool(usdx(1e10)) + suite.FundCommunityPool(usdx(2e10)) // other-denom suite.FundCommunityPool(otherdenom(1e10)) } @@ -332,3 +337,106 @@ func (suite *proposalTestSuite) TestCommunityLendWithdrawProposal() { }) } } + +// expectation: funds in the community module will be used to repay cdps. +// if collateral is returned, it stays in the community module. +func (suite *proposalTestSuite) TestCommunityCDPRepayDebtProposal() { + initialModuleFunds := ukava(2e10).Add(otherdenom(1e9)...) + collateralType := "kava-a" + type debt struct { + collateral sdk.Coin + principal sdk.Coin + } + testcases := []struct { + name string + initialDebt *debt + proposal *types.CommunityCDPRepayDebtProposal + expectedErr string + expectedRepaid sdk.Coin + }{ + { + name: "valid - paid in full", + initialDebt: &debt{c("ukava", 1e10), c("usdx", 1e9)}, + proposal: types.NewCommunityCDPRepayDebtProposal( + "repaying my debts in full", + "title says it all", + collateralType, + c("usdx", 1e9), + ), + expectedErr: "", + expectedRepaid: c("usdx", 1e9), + }, + { + name: "valid - partial payment", + initialDebt: &debt{c("ukava", 1e10), c("usdx", 1e9)}, + proposal: types.NewCommunityCDPRepayDebtProposal( + "title goes here", + "description goes here", + collateralType, + c("usdx", 1e8), + ), + expectedErr: "", + expectedRepaid: c("usdx", 1e8), + }, + { + name: "invalid - insufficient funds", + initialDebt: &debt{c("ukava", 1e10), c("usdx", 1e9)}, + proposal: types.NewCommunityCDPRepayDebtProposal( + "title goes here", + "description goes here", + collateralType, + c("usdx", 1e10), // <-- more usdx than we have + ), + expectedErr: "insufficient balance", + expectedRepaid: c("usdx", 0), + }, + } + + for _, tc := range testcases { + suite.Run(tc.name, func() { + var err error + suite.SetupTest() + + // setup the community module with some initial funds + err = suite.App.FundModuleAccount(suite.Ctx, types.ModuleAccountName, initialModuleFunds) + suite.NoError(err, "failed to initially fund module account for cdp creation") + + // setup initial debt position + err = suite.cdpKeeper.AddCdp(suite.Ctx, suite.MaccAddress, tc.initialDebt.collateral, tc.initialDebt.principal, collateralType) + suite.NoError(err, "unexpected error while creating initial cdp") + + balanceBefore := suite.Keeper.GetModuleAccountBalance(suite.Ctx) + + // submit proposal + err = keeper.HandleCommunityCDPRepayDebtProposal(suite.Ctx, suite.Keeper, tc.proposal) + if tc.expectedErr == "" { + suite.NoError(err) + } else { + suite.ErrorContains(err, tc.expectedErr) + } + suite.NextBlock() + + cdps := suite.cdpKeeper.GetAllCdpsByCollateralType(suite.Ctx, collateralType) + expectedRemainingPrincipal := tc.initialDebt.principal.Sub(tc.expectedRepaid) + fullyRepaid := expectedRemainingPrincipal.IsZero() + + // expect repayment funds to be deducted from community module account + expectedModuleBalance := balanceBefore.Sub(tc.expectedRepaid) + // when fully repaid, the position is closed and collateral is returned. + if fullyRepaid { + suite.Len(cdps, 0, "expected position to have been closed on payment") + // expect balance to include recouped collateral + expectedModuleBalance = expectedModuleBalance.Add(tc.initialDebt.collateral) + } else { + suite.Len(cdps, 1, "expected debt position to remain open") + suite.Equal(suite.MaccAddress, cdps[0].Owner, "sanity check: unexpected owner") + // check the remaining principle on the cdp + suite.Equal(expectedRemainingPrincipal, cdps[0].Principal) + } + + // verify the balance changed as expected + moduleBalanceAfter := suite.Keeper.GetModuleAccountBalance(suite.Ctx) + suite.True(expectedModuleBalance.IsEqual(moduleBalanceAfter), "module balance changed unexpectedly") + }) + } +} diff --git a/x/community/testutil/cdp_genesis.go b/x/community/testutil/cdp_genesis.go new file mode 100644 index 00000000..160d61c2 --- /dev/null +++ b/x/community/testutil/cdp_genesis.go @@ -0,0 +1,56 @@ +package testutil + +import ( + "time" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/kava-labs/kava/app" + cdptypes "github.com/kava-labs/kava/x/cdp/types" +) + +func NewCDPGenState(cdc codec.JSONCodec, denom, asset string, liquidationRatio sdk.Dec) app.GenesisState { + cdpGenesis := cdptypes.GenesisState{ + Params: cdptypes.Params{ + GlobalDebtLimit: sdk.NewInt64Coin("usdx", 1000000000000), + SurplusAuctionThreshold: cdptypes.DefaultSurplusThreshold, + SurplusAuctionLot: cdptypes.DefaultSurplusLot, + DebtAuctionThreshold: cdptypes.DefaultDebtThreshold, + DebtAuctionLot: cdptypes.DefaultDebtLot, + CollateralParams: cdptypes.CollateralParams{ + { + Denom: denom, + Type: asset + "-a", + LiquidationRatio: liquidationRatio, + DebtLimit: sdk.NewInt64Coin("usdx", 1000000000000), + StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), // %5 apr + LiquidationPenalty: sdk.MustNewDecFromStr("0.05"), + AuctionSize: sdk.NewInt(100), + SpotMarketID: asset + ":usd", + LiquidationMarketID: asset + ":usd", + KeeperRewardPercentage: sdk.MustNewDecFromStr("0.01"), + CheckCollateralizationIndexCount: sdk.NewInt(10), + ConversionFactor: sdk.NewInt(6), + }, + }, + DebtParam: cdptypes.DebtParam{ + Denom: "usdx", + ReferenceAsset: "usd", + ConversionFactor: sdk.NewInt(6), + DebtFloor: sdk.NewInt(10000000), + }, + }, + StartingCdpID: cdptypes.DefaultCdpStartingID, + DebtDenom: cdptypes.DefaultDebtDenom, + GovDenom: cdptypes.DefaultGovDenom, + CDPs: cdptypes.CDPs{}, + PreviousAccumulationTimes: cdptypes.GenesisAccumulationTimes{ + cdptypes.NewGenesisAccumulationTime(asset+"-a", time.Time{}, sdk.OneDec()), + }, + TotalPrincipals: cdptypes.GenesisTotalPrincipals{ + cdptypes.NewGenesisTotalPrincipal(asset+"-a", sdk.ZeroInt()), + }, + } + return app.GenesisState{cdptypes.ModuleName: cdc.MustMarshalJSON(&cdpGenesis)} +} diff --git a/x/community/types/codec.go b/x/community/types/codec.go index b86b1028..e01a17d3 100644 --- a/x/community/types/codec.go +++ b/x/community/types/codec.go @@ -15,6 +15,7 @@ func RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) { cdc.RegisterConcrete(&MsgFundCommunityPool{}, "community/MsgFundCommunityPool", nil) cdc.RegisterConcrete(&CommunityPoolLendDepositProposal{}, "kava/CommunityPoolLendDepositProposal", nil) cdc.RegisterConcrete(&CommunityPoolLendWithdrawProposal{}, "kava/CommunityPoolLendWithdrawProposal", nil) + cdc.RegisterConcrete(&CommunityCDPRepayDebtProposal{}, "kava/CommunityCDPRepayDebtProposal", nil) } // RegisterInterfaces registers proto messages under their interfaces for unmarshalling, @@ -26,6 +27,7 @@ func RegisterInterfaces(registry types.InterfaceRegistry) { registry.RegisterImplementations((*govv1beta1.Content)(nil), &CommunityPoolLendDepositProposal{}, &CommunityPoolLendWithdrawProposal{}, + &CommunityCDPRepayDebtProposal{}, ) msgservice.RegisterMsgServiceDesc(registry, &_Msg_serviceDesc) diff --git a/x/community/types/expected_keepers.go b/x/community/types/expected_keepers.go index 38316459..3ad682ef 100644 --- a/x/community/types/expected_keepers.go +++ b/x/community/types/expected_keepers.go @@ -19,6 +19,11 @@ type BankKeeper interface { SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error } +// CdpKeeper defines the contract needed to be fulfilled for cdp dependencies. +type CdpKeeper interface { + RepayPrincipal(ctx sdk.Context, owner sdk.AccAddress, collateralType string, payment sdk.Coin) error +} + // HardKeeper defines the contract needed to be fulfilled for Kava Lend dependencies. type HardKeeper interface { Deposit(ctx sdk.Context, depositor sdk.AccAddress, coins sdk.Coins) error diff --git a/x/community/types/proposal.go b/x/community/types/proposal.go index fca0cbd5..dc0097ff 100644 --- a/x/community/types/proposal.go +++ b/x/community/types/proposal.go @@ -1,6 +1,7 @@ package types import ( + "errors" fmt "fmt" "strings" @@ -15,12 +16,15 @@ const ( ProposalTypeCommunityPoolLendDeposit = "CommunityPoolLendDeposit" // ProposalTypeCommunityPoolLendWithdraw defines the type for a CommunityPoolLendDepositProposal ProposalTypeCommunityPoolLendWithdraw = "CommunityPoolLendWithdraw" + // ProposalTypeCommunityCDPRepayDebt defines the type for a CommunityCDPRepayDebtProposal + ProposalTypeCommunityCDPRepayDebt = "CommunityCDPRepayDebt" ) // Assert CommunityPoolLendDepositProposal implements govtypes.Content at compile-time var ( _ govv1beta1.Content = &CommunityPoolLendDepositProposal{} _ govv1beta1.Content = &CommunityPoolLendWithdrawProposal{} + _ govv1beta1.Content = &CommunityCDPRepayDebtProposal{} ) func init() { @@ -28,8 +32,14 @@ func init() { govv1beta1.ModuleCdc.Amino.RegisterConcrete(&CommunityPoolLendDepositProposal{}, "kava/CommunityPoolLendDepositProposal", nil) govv1beta1.RegisterProposalType(ProposalTypeCommunityPoolLendWithdraw) govv1beta1.ModuleCdc.Amino.RegisterConcrete(&CommunityPoolLendWithdrawProposal{}, "kava/CommunityPoolLendWithdrawProposal", nil) + govv1beta1.RegisterProposalType(ProposalTypeCommunityCDPRepayDebt) + govv1beta1.ModuleCdc.Amino.RegisterConcrete(&CommunityCDPRepayDebtProposal{}, "kava/CommunityCDPRepayDebtProposal", nil) } +////////////////// +// Lend Proposals +////////////////// + // NewCommunityPoolLendDepositProposal creates a new community pool deposit proposal. func NewCommunityPoolLendDepositProposal(title, description string, amount sdk.Coins) *CommunityPoolLendDepositProposal { return &CommunityPoolLendDepositProposal{ @@ -121,3 +131,64 @@ func (p *CommunityPoolLendWithdrawProposal) ValidateBasic() error { } return p.Amount.Validate() } + +///////////////// +// CDP Proposals +///////////////// + +// NewCommunityCDPRepayDebtProposal creates a new community pool cdp debt repay proposal. +func NewCommunityCDPRepayDebtProposal( + title string, + description string, + collateralType string, + payment sdk.Coin, +) *CommunityCDPRepayDebtProposal { + return &CommunityCDPRepayDebtProposal{ + Title: title, + Description: description, + CollateralType: collateralType, + Payment: payment, + } +} + +// GetTitle returns the title of the proposal. +func (p *CommunityCDPRepayDebtProposal) GetTitle() string { return p.Title } + +// GetDescription returns the description of the proposal. +func (p *CommunityCDPRepayDebtProposal) GetDescription() string { return p.Description } + +// GetDescription returns the routing key of the proposal. +func (p *CommunityCDPRepayDebtProposal) ProposalRoute() string { return ModuleName } + +// ProposalType returns the type of the proposal. +func (p *CommunityCDPRepayDebtProposal) ProposalType() string { + return ProposalTypeCommunityCDPRepayDebt +} + +// String implements fmt.Stringer +func (p *CommunityCDPRepayDebtProposal) String() string { + var b strings.Builder + b.WriteString(fmt.Sprintf(`Community CDP Repay Debt Proposal: + Title: %s + Description: %s + Collateral Type: %s + Payment: %s +`, p.Title, p.Description, p.CollateralType, p.Payment)) + return b.String() +} + +// ValidateBasic stateless validation of the proposal. +func (p *CommunityCDPRepayDebtProposal) ValidateBasic() error { + if err := govv1beta1.ValidateAbstract(p); err != nil { + return err + } + // ensure collateral type is set + if strings.TrimSpace(p.CollateralType) == "" { + return errors.New("cdp collateral type cannot be blank") + } + // ensure the proposal has payment amount + if !p.Payment.IsValid() || p.Payment.IsZero() { + return errorsmod.Wrapf(sdkerrors.ErrInvalidCoins, "payment amount %s", p.Payment) + } + return nil +} diff --git a/x/community/types/proposal.pb.go b/x/community/types/proposal.pb.go index 2c0e515e..c20601d1 100644 --- a/x/community/types/proposal.pb.go +++ b/x/community/types/proposal.pb.go @@ -103,9 +103,51 @@ func (m *CommunityPoolLendWithdrawProposal) XXX_DiscardUnknown() { var xxx_messageInfo_CommunityPoolLendWithdrawProposal proto.InternalMessageInfo +// CommunityCDPRepayDebtProposal repays a cdp debt position owned by the community module +// This proposal exists primarily to allow committees to repay community module cdp debts. +type CommunityCDPRepayDebtProposal struct { + Title string `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"` + Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` + CollateralType string `protobuf:"bytes,3,opt,name=collateral_type,json=collateralType,proto3" json:"collateral_type,omitempty"` + Payment types.Coin `protobuf:"bytes,4,opt,name=payment,proto3" json:"payment"` +} + +func (m *CommunityCDPRepayDebtProposal) Reset() { *m = CommunityCDPRepayDebtProposal{} } +func (*CommunityCDPRepayDebtProposal) ProtoMessage() {} +func (*CommunityCDPRepayDebtProposal) Descriptor() ([]byte, []int) { + return fileDescriptor_64aa83b2ed448ec1, []int{2} +} +func (m *CommunityCDPRepayDebtProposal) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *CommunityCDPRepayDebtProposal) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_CommunityCDPRepayDebtProposal.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *CommunityCDPRepayDebtProposal) XXX_Merge(src proto.Message) { + xxx_messageInfo_CommunityCDPRepayDebtProposal.Merge(m, src) +} +func (m *CommunityCDPRepayDebtProposal) XXX_Size() int { + return m.Size() +} +func (m *CommunityCDPRepayDebtProposal) XXX_DiscardUnknown() { + xxx_messageInfo_CommunityCDPRepayDebtProposal.DiscardUnknown(m) +} + +var xxx_messageInfo_CommunityCDPRepayDebtProposal proto.InternalMessageInfo + func init() { proto.RegisterType((*CommunityPoolLendDepositProposal)(nil), "kava.community.v1beta1.CommunityPoolLendDepositProposal") proto.RegisterType((*CommunityPoolLendWithdrawProposal)(nil), "kava.community.v1beta1.CommunityPoolLendWithdrawProposal") + proto.RegisterType((*CommunityCDPRepayDebtProposal)(nil), "kava.community.v1beta1.CommunityCDPRepayDebtProposal") } func init() { @@ -113,7 +155,7 @@ func init() { } var fileDescriptor_64aa83b2ed448ec1 = []byte{ - // 325 bytes of a gzipped FileDescriptorProto + // 399 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x52, 0xcd, 0x4e, 0x2c, 0x4b, 0xd4, 0x4f, 0xce, 0xcf, 0xcd, 0x2d, 0xcd, 0xcb, 0x2c, 0xa9, 0xd4, 0x2f, 0x33, 0x4c, 0x4a, 0x2d, 0x49, 0x34, 0xd4, 0x2f, 0x28, 0xca, 0x2f, 0xc8, 0x2f, 0x4e, 0xcc, 0xd1, 0x2b, 0x28, 0xca, 0x2f, @@ -130,11 +172,15 @@ var fileDescriptor_64aa83b2ed448ec1 = []byte{ 0xe9, 0x43, 0x3d, 0x04, 0xa1, 0x74, 0x8b, 0x53, 0xb2, 0xf5, 0x4b, 0x2a, 0x0b, 0x52, 0x8b, 0xc1, 0x1a, 0x8a, 0x83, 0xa0, 0x46, 0x5b, 0x71, 0x74, 0x2c, 0x90, 0x67, 0x98, 0xb1, 0x40, 0x9e, 0x41, 0xe9, 0x14, 0x23, 0x97, 0x22, 0x86, 0x5f, 0xc2, 0x33, 0x4b, 0x32, 0x52, 0x8a, 0x12, 0xcb, 0x87, - 0x98, 0x67, 0x9c, 0x5c, 0x4f, 0x3c, 0x92, 0x63, 0xbc, 0xf0, 0x48, 0x8e, 0xf1, 0xc1, 0x23, 0x39, - 0xc6, 0x09, 0x8f, 0xe5, 0x18, 0x2e, 0x3c, 0x96, 0x63, 0xb8, 0xf1, 0x58, 0x8e, 0x21, 0x4a, 0x1b, - 0xc9, 0x54, 0x50, 0x5a, 0xd0, 0xcd, 0x49, 0x4c, 0x2a, 0x06, 0xb3, 0xf4, 0x2b, 0x90, 0x92, 0x0f, - 0xd8, 0xf8, 0x24, 0x36, 0x70, 0x34, 0x1b, 0x03, 0x02, 0x00, 0x00, 0xff, 0xff, 0xa0, 0x07, 0x87, - 0x4d, 0x5d, 0x02, 0x00, 0x00, + 0x9a, 0x67, 0x8e, 0x32, 0x72, 0xc9, 0xc2, 0x3d, 0xe3, 0xec, 0x12, 0x10, 0x94, 0x5a, 0x90, 0x58, + 0xe9, 0x92, 0x9a, 0x44, 0x79, 0xac, 0xa8, 0x73, 0xf1, 0x27, 0xe7, 0xe7, 0xe4, 0x24, 0x96, 0xa4, + 0x16, 0x25, 0xe6, 0xc4, 0x83, 0x5c, 0x21, 0xc1, 0x0c, 0x56, 0xc5, 0x87, 0x10, 0x0e, 0xa9, 0x2c, + 0x48, 0x15, 0xb2, 0xe4, 0x62, 0x2f, 0x48, 0xac, 0xcc, 0x4d, 0xcd, 0x2b, 0x91, 0x60, 0x51, 0x60, + 0xc4, 0xef, 0x65, 0x16, 0x90, 0x97, 0x83, 0x60, 0xea, 0x11, 0xfe, 0x70, 0x72, 0x3d, 0xf1, 0x48, + 0x8e, 0xf1, 0xc2, 0x23, 0x39, 0xc6, 0x07, 0x8f, 0xe4, 0x18, 0x27, 0x3c, 0x96, 0x63, 0xb8, 0xf0, + 0x58, 0x8e, 0xe1, 0xc6, 0x63, 0x39, 0x86, 0x28, 0x6d, 0xa4, 0xd0, 0x01, 0xa5, 0x69, 0xdd, 0x9c, + 0xc4, 0xa4, 0x62, 0x30, 0x4b, 0xbf, 0x02, 0x29, 0x1b, 0x80, 0x83, 0x29, 0x89, 0x0d, 0x9c, 0x5c, + 0x8d, 0x01, 0x01, 0x00, 0x00, 0xff, 0xff, 0x4a, 0xf3, 0xc5, 0x33, 0x25, 0x03, 0x00, 0x00, } func (m *CommunityPoolLendDepositProposal) Marshal() (dAtA []byte, err error) { @@ -239,6 +285,60 @@ func (m *CommunityPoolLendWithdrawProposal) MarshalToSizedBuffer(dAtA []byte) (i return len(dAtA) - i, nil } +func (m *CommunityCDPRepayDebtProposal) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *CommunityCDPRepayDebtProposal) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *CommunityCDPRepayDebtProposal) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + { + size, err := m.Payment.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintProposal(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x22 + if len(m.CollateralType) > 0 { + i -= len(m.CollateralType) + copy(dAtA[i:], m.CollateralType) + i = encodeVarintProposal(dAtA, i, uint64(len(m.CollateralType))) + i-- + dAtA[i] = 0x1a + } + if len(m.Description) > 0 { + i -= len(m.Description) + copy(dAtA[i:], m.Description) + i = encodeVarintProposal(dAtA, i, uint64(len(m.Description))) + i-- + dAtA[i] = 0x12 + } + if len(m.Title) > 0 { + i -= len(m.Title) + copy(dAtA[i:], m.Title) + i = encodeVarintProposal(dAtA, i, uint64(len(m.Title))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + func encodeVarintProposal(dAtA []byte, offset int, v uint64) int { offset -= sovProposal(v) base := offset @@ -296,6 +396,29 @@ func (m *CommunityPoolLendWithdrawProposal) Size() (n int) { return n } +func (m *CommunityCDPRepayDebtProposal) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Title) + if l > 0 { + n += 1 + l + sovProposal(uint64(l)) + } + l = len(m.Description) + if l > 0 { + n += 1 + l + sovProposal(uint64(l)) + } + l = len(m.CollateralType) + if l > 0 { + n += 1 + l + sovProposal(uint64(l)) + } + l = m.Payment.Size() + n += 1 + l + sovProposal(uint64(l)) + return n +} + func sovProposal(x uint64) (n int) { return (math_bits.Len64(x|1) + 6) / 7 } @@ -598,6 +721,185 @@ func (m *CommunityPoolLendWithdrawProposal) Unmarshal(dAtA []byte) error { } return nil } +func (m *CommunityCDPRepayDebtProposal) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowProposal + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: CommunityCDPRepayDebtProposal: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: CommunityCDPRepayDebtProposal: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Title", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowProposal + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthProposal + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthProposal + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Title = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Description", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowProposal + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthProposal + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthProposal + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Description = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field CollateralType", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowProposal + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthProposal + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthProposal + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.CollateralType = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 4: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Payment", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowProposal + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthProposal + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthProposal + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := m.Payment.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipProposal(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthProposal + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} func skipProposal(dAtA []byte) (n int, err error) { l := len(dAtA) iNdEx := 0 diff --git a/x/community/types/proposal_test.go b/x/community/types/proposal_test.go index abf7bf89..af61958c 100644 --- a/x/community/types/proposal_test.go +++ b/x/community/types/proposal_test.go @@ -134,3 +134,100 @@ func TestCommunityPoolLendWithdrawProposal_Stringer(t *testing.T) { Amount: 42ukava `, proposal.String()) } + +func TestCommunityCDPRepayDebtProposal_ValidateBasic(t *testing.T) { + type proposalData struct { + Title string + Description string + CollateralType string + Payment sdk.Coin + } + testCases := []struct { + name string + proposal proposalData + expectedErr string + }{ + { + name: "valid proposal", + proposal: proposalData{ + Title: "Repay my debt plz", + Description: "I interact with cdp", + CollateralType: "type-a", + Payment: sdk.NewInt64Coin("ukava", 1e6), + }, + expectedErr: "", + }, + { + name: "invalid - fails gov validation", + proposal: proposalData{ + Description: "I have no title.", + }, + expectedErr: "invalid proposal content", + }, + { + name: "invalid - empty collateral type", + proposal: proposalData{ + Title: "Error profoundly", + Description: "I have no collateral type", + }, + expectedErr: "collateral type cannot be blank", + }, + { + name: "invalid - empty coins", + proposal: proposalData{ + Title: "Error profoundly", + Description: "My coins are empty", + CollateralType: "type-a", + Payment: sdk.Coin{}, + }, + expectedErr: "invalid coins", + }, + { + name: "invalid - zero coins", + proposal: proposalData{ + Title: "Error profoundly", + Description: "My coins are zero", + CollateralType: "type-a", + Payment: sdk.NewInt64Coin("ukava", 0), + }, + expectedErr: "invalid coins", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + repayDebt := types.NewCommunityCDPRepayDebtProposal( + tc.proposal.Title, + tc.proposal.Description, + tc.proposal.CollateralType, + tc.proposal.Payment, + ) + err := repayDebt.ValidateBasic() + if tc.expectedErr != "" { + require.ErrorContains(t, err, tc.expectedErr) + return + } + + require.NoError(t, err) + require.Equal(t, repayDebt.Title, repayDebt.GetTitle()) + require.Equal(t, repayDebt.Description, repayDebt.GetDescription()) + require.Equal(t, types.ModuleName, repayDebt.ProposalRoute()) + require.Equal(t, types.ProposalTypeCommunityCDPRepayDebt, repayDebt.ProposalType()) + }) + } +} + +func TestCommunityCDPRepayDebtProposal_Stringer(t *testing.T) { + proposal := types.NewCommunityCDPRepayDebtProposal( + "title", + "description", + "collateral-type", + sdk.NewInt64Coin("ukava", 42), + ) + require.Equal(t, `Community CDP Repay Debt Proposal: + Title: title + Description: description + Collateral Type: collateral-type + Payment: 42ukava +`, proposal.String()) +}