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
This commit is contained in:
Robert Pirtle 2023-04-20 14:13:16 -07:00 committed by GitHub
parent 99890b9c84
commit c5f414e6a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 699 additions and 13 deletions

View File

@ -47,6 +47,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
- (x/committee) [#1562] Add CommunityPoolLendWithdrawPermission - (x/committee) [#1562] Add CommunityPoolLendWithdrawPermission
- (x/community) [#1563] Include x/community module pool balance in x/distribution - (x/community) [#1563] Include x/community module pool balance in x/distribution
community_pool query response. community_pool query response.
- (x/community) [#1565] Add CommunityCDPRepayDebtProposal
### Deprecated ### 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 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 [#1563]: https://github.com/Kava-Labs/kava/pull/1563
[#1562]: https://github.com/Kava-Labs/kava/pull/1562 [#1562]: https://github.com/Kava-Labs/kava/pull/1562
[#1550]: https://github.com/Kava-Labs/kava/pull/1550 [#1550]: https://github.com/Kava-Labs/kava/pull/1550

View File

@ -635,8 +635,9 @@ func NewApp(
app.communityKeeper = communitykeeper.NewKeeper( app.communityKeeper = communitykeeper.NewKeeper(
app.accountKeeper, app.accountKeeper,
app.bankKeeper, app.bankKeeper,
&cdpKeeper,
app.distrKeeper, app.distrKeeper,
hardKeeper, &hardKeeper,
) )
app.kavadistKeeper = kavadistkeeper.NewKeeper( app.kavadistKeeper = kavadistkeeper.NewKeeper(
appCodec, appCodec,

View File

@ -180,6 +180,7 @@
- [Msg](#kava.committee.v1beta1.Msg) - [Msg](#kava.committee.v1beta1.Msg)
- [kava/community/v1beta1/proposal.proto](#kava/community/v1beta1/proposal.proto) - [kava/community/v1beta1/proposal.proto](#kava/community/v1beta1/proposal.proto)
- [CommunityCDPRepayDebtProposal](#kava.community.v1beta1.CommunityCDPRepayDebtProposal)
- [CommunityPoolLendDepositProposal](#kava.community.v1beta1.CommunityPoolLendDepositProposal) - [CommunityPoolLendDepositProposal](#kava.community.v1beta1.CommunityPoolLendDepositProposal)
- [CommunityPoolLendWithdrawProposal](#kava.community.v1beta1.CommunityPoolLendWithdrawProposal) - [CommunityPoolLendWithdrawProposal](#kava.community.v1beta1.CommunityPoolLendWithdrawProposal)
@ -2857,6 +2858,25 @@ Msg defines the committee Msg service
<a name="kava.community.v1beta1.CommunityCDPRepayDebtProposal"></a>
### 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) | | |
<a name="kava.community.v1beta1.CommunityPoolLendDepositProposal"></a> <a name="kava.community.v1beta1.CommunityPoolLendDepositProposal"></a>
### CommunityPoolLendDepositProposal ### CommunityPoolLendDepositProposal

View File

@ -31,3 +31,15 @@ message CommunityPoolLendWithdrawProposal {
(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins" (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];
}

View File

@ -14,6 +14,8 @@ import (
func NewCommunityPoolProposalHandler(k keeper.Keeper) govv1beta1.Handler { func NewCommunityPoolProposalHandler(k keeper.Keeper) govv1beta1.Handler {
return func(ctx sdk.Context, content govv1beta1.Content) error { return func(ctx sdk.Context, content govv1beta1.Content) error {
switch c := content.(type) { switch c := content.(type) {
case *types.CommunityCDPRepayDebtProposal:
return keeper.HandleCommunityCDPRepayDebtProposal(ctx, k, c)
case *types.CommunityPoolLendDepositProposal: case *types.CommunityPoolLendDepositProposal:
return keeper.HandleCommunityPoolLendDepositProposal(ctx, k, c) return keeper.HandleCommunityPoolLendDepositProposal(ctx, k, c)
case *types.CommunityPoolLendWithdrawProposal: case *types.CommunityPoolLendWithdrawProposal:

View File

@ -12,6 +12,7 @@ import (
// Keeper of the community store // Keeper of the community store
type Keeper struct { type Keeper struct {
bankKeeper types.BankKeeper bankKeeper types.BankKeeper
cdpKeeper types.CdpKeeper
distrKeeper types.DistributionKeeper distrKeeper types.DistributionKeeper
hardKeeper types.HardKeeper hardKeeper types.HardKeeper
moduleAddress sdk.AccAddress moduleAddress sdk.AccAddress
@ -20,7 +21,7 @@ type Keeper struct {
} }
// NewKeeper creates a new community Keeper instance // 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 // ensure community module account is set
addr := ak.GetModuleAddress(types.ModuleAccountName) addr := ak.GetModuleAddress(types.ModuleAccountName)
if addr == nil { if addr == nil {
@ -33,6 +34,7 @@ func NewKeeper(ak types.AccountKeeper, bk types.BankKeeper, dk types.Distributio
return Keeper{ return Keeper{
bankKeeper: bk, bankKeeper: bk,
cdpKeeper: ck,
distrKeeper: dk, distrKeeper: dk,
hardKeeper: hk, hardKeeper: hk,
moduleAddress: addr, moduleAddress: addr,

View File

@ -36,3 +36,9 @@ func HandleCommunityPoolLendWithdrawProposal(ctx sdk.Context, k Keeper, p *types
// send all withdrawn coins back to community pool // send all withdrawn coins back to community pool
return k.distrKeeper.FundCommunityPool(ctx, totalWithdrawn, k.moduleAddress) 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)
}

View File

@ -12,6 +12,7 @@ import (
tmtime "github.com/tendermint/tendermint/types/time" tmtime "github.com/tendermint/tendermint/types/time"
"github.com/kava-labs/kava/app" "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/keeper"
"github.com/kava-labs/kava/x/community/testutil" "github.com/kava-labs/kava/x/community/testutil"
"github.com/kava-labs/kava/x/community/types" "github.com/kava-labs/kava/x/community/types"
@ -22,14 +23,15 @@ import (
const chainID = "kavatest_2221-1" const chainID = "kavatest_2221-1"
func c(denom string, amount int64) sdk.Coin { return sdk.NewInt64Coin(denom, amount) }
func ukava(amt int64) sdk.Coins { 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 { 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 { func otherdenom(amt int64) sdk.Coins {
return sdk.NewCoins(sdk.NewInt64Coin("other-denom", amt)) return sdk.NewCoins(c("other-denom", amt))
} }
type proposalTestSuite struct { type proposalTestSuite struct {
@ -40,6 +42,7 @@ type proposalTestSuite struct {
Keeper keeper.Keeper Keeper keeper.Keeper
MaccAddress sdk.AccAddress MaccAddress sdk.AccAddress
cdpKeeper cdpkeeper.Keeper
hardKeeper hardkeeper.Keeper hardKeeper hardkeeper.Keeper
} }
@ -68,19 +71,21 @@ func (suite *proposalTestSuite) SetupTest() {
genTime, chainID, genTime, chainID,
app.GenesisState{hardtypes.ModuleName: tApp.AppCodec().MustMarshalJSON(&hardGS)}, app.GenesisState{hardtypes.ModuleName: tApp.AppCodec().MustMarshalJSON(&hardGS)},
app.GenesisState{pricefeedtypes.ModuleName: tApp.AppCodec().MustMarshalJSON(&pricefeedGS)}, app.GenesisState{pricefeedtypes.ModuleName: tApp.AppCodec().MustMarshalJSON(&pricefeedGS)},
testutil.NewCDPGenState(tApp.AppCodec(), "ukava", "kava", sdk.NewDec(2)),
) )
suite.App = tApp suite.App = tApp
suite.Ctx = ctx suite.Ctx = ctx
suite.Keeper = tApp.GetCommunityKeeper() suite.Keeper = tApp.GetCommunityKeeper()
suite.MaccAddress = tApp.GetAccountKeeper().GetModuleAddress(types.ModuleAccountName) suite.MaccAddress = tApp.GetAccountKeeper().GetModuleAddress(types.ModuleAccountName)
suite.cdpKeeper = suite.App.GetCDPKeeper()
suite.hardKeeper = suite.App.GetHardKeeper() suite.hardKeeper = suite.App.GetHardKeeper()
// give the community pool some funds // give the community pool some funds
// ukava // ukava
suite.FundCommunityPool(ukava(1e10)) suite.FundCommunityPool(ukava(2e10))
// usdx // usdx
suite.FundCommunityPool(usdx(1e10)) suite.FundCommunityPool(usdx(2e10))
// other-denom // other-denom
suite.FundCommunityPool(otherdenom(1e10)) 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")
})
}
}

View File

@ -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)}
}

View File

@ -15,6 +15,7 @@ func RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) {
cdc.RegisterConcrete(&MsgFundCommunityPool{}, "community/MsgFundCommunityPool", nil) cdc.RegisterConcrete(&MsgFundCommunityPool{}, "community/MsgFundCommunityPool", nil)
cdc.RegisterConcrete(&CommunityPoolLendDepositProposal{}, "kava/CommunityPoolLendDepositProposal", nil) cdc.RegisterConcrete(&CommunityPoolLendDepositProposal{}, "kava/CommunityPoolLendDepositProposal", nil)
cdc.RegisterConcrete(&CommunityPoolLendWithdrawProposal{}, "kava/CommunityPoolLendWithdrawProposal", nil) cdc.RegisterConcrete(&CommunityPoolLendWithdrawProposal{}, "kava/CommunityPoolLendWithdrawProposal", nil)
cdc.RegisterConcrete(&CommunityCDPRepayDebtProposal{}, "kava/CommunityCDPRepayDebtProposal", nil)
} }
// RegisterInterfaces registers proto messages under their interfaces for unmarshalling, // RegisterInterfaces registers proto messages under their interfaces for unmarshalling,
@ -26,6 +27,7 @@ func RegisterInterfaces(registry types.InterfaceRegistry) {
registry.RegisterImplementations((*govv1beta1.Content)(nil), registry.RegisterImplementations((*govv1beta1.Content)(nil),
&CommunityPoolLendDepositProposal{}, &CommunityPoolLendDepositProposal{},
&CommunityPoolLendWithdrawProposal{}, &CommunityPoolLendWithdrawProposal{},
&CommunityCDPRepayDebtProposal{},
) )
msgservice.RegisterMsgServiceDesc(registry, &_Msg_serviceDesc) msgservice.RegisterMsgServiceDesc(registry, &_Msg_serviceDesc)

View File

@ -19,6 +19,11 @@ type BankKeeper interface {
SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error 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. // HardKeeper defines the contract needed to be fulfilled for Kava Lend dependencies.
type HardKeeper interface { type HardKeeper interface {
Deposit(ctx sdk.Context, depositor sdk.AccAddress, coins sdk.Coins) error Deposit(ctx sdk.Context, depositor sdk.AccAddress, coins sdk.Coins) error

View File

@ -1,6 +1,7 @@
package types package types
import ( import (
"errors"
fmt "fmt" fmt "fmt"
"strings" "strings"
@ -15,12 +16,15 @@ const (
ProposalTypeCommunityPoolLendDeposit = "CommunityPoolLendDeposit" ProposalTypeCommunityPoolLendDeposit = "CommunityPoolLendDeposit"
// ProposalTypeCommunityPoolLendWithdraw defines the type for a CommunityPoolLendDepositProposal // ProposalTypeCommunityPoolLendWithdraw defines the type for a CommunityPoolLendDepositProposal
ProposalTypeCommunityPoolLendWithdraw = "CommunityPoolLendWithdraw" ProposalTypeCommunityPoolLendWithdraw = "CommunityPoolLendWithdraw"
// ProposalTypeCommunityCDPRepayDebt defines the type for a CommunityCDPRepayDebtProposal
ProposalTypeCommunityCDPRepayDebt = "CommunityCDPRepayDebt"
) )
// Assert CommunityPoolLendDepositProposal implements govtypes.Content at compile-time // Assert CommunityPoolLendDepositProposal implements govtypes.Content at compile-time
var ( var (
_ govv1beta1.Content = &CommunityPoolLendDepositProposal{} _ govv1beta1.Content = &CommunityPoolLendDepositProposal{}
_ govv1beta1.Content = &CommunityPoolLendWithdrawProposal{} _ govv1beta1.Content = &CommunityPoolLendWithdrawProposal{}
_ govv1beta1.Content = &CommunityCDPRepayDebtProposal{}
) )
func init() { func init() {
@ -28,8 +32,14 @@ func init() {
govv1beta1.ModuleCdc.Amino.RegisterConcrete(&CommunityPoolLendDepositProposal{}, "kava/CommunityPoolLendDepositProposal", nil) govv1beta1.ModuleCdc.Amino.RegisterConcrete(&CommunityPoolLendDepositProposal{}, "kava/CommunityPoolLendDepositProposal", nil)
govv1beta1.RegisterProposalType(ProposalTypeCommunityPoolLendWithdraw) govv1beta1.RegisterProposalType(ProposalTypeCommunityPoolLendWithdraw)
govv1beta1.ModuleCdc.Amino.RegisterConcrete(&CommunityPoolLendWithdrawProposal{}, "kava/CommunityPoolLendWithdrawProposal", nil) 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. // NewCommunityPoolLendDepositProposal creates a new community pool deposit proposal.
func NewCommunityPoolLendDepositProposal(title, description string, amount sdk.Coins) *CommunityPoolLendDepositProposal { func NewCommunityPoolLendDepositProposal(title, description string, amount sdk.Coins) *CommunityPoolLendDepositProposal {
return &CommunityPoolLendDepositProposal{ return &CommunityPoolLendDepositProposal{
@ -121,3 +131,64 @@ func (p *CommunityPoolLendWithdrawProposal) ValidateBasic() error {
} }
return p.Amount.Validate() 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
}

View File

@ -103,9 +103,51 @@ func (m *CommunityPoolLendWithdrawProposal) XXX_DiscardUnknown() {
var xxx_messageInfo_CommunityPoolLendWithdrawProposal proto.InternalMessageInfo 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() { func init() {
proto.RegisterType((*CommunityPoolLendDepositProposal)(nil), "kava.community.v1beta1.CommunityPoolLendDepositProposal") proto.RegisterType((*CommunityPoolLendDepositProposal)(nil), "kava.community.v1beta1.CommunityPoolLendDepositProposal")
proto.RegisterType((*CommunityPoolLendWithdrawProposal)(nil), "kava.community.v1beta1.CommunityPoolLendWithdrawProposal") proto.RegisterType((*CommunityPoolLendWithdrawProposal)(nil), "kava.community.v1beta1.CommunityPoolLendWithdrawProposal")
proto.RegisterType((*CommunityCDPRepayDebtProposal)(nil), "kava.community.v1beta1.CommunityCDPRepayDebtProposal")
} }
func init() { func init() {
@ -113,7 +155,7 @@ func init() {
} }
var fileDescriptor_64aa83b2ed448ec1 = []byte{ 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, 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, 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, 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, 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, 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, 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, 0x9a, 0x67, 0x8e, 0x32, 0x72, 0xc9, 0xc2, 0x3d, 0xe3, 0xec, 0x12, 0x10, 0x94, 0x5a, 0x90, 0x58,
0xc6, 0x09, 0x8f, 0xe5, 0x18, 0x2e, 0x3c, 0x96, 0x63, 0xb8, 0xf1, 0x58, 0x8e, 0x21, 0x4a, 0x1b, 0xe9, 0x92, 0x9a, 0x44, 0x79, 0xac, 0xa8, 0x73, 0xf1, 0x27, 0xe7, 0xe7, 0xe4, 0x24, 0x96, 0xa4,
0xc9, 0x54, 0x50, 0x5a, 0xd0, 0xcd, 0x49, 0x4c, 0x2a, 0x06, 0xb3, 0xf4, 0x2b, 0x90, 0x92, 0x0f, 0x16, 0x25, 0xe6, 0xc4, 0x83, 0x5c, 0x21, 0xc1, 0x0c, 0x56, 0xc5, 0x87, 0x10, 0x0e, 0xa9, 0x2c,
0xd8, 0xf8, 0x24, 0x36, 0x70, 0x34, 0x1b, 0x03, 0x02, 0x00, 0x00, 0xff, 0xff, 0xa0, 0x07, 0x87, 0x48, 0x15, 0xb2, 0xe4, 0x62, 0x2f, 0x48, 0xac, 0xcc, 0x4d, 0xcd, 0x2b, 0x91, 0x60, 0x51, 0x60,
0x4d, 0x5d, 0x02, 0x00, 0x00, 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) { func (m *CommunityPoolLendDepositProposal) Marshal() (dAtA []byte, err error) {
@ -239,6 +285,60 @@ func (m *CommunityPoolLendWithdrawProposal) MarshalToSizedBuffer(dAtA []byte) (i
return len(dAtA) - i, nil 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 { func encodeVarintProposal(dAtA []byte, offset int, v uint64) int {
offset -= sovProposal(v) offset -= sovProposal(v)
base := offset base := offset
@ -296,6 +396,29 @@ func (m *CommunityPoolLendWithdrawProposal) Size() (n int) {
return n 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) { func sovProposal(x uint64) (n int) {
return (math_bits.Len64(x|1) + 6) / 7 return (math_bits.Len64(x|1) + 6) / 7
} }
@ -598,6 +721,185 @@ func (m *CommunityPoolLendWithdrawProposal) Unmarshal(dAtA []byte) error {
} }
return nil 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) { func skipProposal(dAtA []byte) (n int, err error) {
l := len(dAtA) l := len(dAtA)
iNdEx := 0 iNdEx := 0

View File

@ -134,3 +134,100 @@ func TestCommunityPoolLendWithdrawProposal_Stringer(t *testing.T) {
Amount: 42ukava Amount: 42ukava
`, proposal.String()) `, 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())
}