Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add community cdp repay debt proposal #1565

Merged
merged 7 commits into from
Apr 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -635,8 +635,9 @@ func NewApp(
app.communityKeeper = communitykeeper.NewKeeper(
app.accountKeeper,
app.bankKeeper,
&cdpKeeper,
app.distrKeeper,
hardKeeper,
&hardKeeper,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

creating a public record of the fact that because this was originally passed as a copy, any positions it created never had claims instantiated (because the hooks do not get registered to the copy).
this may have consequences for the open lend position held by the community module.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

confirmed with @rhuairahrighairidh any fixes needed for this would be in a follow up pr. commencing merge!

)
app.kavadistKeeper = kavadistkeeper.NewKeeper(
appCodec,
Expand Down
20 changes: 20 additions & 0 deletions docs/core/proto-docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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>

### CommunityPoolLendDepositProposal
Expand Down
12 changes: 12 additions & 0 deletions proto/kava/community/v1beta1/proposal.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
pirtleshell marked this conversation as resolved.
Show resolved Hide resolved
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];
}
2 changes: 2 additions & 0 deletions x/community/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 3 additions & 1 deletion x/community/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -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,
Expand Down
6 changes: 6 additions & 0 deletions x/community/keeper/proposal_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
118 changes: 113 additions & 5 deletions x/community/keeper/proposal_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 {
Expand All @@ -40,6 +42,7 @@ type proposalTestSuite struct {
Keeper keeper.Keeper
MaccAddress sdk.AccAddress

cdpKeeper cdpkeeper.Keeper
hardKeeper hardkeeper.Keeper
}

Expand Down Expand Up @@ -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))
}
Expand Down Expand Up @@ -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")
})
}
}
56 changes: 56 additions & 0 deletions x/community/testutil/cdp_genesis.go
Original file line number Diff line number Diff line change
@@ -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)}
}
2 changes: 2 additions & 0 deletions x/community/types/codec.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -26,6 +27,7 @@ func RegisterInterfaces(registry types.InterfaceRegistry) {
registry.RegisterImplementations((*govv1beta1.Content)(nil),
&CommunityPoolLendDepositProposal{},
&CommunityPoolLendWithdrawProposal{},
&CommunityCDPRepayDebtProposal{},
)

msgservice.RegisterMsgServiceDesc(registry, &_Msg_serviceDesc)
Expand Down
5 changes: 5 additions & 0 deletions x/community/types/expected_keepers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading