From 9b1bf55be78bd8005b47c55bc6ad5e39719ecdac Mon Sep 17 00:00:00 2001 From: Kevin Davis Date: Wed, 15 Jan 2020 15:19:33 +0100 Subject: [PATCH] R4R: Move liquidator functions to cdp module (#280) * wip: tpyes and keeper methods * wip: iterators * wip: types and keeper methods * wip: add msgs * wip: client methods * wip: rebase develop * wip: types tests * wip: keeper tests, small fixes * wip: add cdp tests * wip: deposit tests * wip: keeper tests * wip: tests and module methods * feat: error when fetching expired price * feat: conversion factor for external assets * feat: debt floor for new cdps * feat: save deposits on export genesis * feat: ensure messages implement msg * feat: index deposits by status * fix: stray comment * wip: address review comments * address review comments * wip: move liquidation to cdp module * wip: handle liquidations directly * wip: use new auction interface * feat: auction collateral in cdp begin block * feat: update param validation * feat: surplus and debt auctions * address review comments * address review comments * fix: auction multiple deposits * clean up netting function --- .gitignore | 3 + app/app.go | 15 +- app/test_common.go | 28 ++-- x/auction/keeper/keeper.go | 3 + x/auction/types/expected_keepers.go | 1 + x/cdp/abci.go | 26 +++- x/cdp/abci_test.go | 3 +- x/cdp/alias.go | 14 +- x/cdp/client/cli/query.go | 7 +- x/cdp/genesis.go | 1 + x/cdp/integration_test.go | 75 ++++++--- x/cdp/keeper/auctions.go | 231 ++++++++++++++++++++++++++++ x/cdp/keeper/auctions_test.go | 76 +++++++++ x/cdp/keeper/cdp.go | 18 +++ x/cdp/keeper/deposit.go | 79 ++-------- x/cdp/keeper/deposit_test.go | 46 +----- x/cdp/keeper/draw.go | 14 +- x/cdp/keeper/integration_test.go | 58 ++++--- x/cdp/keeper/keeper.go | 11 +- x/cdp/keeper/params.go | 19 ++- x/cdp/keeper/seize.go | 82 +++++----- x/cdp/keeper/seize_test.go | 42 ++++- x/cdp/types/deposit.go | 53 ++----- x/cdp/types/events.go | 16 +- x/cdp/types/expected_keepers.go | 7 + x/cdp/types/genesis.go | 2 + x/cdp/types/keys.go | 32 ++-- x/cdp/types/keys_test.go | 12 +- x/cdp/types/params.go | 68 +++++--- 29 files changed, 706 insertions(+), 336 deletions(-) create mode 100644 x/cdp/keeper/auctions.go create mode 100644 x/cdp/keeper/auctions_test.go diff --git a/.gitignore b/.gitignore index ae38c8a5..648f2b9b 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,6 @@ # Exclude build files vendor build + +# IDE files +*.vscode \ No newline at end of file diff --git a/app/app.go b/app/app.go index 5ad36569..70ea5031 100644 --- a/app/app.go +++ b/app/app.go @@ -229,18 +229,19 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, pricefeedSubspace, pricefeed.DefaultCodespace) // NewKeeper(cdc *codec.Codec, key sdk.StoreKey, paramstore subspace.Subspace, pfk types.PricefeedKeeper, sk types.SupplyKeeper, codespace sdk.CodespaceType) - app.cdpKeeper = cdp.NewKeeper( - app.cdc, - keys[cdp.StoreKey], - cdpSubspace, - app.pricefeedKeeper, - app.supplyKeeper, - cdp.DefaultCodespace) app.auctionKeeper = auction.NewKeeper( app.cdc, keys[auction.StoreKey], app.supplyKeeper, auctionSubspace) + app.cdpKeeper = cdp.NewKeeper( + app.cdc, + keys[cdp.StoreKey], + cdpSubspace, + app.pricefeedKeeper, + app.auctionKeeper, + app.supplyKeeper, + cdp.DefaultCodespace) // register the staking hooks // NOTE: stakingKeeper above is passed by reference, so that it will contain these hooks diff --git a/app/test_common.go b/app/test_common.go index 2b598bb5..7bb0da05 100644 --- a/app/test_common.go +++ b/app/test_common.go @@ -53,20 +53,20 @@ func NewTestApp() TestApp { return TestApp{App: *app} } -func (tApp TestApp) GetAccountKeeper() auth.AccountKeeper { return tApp.accountKeeper } -func (tApp TestApp) GetBankKeeper() bank.Keeper { return tApp.bankKeeper } -func (tApp TestApp) GetSupplyKeeper() supply.Keeper { return tApp.supplyKeeper } -func (tApp TestApp) GetStakingKeeper() staking.Keeper { return tApp.stakingKeeper } -func (tApp TestApp) GetSlashingKeeper() slashing.Keeper { return tApp.slashingKeeper } -func (tApp TestApp) GetMintKeeper() mint.Keeper { return tApp.mintKeeper } -func (tApp TestApp) GetDistrKeeper() distribution.Keeper { return tApp.distrKeeper } -func (tApp TestApp) GetGovKeeper() gov.Keeper { return tApp.govKeeper } -func (tApp TestApp) GetCrisisKeeper() crisis.Keeper { return tApp.crisisKeeper } -func (tApp TestApp) GetParamsKeeper() params.Keeper { return tApp.paramsKeeper } -func (tApp TestApp) GetVVKeeper() validatorvesting.Keeper { return tApp.vvKeeper } -func (tApp TestApp) GetAuctionKeeper() auction.Keeper { return tApp.auctionKeeper } -func (tApp TestApp) GetCDPKeeper() cdp.Keeper { return tApp.cdpKeeper } -func (tApp TestApp) GetPriceFeedKeeper() pricefeed.Keeper { return tApp.pricefeedKeeper } +func (tApp TestApp) GetAccountKeeper() auth.AccountKeeper { return tApp.accountKeeper } +func (tApp TestApp) GetBankKeeper() bank.Keeper { return tApp.bankKeeper } +func (tApp TestApp) GetSupplyKeeper() supply.Keeper { return tApp.supplyKeeper } +func (tApp TestApp) GetStakingKeeper() staking.Keeper { return tApp.stakingKeeper } +func (tApp TestApp) GetSlashingKeeper() slashing.Keeper { return tApp.slashingKeeper } +func (tApp TestApp) GetMintKeeper() mint.Keeper { return tApp.mintKeeper } +func (tApp TestApp) GetDistrKeeper() distribution.Keeper { return tApp.distrKeeper } +func (tApp TestApp) GetGovKeeper() gov.Keeper { return tApp.govKeeper } +func (tApp TestApp) GetCrisisKeeper() crisis.Keeper { return tApp.crisisKeeper } +func (tApp TestApp) GetParamsKeeper() params.Keeper { return tApp.paramsKeeper } +func (tApp TestApp) GetVVKeeper() validatorvesting.Keeper { return tApp.vvKeeper } +func (tApp TestApp) GetAuctionKeeper() auction.Keeper { return tApp.auctionKeeper } +func (tApp TestApp) GetCDPKeeper() cdp.Keeper { return tApp.cdpKeeper } +func (tApp TestApp) GetPriceFeedKeeper() pricefeed.Keeper { return tApp.pricefeedKeeper } // This calls InitChain on the app using the default genesis state, overwitten with any passed in genesis states func (tApp TestApp) InitializeFromGenesisStates(genesisStates ...GenesisState) TestApp { diff --git a/x/auction/keeper/keeper.go b/x/auction/keeper/keeper.go index 49229461..7a51302b 100644 --- a/x/auction/keeper/keeper.go +++ b/x/auction/keeper/keeper.go @@ -23,6 +23,9 @@ type Keeper struct { // NewKeeper returns a new auction keeper. func NewKeeper(cdc *codec.Codec, storeKey sdk.StoreKey, supplyKeeper types.SupplyKeeper, paramstore subspace.Subspace) Keeper { + if addr := supplyKeeper.GetModuleAddress(types.ModuleName); addr == nil { + panic(fmt.Sprintf("%s module account has not been set", types.ModuleName)) + } return Keeper{ supplyKeeper: supplyKeeper, storeKey: storeKey, diff --git a/x/auction/types/expected_keepers.go b/x/auction/types/expected_keepers.go index 021b8918..83141c9e 100644 --- a/x/auction/types/expected_keepers.go +++ b/x/auction/types/expected_keepers.go @@ -7,6 +7,7 @@ import ( // SupplyKeeper defines the expected supply Keeper type SupplyKeeper interface { + GetModuleAddress(name string) sdk.AccAddress GetModuleAccount(ctx sdk.Context, moduleName string) supplyexported.ModuleAccountI SendCoinsFromModuleToModule(ctx sdk.Context, sender, recipient string, amt sdk.Coins) sdk.Error diff --git a/x/cdp/abci.go b/x/cdp/abci.go index 5c3e1bb9..cc36bd74 100644 --- a/x/cdp/abci.go +++ b/x/cdp/abci.go @@ -1,11 +1,14 @@ package cdp import ( + "fmt" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/kava-labs/kava/x/cdp/types" abci "github.com/tendermint/tendermint/abci/types" ) -// BeginBlock compounds the debt in outstanding cdps and liquidates cdps that are below the required collateralization ratio +// BeginBlocker compounds the debt in outstanding cdps and liquidates cdps that are below the required collateralization ratio func BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock, k Keeper) { params := k.GetParams(ctx) previousBlockTime, found := k.GetPreviousBlockTime(ctx) @@ -18,7 +21,26 @@ func BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock, k Keeper) { k.HandleNewDebt(ctx, cp.Denom, dp.Denom, timeElapsed) } - k.LiquidateCdps(ctx, cp.MarketID, cp.Denom, cp.LiquidationRatio) + err := k.LiquidateCdps(ctx, cp.MarketID, cp.Denom, cp.LiquidationRatio) + if err != nil { + ctx.EventManager().EmitEvent( + sdk.NewEvent( + EventTypeBeginBlockerFatal, + sdk.NewAttribute(sdk.AttributeKeyModule, fmt.Sprintf("%s", ModuleName)), + sdk.NewAttribute(types.AttributeKeyError, fmt.Sprintf("%s", err)), + ), + ) + } + } + err := k.RunSurplusAndDebtAuctions(ctx) + if err != nil { + ctx.EventManager().EmitEvent( + sdk.NewEvent( + EventTypeBeginBlockerFatal, + sdk.NewAttribute(sdk.AttributeKeyModule, fmt.Sprintf("%s", ModuleName)), + sdk.NewAttribute(types.AttributeKeyError, fmt.Sprintf("%s", err)), + ), + ) } k.SetPreviousBlockTime(ctx, ctx.BlockTime()) return diff --git a/x/cdp/abci_test.go b/x/cdp/abci_test.go index f9249856..e8c9573f 100644 --- a/x/cdp/abci_test.go +++ b/x/cdp/abci_test.go @@ -8,6 +8,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/simulation" "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/auction" "github.com/kava-labs/kava/x/cdp" "github.com/stretchr/testify/suite" abci "github.com/tendermint/tendermint/abci/types" @@ -116,7 +117,7 @@ func (suite *ModuleTestSuite) TestBeginBlock() { btcLiquidations := int(seizedBtcCollateral.Quo(i(100000000)).Int64()) suite.Equal(len(suite.liquidations.btc), btcLiquidations) - acc = sk.GetModuleAccount(suite.ctx, cdp.LiquidatorMacc) + acc = sk.GetModuleAccount(suite.ctx, auction.ModuleName) suite.Equal(suite.liquidations.debt, acc.GetCoins().AmountOf("debt").Int64()) } diff --git a/x/cdp/alias.go b/x/cdp/alias.go index 19618d6a..951dff00 100644 --- a/x/cdp/alias.go +++ b/x/cdp/alias.go @@ -11,8 +11,6 @@ import ( ) const ( - StatusNil = types.StatusNil - StatusLiquidated = types.StatusLiquidated DefaultCodespace = types.DefaultCodespace CodeCdpAlreadyExists = types.CodeCdpAlreadyExists CodeCollateralLengthInvalid = types.CodeCollateralLengthInvalid @@ -36,15 +34,17 @@ const ( EventTypeCdpClose = types.EventTypeCdpClose EventTypeCdpWithdrawal = types.EventTypeCdpWithdrawal EventTypeCdpLiquidation = types.EventTypeCdpLiquidation + EventTypeBeginBlockerFatal = types.EventTypeBeginBlockerFatal AttributeKeyCdpID = types.AttributeKeyCdpID AttributeKeyDepositor = types.AttributeKeyDepositor AttributeValueCategory = types.AttributeValueCategory - LiquidatorMacc = types.LiquidatorMacc + AttributeKeyError = types.AttributeKeyError ModuleName = types.ModuleName StoreKey = types.StoreKey RouterKey = types.RouterKey QuerierRoute = types.QuerierRoute DefaultParamspace = types.DefaultParamspace + LiquidatorMacc = types.LiquidatorMacc QueryGetCdp = types.QueryGetCdp QueryGetCdps = types.QueryGetCdps QueryGetCdpsByCollateralization = types.QueryGetCdpsByCollateralization @@ -58,7 +58,6 @@ var ( // functions aliases NewCDP = types.NewCDP RegisterCodec = types.RegisterCodec - StatusFromByte = types.StatusFromByte NewDeposit = types.NewDeposit ErrCdpAlreadyExists = types.ErrCdpAlreadyExists ErrInvalidCollateralLength = types.ErrInvalidCollateralLength @@ -116,6 +115,7 @@ var ( CollateralRatioIndexPrefix = types.CollateralRatioIndexPrefix CdpIDKey = types.CdpIDKey DebtDenomKey = types.DebtDenomKey + GovDenomKey = types.GovDenomKey DepositKeyPrefix = types.DepositKeyPrefix PrincipalKeyPrefix = types.PrincipalKeyPrefix AccumulatorKeyPrefix = types.AccumulatorKeyPrefix @@ -124,12 +124,17 @@ var ( KeyCollateralParams = types.KeyCollateralParams KeyDebtParams = types.KeyDebtParams KeyCircuitBreaker = types.KeyCircuitBreaker + KeyDebtThreshold = types.KeyDebtThreshold + KeySurplusThreshold = types.KeySurplusThreshold DefaultGlobalDebt = types.DefaultGlobalDebt DefaultCircuitBreaker = types.DefaultCircuitBreaker DefaultCollateralParams = types.DefaultCollateralParams DefaultDebtParams = types.DefaultDebtParams DefaultCdpStartingID = types.DefaultCdpStartingID DefaultDebtDenom = types.DefaultDebtDenom + DefaultGovDenom = types.DefaultGovDenom + DefaultSurplusThreshold = types.DefaultSurplusThreshold + DefaultDebtThreshold = types.DefaultDebtThreshold DefaultPreviousBlockTime = types.DefaultPreviousBlockTime MaxSortableDec = types.MaxSortableDec ) @@ -138,7 +143,6 @@ type ( CDP = types.CDP CDPs = types.CDPs Deposit = types.Deposit - DepositStatus = types.DepositStatus Deposits = types.Deposits SupplyKeeper = types.SupplyKeeper PricefeedKeeper = types.PricefeedKeeper diff --git a/x/cdp/client/cli/query.go b/x/cdp/client/cli/query.go index eedd74bc..45b768ec 100644 --- a/x/cdp/client/cli/query.go +++ b/x/cdp/client/cli/query.go @@ -47,8 +47,9 @@ func QueryCdpCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { return err } collateralType := args[1] // TODO validation? - bz, err := cdc.MarshalJSON(types.QueryCdpsParams{ + bz, err := cdc.MarshalJSON(types.QueryCdpParams{ CollateralDenom: collateralType, + Owner: ownerAddress, }) if err != nil { return err @@ -58,8 +59,6 @@ func QueryCdpCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { route := fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetCdp) res, _, err := cliCtx.QueryWithData(route, bz) if err != nil { - fmt.Printf("error when getting cdp info - %s", err) - fmt.Printf("could not get current cdp info - %s %s \n", string(ownerAddress), string(collateralType)) return err } @@ -78,7 +77,7 @@ func QueryCdpsByDenomCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { Short: "Query cdps by collateral type", Long: strings.TrimSpace(`Query cdps by a specific collateral type, or query all cdps if none is specifed: -$ query cdp cdps atom +$ query cdp cdps uatom `), Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { diff --git a/x/cdp/genesis.go b/x/cdp/genesis.go index 10de0f86..ba6fa416 100644 --- a/x/cdp/genesis.go +++ b/x/cdp/genesis.go @@ -51,6 +51,7 @@ func InitGenesis(ctx sdk.Context, k Keeper, pk PricefeedKeeper, gs GenesisState) k.SetNextCdpID(ctx, gs.StartingCdpID) k.SetDebtDenom(ctx, gs.DebtDenom) + k.SetGovDenom(ctx, gs.GovDenom) for _, d := range gs.Deposits { k.SetDeposit(ctx, d) diff --git a/x/cdp/integration_test.go b/x/cdp/integration_test.go index 0a6eafcd..7cd40fcf 100644 --- a/x/cdp/integration_test.go +++ b/x/cdp/integration_test.go @@ -39,16 +39,20 @@ func NewPricefeedGenState(asset string, price sdk.Dec) app.GenesisState { func NewCDPGenState(asset string, liquidationRatio sdk.Dec) app.GenesisState { cdpGenesis := cdp.GenesisState{ Params: cdp.Params{ - GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000)), + GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000)), + SurplusAuctionThreshold: cdp.DefaultSurplusThreshold, + DebtAuctionThreshold: cdp.DefaultDebtThreshold, CollateralParams: cdp.CollateralParams{ { - Denom: asset, - LiquidationRatio: liquidationRatio, - DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000)), - StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), // %5 apr - Prefix: 0x20, - ConversionFactor: i(6), - MarketID: asset + ":usd", + Denom: asset, + LiquidationRatio: liquidationRatio, + DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000)), + StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), // %5 apr + LiquidationPenalty: d("0.05"), + AuctionSize: i(1000000000), + Prefix: 0x20, + ConversionFactor: i(6), + MarketID: asset + ":usd", }, }, DebtParams: cdp.DebtParams{ @@ -63,6 +67,7 @@ func NewCDPGenState(asset string, liquidationRatio sdk.Dec) app.GenesisState { }, StartingCdpID: cdp.DefaultCdpStartingID, DebtDenom: cdp.DefaultDebtDenom, + GovDenom: cdp.DefaultGovDenom, CDPs: cdp.CDPs{}, PreviousBlockTime: cdp.DefaultPreviousBlockTime, } @@ -97,25 +102,31 @@ func NewPricefeedGenStateMulti() app.GenesisState { func NewCDPGenStateMulti() app.GenesisState { cdpGenesis := cdp.GenesisState{ Params: cdp.Params{ - GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000), sdk.NewInt64Coin("susd", 1000000000000)), + GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000), sdk.NewInt64Coin("susd", 1000000000000)), + SurplusAuctionThreshold: cdp.DefaultSurplusThreshold, + DebtAuctionThreshold: cdp.DefaultDebtThreshold, CollateralParams: cdp.CollateralParams{ { - Denom: "xrp", - LiquidationRatio: sdk.MustNewDecFromStr("2.0"), - DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 500000000000), sdk.NewInt64Coin("susd", 500000000000)), - StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), // %5 apr - Prefix: 0x20, - MarketID: "xrp:usd", - ConversionFactor: i(6), + Denom: "xrp", + LiquidationRatio: sdk.MustNewDecFromStr("2.0"), + DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 500000000000), sdk.NewInt64Coin("susd", 500000000000)), + StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), // %5 apr + LiquidationPenalty: d("0.05"), + AuctionSize: i(7000000000), + Prefix: 0x20, + MarketID: "xrp:usd", + ConversionFactor: i(6), }, { - Denom: "btc", - LiquidationRatio: sdk.MustNewDecFromStr("1.5"), - DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 500000000000), sdk.NewInt64Coin("susd", 500000000000)), - StabilityFee: sdk.MustNewDecFromStr("1.000000000782997609"), // %2.5 apr - Prefix: 0x21, - MarketID: "btc:usd", - ConversionFactor: i(8), + Denom: "btc", + LiquidationRatio: sdk.MustNewDecFromStr("1.5"), + DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 500000000000), sdk.NewInt64Coin("susd", 500000000000)), + StabilityFee: sdk.MustNewDecFromStr("1.000000000782997609"), // %2.5 apr + LiquidationPenalty: d("0.025"), + AuctionSize: i(10000000), + Prefix: 0x21, + MarketID: "btc:usd", + ConversionFactor: i(8), }, }, DebtParams: cdp.DebtParams{ @@ -137,6 +148,7 @@ func NewCDPGenStateMulti() app.GenesisState { }, StartingCdpID: cdp.DefaultCdpStartingID, DebtDenom: cdp.DefaultDebtDenom, + GovDenom: cdp.DefaultGovDenom, CDPs: cdp.CDPs{}, PreviousBlockTime: cdp.DefaultPreviousBlockTime, } @@ -193,6 +205,15 @@ func badGenStates() []badGenState { g10 := baseGenState() g10.PreviousBlockTime = time.Time{} + g11 := baseGenState() + g11.Params.CollateralParams[0].AuctionSize = i(-10) + + g12 := baseGenState() + g12.Params.CollateralParams[0].LiquidationPenalty = d("5.0") + + g13 := baseGenState() + g13.GovDenom = "" + return []badGenState{ badGenState{Genesis: g1, Reason: "duplicate collateral denom"}, badGenState{Genesis: g2, Reason: "duplicate collateral prefix"}, @@ -204,13 +225,18 @@ func badGenStates() []badGenState { badGenState{Genesis: g8, Reason: "debt param not found in global debt limit"}, badGenState{Genesis: g9, Reason: "debt denom not set"}, badGenState{Genesis: g10, Reason: "previous block time not set"}, + badGenState{Genesis: g11, Reason: "negative auction size"}, + badGenState{Genesis: g12, Reason: "invalid liquidation penalty"}, + badGenState{Genesis: g13, Reason: "gov denom not set"}, } } func baseGenState() cdp.GenesisState { return cdp.GenesisState{ Params: cdp.Params{ - GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000), sdk.NewInt64Coin("susd", 1000000000000)), + GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000), sdk.NewInt64Coin("susd", 1000000000000)), + SurplusAuctionThreshold: cdp.DefaultSurplusThreshold, + DebtAuctionThreshold: cdp.DefaultDebtThreshold, CollateralParams: cdp.CollateralParams{ { Denom: "xrp", @@ -250,6 +276,7 @@ func baseGenState() cdp.GenesisState { }, StartingCdpID: cdp.DefaultCdpStartingID, DebtDenom: cdp.DefaultDebtDenom, + GovDenom: cdp.DefaultGovDenom, CDPs: cdp.CDPs{}, PreviousBlockTime: cdp.DefaultPreviousBlockTime, } diff --git a/x/cdp/keeper/auctions.go b/x/cdp/keeper/auctions.go new file mode 100644 index 00000000..7c8397b9 --- /dev/null +++ b/x/cdp/keeper/auctions.go @@ -0,0 +1,231 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/kava-labs/kava/x/cdp/types" +) + +const ( + // factor for setting the initial value of gov tokens to sell at debt auctions -- assuming stable token is ~1 usd, this starts the auction with a price of $0.01 KAVA + dump = 100 +) + +type partialDeposit struct { + Depositor sdk.AccAddress + Amount sdk.Coins + DebtShare sdk.Int +} + +func newPartialDeposit(depositor sdk.AccAddress, amount sdk.Coins, ds sdk.Int) partialDeposit { + return partialDeposit{ + Depositor: depositor, + Amount: amount, + DebtShare: ds, + } +} + +type partialDeposits []partialDeposit + +func (pd partialDeposits) SumCollateral() (sum sdk.Int) { + sum = sdk.ZeroInt() + for _, d := range pd { + sum = sum.Add(d.Amount[0].Amount) + } + return +} + +func (pd partialDeposits) SumDebt() (sum sdk.Int) { + sum = sdk.ZeroInt() + for _, d := range pd { + sum = sum.Add(d.DebtShare) + } + return +} + +// AuctionCollateral creates auctions from the input deposits which attempt to raise the corresponding amount of debt +func (k Keeper) AuctionCollateral(ctx sdk.Context, deposits types.Deposits, debt sdk.Int, bidDenom string) sdk.Error { + auctionSize := k.getAuctionSize(ctx, deposits[0].Amount[0].Denom) + partialAuctionDeposits := partialDeposits{} + totalCollateral := deposits.SumCollateral() + for totalCollateral.GT(sdk.ZeroInt()) { + for i, dep := range deposits { + if dep.Amount.IsZero() { + continue + } + collateralAmount := dep.Amount[0].Amount + collateralDenom := dep.Amount[0].Denom + // create auctions from individual deposits that are larger than the auction size + debtChange, collateralChange, err := k.CreateAuctionsFromDeposit(ctx, dep, debt, totalCollateral, auctionSize, bidDenom) + if err != nil { + return err + } + debt = debt.Sub(debtChange) + totalCollateral = totalCollateral.Sub(collateralChange) + dep.Amount = sdk.NewCoins(sdk.NewCoin(collateralDenom, collateralAmount.Sub(collateralChange))) + collateralAmount = collateralAmount.Sub(collateralChange) + // if there is leftover collateral that is less than a lot + if !dep.Amount.IsZero() { + // figure out how much debt this deposit accounts for + // (depositCollateral / totalCollateral) * totalDebtFromCDP + debtCoveredByDeposit := (collateralAmount.Quo(totalCollateral)).Mul(debt) + // if adding this deposit to the other partial deposits is less than a lot + if (partialAuctionDeposits.SumCollateral().Add(collateralAmount)).LT(auctionSize) { + // append the deposit to the partial deposits and zero out the deposit + pd := newPartialDeposit(dep.Depositor, dep.Amount, debtCoveredByDeposit) + partialAuctionDeposits = append(partialAuctionDeposits, pd) + dep.Amount = sdk.NewCoins(sdk.NewCoin(collateralDenom, sdk.ZeroInt())) + } else { + // if the sum of partial deposits now makes a lot + partialCollateral := sdk.NewCoins(sdk.NewCoin(collateralDenom, auctionSize.Sub(partialAuctionDeposits.SumCollateral()))) + partialAmount := partialCollateral[0].Amount + partialDebt := (partialAmount.Quo(collateralAmount)).Mul(debtCoveredByDeposit) + + // create a partial deposit from the deposit + partialDep := newPartialDeposit(dep.Depositor, partialCollateral, partialDebt) + // append it to the partial deposits + partialAuctionDeposits = append(partialAuctionDeposits, partialDep) + // create an auction from the partial deposits + debtChange, collateralChange, err := k.CreateAuctionFromPartialDeposits(ctx, partialAuctionDeposits, debt, totalCollateral, auctionSize, bidDenom) + if err != nil { + return err + } + debt = debt.Sub(debtChange) + totalCollateral = totalCollateral.Sub(collateralChange) + // reset partial deposits and update the deposit amount + partialAuctionDeposits = partialDeposits{} + dep.Amount = sdk.NewCoins(sdk.NewCoin(collateralDenom, collateralAmount.Sub(partialAmount))) + } + } + deposits[i] = dep + totalCollateral = deposits.SumCollateral() + } + } + if partialAuctionDeposits.SumCollateral().GT(sdk.ZeroInt()) { + _, _, err := k.CreateAuctionFromPartialDeposits(ctx, partialAuctionDeposits, debt, totalCollateral, partialAuctionDeposits.SumCollateral(), bidDenom) + if err != nil { + return err + } + } + return nil +} + +// CreateAuctionsFromDeposit creates auctions from the input deposit until there is less than auctionSize left on the deposit +func (k Keeper) CreateAuctionsFromDeposit(ctx sdk.Context, dep types.Deposit, debt sdk.Int, totalCollateral sdk.Int, auctionSize sdk.Int, principalDenom string) (debtChange sdk.Int, collateralChange sdk.Int, err sdk.Error) { + debtChange = sdk.ZeroInt() + collateralChange = sdk.ZeroInt() + depositAmount := dep.Amount[0].Amount + depositDenom := dep.Amount[0].Denom + for depositAmount.GTE(auctionSize) { + // figure out how much debt is covered by one lots worth of collateral + depositDebtAmount := (sdk.NewDecFromInt(auctionSize).Quo(sdk.NewDecFromInt(totalCollateral))).Mul(sdk.NewDecFromInt(debt)).RoundInt() + // start an auction for one lot, attempting to raise depositDebtAmount + _, err := k.auctionKeeper.StartCollateralAuction( + ctx, types.LiquidatorMacc, sdk.NewCoin(depositDenom, auctionSize), sdk.NewCoin(principalDenom, depositDebtAmount), []sdk.AccAddress{dep.Depositor}, + []sdk.Int{auctionSize}, sdk.NewCoin(k.GetDebtDenom(ctx), depositDebtAmount)) + if err != nil { + return sdk.ZeroInt(), sdk.ZeroInt(), err + } + depositAmount = depositAmount.Sub(auctionSize) + totalCollateral = totalCollateral.Sub(auctionSize) + debt = debt.Sub(depositDebtAmount) + // subtract one lot's worth of debt from the total debt covered by this deposit + debtChange = debtChange.Add(depositDebtAmount) + collateralChange = collateralChange.Add(auctionSize) + + } + return debtChange, collateralChange, nil +} + +// CreateAuctionFromPartialDeposits creates an auction from the input partial deposits +func (k Keeper) CreateAuctionFromPartialDeposits(ctx sdk.Context, partialDeps partialDeposits, debt sdk.Int, collateral sdk.Int, auctionSize sdk.Int, bidDenom string) (debtChange, collateralChange sdk.Int, err sdk.Error) { + + returnAddrs := []sdk.AccAddress{} + returnWeights := []sdk.Int{} + for _, pd := range partialDeps { + returnAddrs = append(returnAddrs, pd.Depositor) + returnWeights = append(returnWeights, pd.DebtShare) + } + _, err = k.auctionKeeper.StartCollateralAuction(ctx, types.LiquidatorMacc, sdk.NewCoin(partialDeps[0].Amount[0].Denom, auctionSize), sdk.NewCoin(bidDenom, partialDeps.SumDebt()), returnAddrs, returnWeights, sdk.NewCoin(k.GetDebtDenom(ctx), partialDeps.SumDebt())) + if err != nil { + return sdk.ZeroInt(), sdk.ZeroInt(), err + } + debtChange = partialDeps.SumDebt() + collateralChange = partialDeps.SumCollateral() + return debtChange, collateralChange, nil +} + +// NetSurplusAndDebt burns surplus and debt coins equal to the minimum of surplus and debt balances held by the liquidator module account +// for example, if there is 1000 debt and 100 surplus, 100 surplus and 100 debt are burned, netting to 900 debt +func (k Keeper) NetSurplusAndDebt(ctx sdk.Context) sdk.Error { + totalSurplus := k.GetTotalSurplus(ctx, types.LiquidatorMacc) + debt := k.GetTotalDebt(ctx, types.LiquidatorMacc) + netAmount := sdk.MinInt(totalSurplus, debt) + if netAmount.IsZero() { + return nil + } + err := k.supplyKeeper.BurnCoins(ctx, types.LiquidatorMacc, sdk.NewCoins(sdk.NewCoin(k.GetDebtDenom(ctx), netAmount))) + if err != nil { + return err + } + for netAmount.GT(sdk.ZeroInt()) { + for _, dp := range k.GetParams(ctx).DebtParams { + balance := k.supplyKeeper.GetModuleAccount(ctx, types.LiquidatorMacc).GetCoins().AmountOf(dp.Denom) + if balance.LT(netAmount) { + err = k.supplyKeeper.BurnCoins(ctx, types.LiquidatorMacc, sdk.NewCoins(sdk.NewCoin(dp.Denom, balance))) + if err != nil { + return err + } + netAmount = netAmount.Sub(balance) + } else { + err = k.supplyKeeper.BurnCoins(ctx, types.LiquidatorMacc, sdk.NewCoins(sdk.NewCoin(dp.Denom, netAmount))) + if err != nil { + return err + } + netAmount = sdk.ZeroInt() + } + } + } + return nil +} + +// GetTotalSurplus returns the total amount of surplus tokens held by the liquidator module account +func (k Keeper) GetTotalSurplus(ctx sdk.Context, accountName string) sdk.Int { + acc := k.supplyKeeper.GetModuleAccount(ctx, accountName) + totalSurplus := sdk.ZeroInt() + for _, dp := range k.GetParams(ctx).DebtParams { + surplus := acc.GetCoins().AmountOf(dp.Denom) + totalSurplus = totalSurplus.Add(surplus) + } + return totalSurplus +} + +// GetTotalDebt returns the total amount of debt tokens held by the liquidator module account +func (k Keeper) GetTotalDebt(ctx sdk.Context, accountName string) sdk.Int { + acc := k.supplyKeeper.GetModuleAccount(ctx, accountName) + debt := acc.GetCoins().AmountOf(k.GetDebtDenom(ctx)) + return debt +} + +// RunSurplusAndDebtAuctions nets the surplus and debt balances and then creates surplus or debt auctions if the remaining balance is above the auction threshold parameter +func (k Keeper) RunSurplusAndDebtAuctions(ctx sdk.Context) sdk.Error { + k.NetSurplusAndDebt(ctx) + remainingDebt := k.GetTotalDebt(ctx, types.LiquidatorMacc) + params := k.GetParams(ctx) + if remainingDebt.GTE(params.DebtAuctionThreshold) { + _, err := k.auctionKeeper.StartDebtAuction(ctx, types.LiquidatorMacc, sdk.NewCoin("usdx", remainingDebt), sdk.NewCoin(k.GetGovDenom(ctx), remainingDebt.Mul(sdk.NewInt(dump))), sdk.NewCoin(k.GetDebtDenom(ctx), remainingDebt)) + if err != nil { + return err + } + } + remainingSurplus := k.GetTotalSurplus(ctx, types.LiquidatorMacc) + if remainingSurplus.GTE(params.SurplusAuctionThreshold) { + for _, dp := range params.DebtParams { + surplusLot := k.supplyKeeper.GetModuleAccount(ctx, types.LiquidatorMacc).GetCoins().AmountOf(dp.Denom) + _, err := k.auctionKeeper.StartSurplusAuction(ctx, types.LiquidatorMacc, sdk.NewCoin(dp.Denom, surplusLot), k.GetGovDenom(ctx)) + if err != nil { + return err + } + } + } + return nil +} diff --git a/x/cdp/keeper/auctions_test.go b/x/cdp/keeper/auctions_test.go new file mode 100644 index 00000000..bfa6b94a --- /dev/null +++ b/x/cdp/keeper/auctions_test.go @@ -0,0 +1,76 @@ +package keeper_test + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/auction" + "github.com/kava-labs/kava/x/cdp/keeper" + "github.com/kava-labs/kava/x/cdp/types" + + "github.com/stretchr/testify/suite" + abci "github.com/tendermint/tendermint/abci/types" + tmtime "github.com/tendermint/tendermint/types/time" +) + +type AuctionTestSuite struct { + suite.Suite + + keeper keeper.Keeper + app app.TestApp + ctx sdk.Context +} + +func (suite *AuctionTestSuite) SetupTest() { + config := sdk.GetConfig() + app.SetBech32AddressPrefixes(config) + tApp := app.NewTestApp() + ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()}) + tApp.InitializeFromGenesisStates( + NewPricefeedGenStateMulti(), + NewCDPGenStateMulti(), + ) + keeper := tApp.GetCDPKeeper() + suite.app = tApp + suite.ctx = ctx + suite.keeper = keeper + return +} + +func (suite *AuctionTestSuite) TestNetDebtSurplus() { + sk := suite.app.GetSupplyKeeper() + err := sk.MintCoins(suite.ctx, types.LiquidatorMacc, cs(c("debt", 100))) + suite.NoError(err) + err = sk.MintCoins(suite.ctx, types.LiquidatorMacc, cs(c("usdx", 10))) + suite.NoError(err) + suite.NotPanics(func() { suite.keeper.NetSurplusAndDebt(suite.ctx) }) + acc := sk.GetModuleAccount(suite.ctx, types.LiquidatorMacc) + suite.Equal(cs(c("debt", 90)), acc.GetCoins()) +} + +func (suite *AuctionTestSuite) TestSurplusAuction() { + sk := suite.app.GetSupplyKeeper() + err := sk.MintCoins(suite.ctx, types.LiquidatorMacc, cs(c("usdx", 10000))) + suite.NoError(err) + err = sk.MintCoins(suite.ctx, types.LiquidatorMacc, cs(c("debt", 1000))) + suite.NoError(err) + suite.keeper.RunSurplusAndDebtAuctions(suite.ctx) + acc := sk.GetModuleAccount(suite.ctx, auction.ModuleName) + suite.Equal(cs(c("usdx", 9000)), acc.GetCoins()) +} + +func (suite *AuctionTestSuite) TestDebtAuction() { + sk := suite.app.GetSupplyKeeper() + err := sk.MintCoins(suite.ctx, types.LiquidatorMacc, cs(c("usdx", 1000))) + suite.NoError(err) + err = sk.MintCoins(suite.ctx, types.LiquidatorMacc, cs(c("debt", 10000))) + suite.NoError(err) + suite.keeper.RunSurplusAndDebtAuctions(suite.ctx) + acc := sk.GetModuleAccount(suite.ctx, auction.ModuleName) + suite.Equal(cs(c("debt", 9000)), acc.GetCoins()) +} + +func TestAuctionTestSuite(t *testing.T) { + suite.Run(t, new(AuctionTestSuite)) +} diff --git a/x/cdp/keeper/cdp.go b/x/cdp/keeper/cdp.go index af4f4808..335cae6a 100644 --- a/x/cdp/keeper/cdp.go +++ b/x/cdp/keeper/cdp.go @@ -303,6 +303,14 @@ func (k Keeper) GetDebtDenom(ctx sdk.Context) (denom string) { return } +// GetGovDenom returns the denom of debt in the system +func (k Keeper) GetGovDenom(ctx sdk.Context) (denom string) { + store := prefix.NewStore(ctx.KVStore(k.key), types.DebtDenomKey) + bz := store.Get([]byte{}) + k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &denom) + return +} + // SetDebtDenom set the denom of debt in the system func (k Keeper) SetDebtDenom(ctx sdk.Context, denom string) { if denom == "" { @@ -313,6 +321,16 @@ func (k Keeper) SetDebtDenom(ctx sdk.Context, denom string) { return } +// SetGovDenom set the denom of the governance token in the system +func (k Keeper) SetGovDenom(ctx sdk.Context, denom string) { + if denom == "" { + panic("gov denom not set in genesis") + } + store := prefix.NewStore(ctx.KVStore(k.key), types.GovDenomKey) + store.Set([]byte{}, k.cdc.MustMarshalBinaryLengthPrefixed(denom)) + return +} + // ValidateCollateral validates that a collateral is valid for use in cdps func (k Keeper) ValidateCollateral(ctx sdk.Context, collateral sdk.Coins) sdk.Error { if len(collateral) != 1 { diff --git a/x/cdp/keeper/deposit.go b/x/cdp/keeper/deposit.go index 7bfa2293..35258c9b 100644 --- a/x/cdp/keeper/deposit.go +++ b/x/cdp/keeper/deposit.go @@ -18,13 +18,8 @@ func (k Keeper) DepositCollateral(ctx sdk.Context, owner sdk.AccAddress, deposit if !found { return types.ErrCdpNotFound(k.codespace, owner, collateral[0].Denom) } - // deposits blocked if cdp is in liquidation, have to check all deposits - err = k.ValidateAvailableCDP(ctx, cdp.ID) - if err != nil { - return err - } - deposit, found := k.GetDeposit(ctx, types.StatusNil, cdp.ID, depositor) + deposit, found := k.GetDeposit(ctx, cdp.ID, depositor) if found { deposit.Amount = deposit.Amount.Add(collateral) } else { @@ -67,12 +62,7 @@ func (k Keeper) WithdrawCollateral(ctx sdk.Context, owner sdk.AccAddress, deposi if !found { return types.ErrCdpNotFound(k.codespace, owner, collateral[0].Denom) } - // withdrawals blocked if cdp is in liquidation - err = k.ValidateAvailableCDP(ctx, cdp.ID) - if err != nil { - return err - } - deposit, found := k.GetDeposit(ctx, types.StatusNil, cdp.ID, depositor) + deposit, found := k.GetDeposit(ctx, cdp.ID, depositor) if !found { return types.ErrDepositNotFound(k.codespace, depositor, cdp.ID) } @@ -113,28 +103,17 @@ func (k Keeper) WithdrawCollateral(ctx sdk.Context, owner sdk.AccAddress, deposi deposit.Amount = deposit.Amount.Sub(collateral) if deposit.Amount.IsZero() { - k.DeleteDeposit(ctx, types.StatusNil, deposit.CdpID, deposit.Depositor) + k.DeleteDeposit(ctx, deposit.CdpID, deposit.Depositor) } else { k.SetDeposit(ctx, deposit) } return nil } -// ValidateAvailableCDP validates that the deposits of a cdp are not in liquidation -func (k Keeper) ValidateAvailableCDP(ctx sdk.Context, cdpID uint64) sdk.Error { - deposits := k.GetDeposits(ctx, cdpID) - for _, d := range deposits { - if d.InLiquidation { - return types.ErrCdpNotAvailable(k.codespace, cdpID) - } - } - return nil -} - // GetDeposit returns the deposit of a depositor on a particular cdp from the store -func (k Keeper) GetDeposit(ctx sdk.Context, status types.DepositStatus, cdpID uint64, depositor sdk.AccAddress) (deposit types.Deposit, found bool) { +func (k Keeper) GetDeposit(ctx sdk.Context, cdpID uint64, depositor sdk.AccAddress) (deposit types.Deposit, found bool) { store := prefix.NewStore(ctx.KVStore(k.key), types.DepositKeyPrefix) - bz := store.Get(types.DepositKey(status, cdpID, depositor)) + bz := store.Get(types.DepositKey(cdpID, depositor)) if bz == nil { return deposit, false } @@ -147,35 +126,20 @@ func (k Keeper) GetDeposit(ctx sdk.Context, status types.DepositStatus, cdpID ui func (k Keeper) SetDeposit(ctx sdk.Context, deposit types.Deposit) { store := prefix.NewStore(ctx.KVStore(k.key), types.DepositKeyPrefix) bz := k.cdc.MustMarshalBinaryLengthPrefixed(deposit) - if deposit.InLiquidation { - store.Set(types.DepositKey(types.StatusLiquidated, deposit.CdpID, deposit.Depositor), bz) - return - } - store.Set(types.DepositKey(types.StatusNil, deposit.CdpID, deposit.Depositor), bz) + store.Set(types.DepositKey(deposit.CdpID, deposit.Depositor), bz) } // DeleteDeposit deletes a deposit from the store -func (k Keeper) DeleteDeposit(ctx sdk.Context, status types.DepositStatus, cdpID uint64, depositor sdk.AccAddress) { +func (k Keeper) DeleteDeposit(ctx sdk.Context, cdpID uint64, depositor sdk.AccAddress) { store := prefix.NewStore(ctx.KVStore(k.key), types.DepositKeyPrefix) - store.Delete(types.DepositKey(status, cdpID, depositor)) + store.Delete(types.DepositKey(cdpID, depositor)) } // IterateDeposits iterates over the all the deposits of a cdp and performs a callback function func (k Keeper) IterateDeposits(ctx sdk.Context, cdpID uint64, cb func(deposit types.Deposit) (stop bool)) { store := prefix.NewStore(ctx.KVStore(k.key), types.DepositKeyPrefix) - iterator := sdk.KVStorePrefixIterator(store, types.DepositIterKey(types.StatusNil, cdpID)) - - defer iterator.Close() - for ; iterator.Valid(); iterator.Next() { - var deposit types.Deposit - k.cdc.MustUnmarshalBinaryLengthPrefixed(iterator.Value(), &deposit) - - if cb(deposit) { - break - } - } - iterator = sdk.KVStorePrefixIterator(store, types.DepositIterKey(types.StatusLiquidated, cdpID)) + iterator := sdk.KVStorePrefixIterator(store, types.GetCdpIDBytes(cdpID)) defer iterator.Close() for ; iterator.Valid(); iterator.Next() { @@ -196,28 +160,3 @@ func (k Keeper) GetDeposits(ctx sdk.Context, cdpID uint64) (deposits types.Depos }) return } - -// IterateLiquidatedDeposits iterates over the all liquidated deposits performs a callback function -func (k Keeper) IterateLiquidatedDeposits(ctx sdk.Context, cb func(deposit types.Deposit) (stop bool)) { - store := prefix.NewStore(ctx.KVStore(k.key), types.DepositKeyPrefix) - iterator := sdk.KVStorePrefixIterator(store, []byte{types.StatusLiquidated.AsByte()}) - - defer iterator.Close() - for ; iterator.Valid(); iterator.Next() { - var deposit types.Deposit - k.cdc.MustUnmarshalBinaryLengthPrefixed(iterator.Value(), &deposit) - - if cb(deposit) { - break - } - } -} - -// GetAllLiquidatedDeposits returns all deposits with status liquidated -func (k Keeper) GetAllLiquidatedDeposits(ctx sdk.Context) (deposits types.Deposits) { - k.IterateLiquidatedDeposits(ctx, func(deposit types.Deposit) bool { - deposits = append(deposits, deposit) - return false - }) - return -} diff --git a/x/cdp/keeper/deposit_test.go b/x/cdp/keeper/deposit_test.go index 8f7cb316..fab85c8a 100644 --- a/x/cdp/keeper/deposit_test.go +++ b/x/cdp/keeper/deposit_test.go @@ -45,15 +45,15 @@ func (suite *DepositTestSuite) SetupTest() { } func (suite *DepositTestSuite) TestGetSetDeposit() { - d, found := suite.keeper.GetDeposit(suite.ctx, types.StatusNil, uint64(1), suite.addrs[0]) + d, found := suite.keeper.GetDeposit(suite.ctx, uint64(1), suite.addrs[0]) suite.True(found) td := types.NewDeposit(uint64(1), suite.addrs[0], cs(c("xrp", 400000000))) suite.True(d.Equals(td)) ds := suite.keeper.GetDeposits(suite.ctx, uint64(1)) suite.Equal(1, len(ds)) suite.True(ds[0].Equals(td)) - suite.keeper.DeleteDeposit(suite.ctx, types.StatusNil, uint64(1), suite.addrs[0]) - _, found = suite.keeper.GetDeposit(suite.ctx, types.StatusNil, uint64(1), suite.addrs[0]) + suite.keeper.DeleteDeposit(suite.ctx, uint64(1), suite.addrs[0]) + _, found = suite.keeper.GetDeposit(suite.ctx, uint64(1), suite.addrs[0]) suite.False(found) ds = suite.keeper.GetDeposits(suite.ctx, uint64(1)) suite.Equal(0, len(ds)) @@ -62,7 +62,7 @@ func (suite *DepositTestSuite) TestGetSetDeposit() { func (suite *DepositTestSuite) TestDepositCollateral() { err := suite.keeper.DepositCollateral(suite.ctx, suite.addrs[0], suite.addrs[0], cs(c("xrp", 10000000))) suite.NoError(err) - d, found := suite.keeper.GetDeposit(suite.ctx, types.StatusNil, uint64(1), suite.addrs[0]) + d, found := suite.keeper.GetDeposit(suite.ctx, uint64(1), suite.addrs[0]) suite.True(found) td := types.NewDeposit(uint64(1), suite.addrs[0], cs(c("xrp", 410000000))) suite.True(d.Equals(td)) @@ -83,7 +83,7 @@ func (suite *DepositTestSuite) TestDepositCollateral() { err = suite.keeper.DepositCollateral(suite.ctx, suite.addrs[0], suite.addrs[1], cs(c("xrp", 10000000))) suite.NoError(err) - d, found = suite.keeper.GetDeposit(suite.ctx, types.StatusNil, uint64(1), suite.addrs[1]) + d, found = suite.keeper.GetDeposit(suite.ctx, uint64(1), suite.addrs[1]) suite.True(found) td = types.NewDeposit(uint64(1), suite.addrs[1], cs(c("xrp", 10000000))) suite.True(d.Equals(td)) @@ -98,24 +98,6 @@ func (suite *DepositTestSuite) TestWithdrawCollateral() { err = suite.keeper.WithdrawCollateral(suite.ctx, suite.addrs[1], suite.addrs[0], cs(c("xrp", 10000000))) suite.Equal(types.CodeCdpNotFound, err.Result().Code) - d, _ := suite.keeper.GetDeposit(suite.ctx, types.StatusNil, uint64(1), suite.addrs[0]) - d.InLiquidation = true - suite.keeper.DeleteDeposit(suite.ctx, types.StatusNil, uint64(1), suite.addrs[0]) - suite.keeper.SetDeposit(suite.ctx, d) - _, f := suite.keeper.GetDeposit(suite.ctx, types.StatusNil, uint64(1), suite.addrs[0]) - suite.False(f) - - err = suite.keeper.WithdrawCollateral(suite.ctx, suite.addrs[0], suite.addrs[0], cs(c("xrp", 10000000))) - suite.Equal(types.CodeCdpNotAvailable, err.Result().Code) - - d, f = suite.keeper.GetDeposit(suite.ctx, types.StatusLiquidated, uint64(1), suite.addrs[0]) - suite.True(f) - suite.keeper.DeleteDeposit(suite.ctx, types.StatusLiquidated, uint64(1), suite.addrs[0]) - d.InLiquidation = false - suite.keeper.SetDeposit(suite.ctx, d) - _, f = suite.keeper.GetDeposit(suite.ctx, types.StatusLiquidated, uint64(1), suite.addrs[0]) - suite.False(f) - cd, _ := suite.keeper.GetCDP(suite.ctx, "xrp", uint64(1)) cd.AccumulatedFees = cs(c("usdx", 1)) suite.keeper.SetCDP(suite.ctx, cd) @@ -124,9 +106,9 @@ func (suite *DepositTestSuite) TestWithdrawCollateral() { err = suite.keeper.WithdrawCollateral(suite.ctx, suite.addrs[0], suite.addrs[0], cs(c("xrp", 10000000))) suite.NoError(err) - d, _ = suite.keeper.GetDeposit(suite.ctx, types.StatusNil, uint64(1), suite.addrs[0]) + dep, _ := suite.keeper.GetDeposit(suite.ctx, uint64(1), suite.addrs[0]) td := types.NewDeposit(uint64(1), suite.addrs[0], cs(c("xrp", 390000000))) - suite.True(d.Equals(td)) + suite.True(dep.Equals(td)) ak := suite.app.GetAccountKeeper() acc := ak.GetAccount(suite.ctx, suite.addrs[0]) suite.Equal(i(110000000), acc.GetCoins().AmountOf("xrp")) @@ -135,20 +117,6 @@ func (suite *DepositTestSuite) TestWithdrawCollateral() { suite.Equal(types.CodeDepositNotFound, err.Result().Code) } -func (suite *DepositTestSuite) TestIterateLiquidatedDeposits() { - for j := 0; j < 10; j++ { - d := types.NewDeposit(uint64(j+2), suite.addrs[j], cs(c("xrp", 1000000))) - if j%2 == 0 { - d.InLiquidation = true - } - suite.keeper.SetDeposit(suite.ctx, d) - } - ds := suite.keeper.GetAllLiquidatedDeposits(suite.ctx) - for _, d := range ds { - suite.True(d.InLiquidation) - } - suite.Equal(5, len(ds)) -} func TestDepositTestSuite(t *testing.T) { suite.Run(t, new(DepositTestSuite)) } diff --git a/x/cdp/keeper/draw.go b/x/cdp/keeper/draw.go index 4571edf4..1bc1ac66 100644 --- a/x/cdp/keeper/draw.go +++ b/x/cdp/keeper/draw.go @@ -14,11 +14,7 @@ func (k Keeper) AddPrincipal(ctx sdk.Context, owner sdk.AccAddress, denom string if !found { return types.ErrCdpNotFound(k.codespace, owner, denom) } - err := k.ValidateAvailableCDP(ctx, cdp.ID) - if err != nil { - return err - } - err = k.ValidatePrincipalDraw(ctx, principal) + err := k.ValidatePrincipalDraw(ctx, principal) if err != nil { return err } @@ -84,11 +80,7 @@ func (k Keeper) RepayPrincipal(ctx sdk.Context, owner sdk.AccAddress, denom stri if !found { return types.ErrCdpNotFound(k.codespace, owner, denom) } - err := k.ValidateAvailableCDP(ctx, cdp.ID) - if err != nil { - return err - } - err = k.ValidatePaymentCoins(ctx, cdp, payment) + err := k.ValidatePaymentCoins(ctx, cdp, payment) if err != nil { return err } @@ -189,7 +181,7 @@ func (k Keeper) ReturnCollateral(ctx sdk.Context, cdp types.CDP) { if err != nil { panic(err) } - k.DeleteDeposit(ctx, types.StatusNil, cdp.ID, deposit.Depositor) + k.DeleteDeposit(ctx, cdp.ID, deposit.Depositor) } } diff --git a/x/cdp/keeper/integration_test.go b/x/cdp/keeper/integration_test.go index fd8997f2..78e1c980 100644 --- a/x/cdp/keeper/integration_test.go +++ b/x/cdp/keeper/integration_test.go @@ -39,16 +39,20 @@ func NewPricefeedGenState(asset string, price sdk.Dec) app.GenesisState { func NewCDPGenState(asset string, liquidationRatio sdk.Dec) app.GenesisState { cdpGenesis := cdp.GenesisState{ Params: cdp.Params{ - GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000)), + GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000)), + SurplusAuctionThreshold: cdp.DefaultSurplusThreshold, + DebtAuctionThreshold: cdp.DefaultDebtThreshold, CollateralParams: cdp.CollateralParams{ { - Denom: asset, - LiquidationRatio: liquidationRatio, - DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000)), - StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), // %5 apr - Prefix: 0x20, - ConversionFactor: i(6), - MarketID: asset + ":usd", + Denom: asset, + LiquidationRatio: liquidationRatio, + DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000)), + StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), // %5 apr + LiquidationPenalty: d("0.05"), + AuctionSize: i(100), + Prefix: 0x20, + ConversionFactor: i(6), + MarketID: asset + ":usd", }, }, DebtParams: cdp.DebtParams{ @@ -63,6 +67,7 @@ func NewCDPGenState(asset string, liquidationRatio sdk.Dec) app.GenesisState { }, StartingCdpID: cdp.DefaultCdpStartingID, DebtDenom: cdp.DefaultDebtDenom, + GovDenom: cdp.DefaultGovDenom, CDPs: cdp.CDPs{}, PreviousBlockTime: cdp.DefaultPreviousBlockTime, } @@ -97,25 +102,31 @@ func NewPricefeedGenStateMulti() app.GenesisState { func NewCDPGenStateMulti() app.GenesisState { cdpGenesis := cdp.GenesisState{ Params: cdp.Params{ - GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000), sdk.NewInt64Coin("susd", 1000000000000)), + GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000), sdk.NewInt64Coin("susd", 1000000000000)), + SurplusAuctionThreshold: cdp.DefaultSurplusThreshold, + DebtAuctionThreshold: cdp.DefaultDebtThreshold, CollateralParams: cdp.CollateralParams{ { - Denom: "xrp", - LiquidationRatio: sdk.MustNewDecFromStr("2.0"), - DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 500000000000), sdk.NewInt64Coin("susd", 500000000000)), - StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), // %5 apr - Prefix: 0x20, - MarketID: "xrp:usd", - ConversionFactor: i(6), + Denom: "xrp", + LiquidationRatio: sdk.MustNewDecFromStr("2.0"), + DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 500000000000), sdk.NewInt64Coin("susd", 500000000000)), + StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), // %5 apr + LiquidationPenalty: d("0.05"), + AuctionSize: i(7000000000), + Prefix: 0x20, + MarketID: "xrp:usd", + ConversionFactor: i(6), }, { - Denom: "btc", - LiquidationRatio: sdk.MustNewDecFromStr("1.5"), - DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 500000000000), sdk.NewInt64Coin("susd", 500000000000)), - StabilityFee: sdk.MustNewDecFromStr("1.000000000782997609"), // %2.5 apr - Prefix: 0x21, - MarketID: "btc:usd", - ConversionFactor: i(8), + Denom: "btc", + LiquidationRatio: sdk.MustNewDecFromStr("1.5"), + DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 500000000000), sdk.NewInt64Coin("susd", 500000000000)), + StabilityFee: sdk.MustNewDecFromStr("1.000000000782997609"), // %2.5 apr + LiquidationPenalty: d("0.025"), + AuctionSize: i(10000000), + Prefix: 0x21, + MarketID: "btc:usd", + ConversionFactor: i(8), }, }, DebtParams: cdp.DebtParams{ @@ -137,6 +148,7 @@ func NewCDPGenStateMulti() app.GenesisState { }, StartingCdpID: cdp.DefaultCdpStartingID, DebtDenom: cdp.DefaultDebtDenom, + GovDenom: cdp.DefaultGovDenom, CDPs: cdp.CDPs{}, PreviousBlockTime: cdp.DefaultPreviousBlockTime, } diff --git a/x/cdp/keeper/keeper.go b/x/cdp/keeper/keeper.go index 90c3410e..94570d2b 100644 --- a/x/cdp/keeper/keeper.go +++ b/x/cdp/keeper/keeper.go @@ -17,22 +17,29 @@ type Keeper struct { paramSubspace subspace.Subspace pricefeedKeeper types.PricefeedKeeper supplyKeeper types.SupplyKeeper + auctionKeeper types.AuctionKeeper codespace sdk.CodespaceType } // NewKeeper creates a new keeper -func NewKeeper(cdc *codec.Codec, key sdk.StoreKey, paramstore subspace.Subspace, pfk types.PricefeedKeeper, sk types.SupplyKeeper, codespace sdk.CodespaceType) Keeper { +func NewKeeper(cdc *codec.Codec, key sdk.StoreKey, paramstore subspace.Subspace, pfk types.PricefeedKeeper, ak types.AuctionKeeper, sk types.SupplyKeeper, codespace sdk.CodespaceType) Keeper { - // ensure module account is set + // ensure cdp module account is set if addr := sk.GetModuleAddress(types.ModuleName); addr == nil { panic(fmt.Sprintf("%s module account has not been set", types.ModuleName)) } + // ensure liquidator module account is set + if addr := sk.GetModuleAddress(types.LiquidatorMacc); addr == nil { + panic(fmt.Sprintf("%s module account has not been set", types.LiquidatorMacc)) + } + return Keeper{ key: key, cdc: cdc, paramSubspace: paramstore.WithKeyTable(types.ParamKeyTable()), pricefeedKeeper: pfk, + auctionKeeper: ak, supplyKeeper: sk, codespace: codespace, } diff --git a/x/cdp/keeper/params.go b/x/cdp/keeper/params.go index d7d34156..337af5a3 100644 --- a/x/cdp/keeper/params.go +++ b/x/cdp/keeper/params.go @@ -52,8 +52,7 @@ func (k Keeper) GetDenomPrefix(ctx sdk.Context, denom string) (byte, bool) { return 0x00, false } -// private methods panic if the input is invalid - +// private methods assume collateral has been validated, panic if the input is invalid func (k Keeper) getDenomFromByte(ctx sdk.Context, db byte) string { params := k.GetParams(ctx) for _, cp := range params.CollateralParams { @@ -79,3 +78,19 @@ func (k Keeper) getLiquidationRatio(ctx sdk.Context, denom string) sdk.Dec { } return cp.LiquidationRatio } + +func (k Keeper) getLiquidationPenalty(ctx sdk.Context, denom string) sdk.Dec { + cp, found := k.GetCollateral(ctx, denom) + if !found { + panic(fmt.Sprintf("collateral not found: %s", denom)) + } + return cp.LiquidationPenalty +} + +func (k Keeper) getAuctionSize(ctx sdk.Context, denom string) sdk.Int { + cp, found := k.GetCollateral(ctx, denom) + if !found { + panic(fmt.Sprintf("collateral not found: %s", denom)) + } + return cp.AuctionSize +} diff --git a/x/cdp/keeper/seize.go b/x/cdp/keeper/seize.go index 09598ecd..fcb15756 100644 --- a/x/cdp/keeper/seize.go +++ b/x/cdp/keeper/seize.go @@ -15,50 +15,51 @@ import ( // 4. decrements the total amount of principal outstanding for that collateral type // (this is the equivalent of saying that fees are no longer accumulated by a cdp once it // gets liquidated) -func (k Keeper) SeizeCollateral(ctx sdk.Context, cdp types.CDP) { +func (k Keeper) SeizeCollateral(ctx sdk.Context, cdp types.CDP) sdk.Error { + // Calculate the previous collateral ratio + oldCollateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees)) // Update fees periods := sdk.NewInt(ctx.BlockTime().Unix()).Sub(sdk.NewInt(cdp.FeesUpdated.Unix())) fees := k.CalculateFees(ctx, cdp.Principal.Add(cdp.AccumulatedFees), periods, cdp.Collateral[0].Denom) cdp.AccumulatedFees = cdp.AccumulatedFees.Add(fees) cdp.FeesUpdated = ctx.BlockTime() - // Liquidate deposits + // TODO implement liquidation penalty + + // Move debt coins from cdp to liquidator account deposits := k.GetDeposits(ctx, cdp.ID) - for _, dep := range deposits { - if !dep.InLiquidation { - dep.InLiquidation = true - - ctx.EventManager().EmitEvent( - sdk.NewEvent( - types.EventTypeCdpLiquidation, - sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), - sdk.NewAttribute(types.AttributeKeyCdpID, fmt.Sprintf("%d", cdp.ID)), - sdk.NewAttribute(types.AttributeKeyDepositor, fmt.Sprintf("%s", dep.Depositor)), - ), - ) - k.DeleteDeposit(ctx, types.StatusNil, cdp.ID, dep.Depositor) - k.SetDeposit(ctx, dep) - err := k.supplyKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, types.LiquidatorMacc, dep.Amount) - if err != nil { - panic(err) - } - } else { - return - } - } - - // Transfer debt coins from cdp module account to liquidator module account - debtAmt := sdk.ZeroInt() - for _, dc := range cdp.Principal { - debtAmt = debtAmt.Add(dc.Amount) + debt := sdk.ZeroInt() + for _, pc := range cdp.Principal { + debt = debt.Add(pc.Amount) } for _, dc := range cdp.AccumulatedFees { - debtAmt = debtAmt.Add(dc.Amount) + debt = debt.Add(dc.Amount) } - debtCoins := sdk.NewCoins(sdk.NewCoin(k.GetDebtDenom(ctx), debtAmt)) - err := k.supplyKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, types.LiquidatorMacc, debtCoins) + debtCoin := sdk.NewCoin(k.GetDebtDenom(ctx), debt) + err := k.supplyKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, types.LiquidatorMacc, sdk.NewCoins(debtCoin)) if err != nil { - panic(err) + return err + } + + // liquidate deposits and send collateral from cdp to liquidator + for _, dep := range deposits { + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeCdpLiquidation, + sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), + sdk.NewAttribute(types.AttributeKeyCdpID, fmt.Sprintf("%d", cdp.ID)), + sdk.NewAttribute(types.AttributeKeyDepositor, fmt.Sprintf("%s", dep.Depositor)), + ), + ) + err := k.supplyKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, types.LiquidatorMacc, dep.Amount) + if err != nil { + return err + } + k.DeleteDeposit(ctx, dep.CdpID, dep.Depositor) + } + err = k.AuctionCollateral(ctx, deposits, debt, cdp.Principal[0].Denom) + if err != nil { + return err } // Decrement total principal for this collateral type @@ -71,6 +72,10 @@ func (k Keeper) SeizeCollateral(ctx sdk.Context, cdp types.CDP) { } k.DecrementTotalPrincipal(ctx, cdp.Collateral[0].Denom, coinsToDecrement) } + k.RemoveCdpOwnerIndex(ctx, cdp) + k.RemoveCdpCollateralRatioIndex(ctx, cdp.Collateral[0].Denom, cdp.ID, oldCollateralToDebtRatio) + k.DeleteCDP(ctx, cdp) + return nil } // HandleNewDebt compounds the accumulated fees for the input collateral and principal coins. @@ -88,15 +93,18 @@ func (k Keeper) HandleNewDebt(ctx sdk.Context, collateralDenom string, principal } // LiquidateCdps seizes collateral from all CDPs below the input liquidation ratio -func (k Keeper) LiquidateCdps(ctx sdk.Context, marketID string, denom string, liquidationRatio sdk.Dec) { +func (k Keeper) LiquidateCdps(ctx sdk.Context, marketID string, denom string, liquidationRatio sdk.Dec) sdk.Error { price, err := k.pricefeedKeeper.GetCurrentPrice(ctx, marketID) if err != nil { - return + return err } normalizedRatio := sdk.OneDec().Quo(price.Price.Quo(liquidationRatio)) cdpsToLiquidate := k.GetAllCdpsByDenomAndRatio(ctx, denom, normalizedRatio) for _, c := range cdpsToLiquidate { - k.SeizeCollateral(ctx, c) + err := k.SeizeCollateral(ctx, c) + if err != nil { + return err + } } - return + return nil } diff --git a/x/cdp/keeper/seize_test.go b/x/cdp/keeper/seize_test.go index e40dc786..0c89db6b 100644 --- a/x/cdp/keeper/seize_test.go +++ b/x/cdp/keeper/seize_test.go @@ -8,6 +8,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/simulation" "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/auction" "github.com/kava-labs/kava/x/cdp/keeper" "github.com/kava-labs/kava/x/cdp/types" "github.com/stretchr/testify/suite" @@ -55,15 +56,15 @@ func (suite *SeizeTestSuite) SetupTest() { suite.ctx = ctx suite.app = tApp suite.keeper = tApp.GetCDPKeeper() - + randSource := rand.New(rand.NewSource(int64(777))) for j := 0; j < 100; j++ { collateral := "xrp" amount := 10000000000 - debt := simulation.RandIntBetween(rand.New(rand.NewSource(int64(j))), 750000000, 1249000000) + debt := simulation.RandIntBetween(randSource, 750000000, 1249000000) if j%2 == 0 { collateral = "btc" amount = 100000000 - debt = simulation.RandIntBetween(rand.New(rand.NewSource(int64(j))), 2700000000, 5332000000) + debt = simulation.RandIntBetween(randSource, 2700000000, 5332000000) if debt >= 4000000000 { tracker.btc = append(tracker.btc, uint64(j+1)) tracker.debt += int64(debt) @@ -103,16 +104,41 @@ func (suite *SeizeTestSuite) TestSeizeCollateral() { p := cdp.Principal[0].Amount cl := cdp.Collateral[0].Amount tpb := suite.keeper.GetTotalPrincipal(suite.ctx, "xrp", "usdx") - suite.keeper.SeizeCollateral(suite.ctx, cdp) + err := suite.keeper.SeizeCollateral(suite.ctx, cdp) + suite.NoError(err) tpa := suite.keeper.GetTotalPrincipal(suite.ctx, "xrp", "usdx") suite.Equal(tpb.Sub(tpa), p) - liqModAcc := sk.GetModuleAccount(suite.ctx, types.LiquidatorMacc) - suite.Equal(cs(c("debt", p.Int64()), c("xrp", cl.Int64())), liqModAcc.GetCoins()) + auctionMacc := sk.GetModuleAccount(suite.ctx, auction.ModuleName) + suite.Equal(cs(c("debt", p.Int64()), c("xrp", cl.Int64())), auctionMacc.GetCoins()) ak := suite.app.GetAccountKeeper() acc := ak.GetAccount(suite.ctx, suite.addrs[1]) suite.Equal(p.Int64(), acc.GetCoins().AmountOf("usdx").Int64()) - err := suite.keeper.WithdrawCollateral(suite.ctx, suite.addrs[1], suite.addrs[1], cs(c("xrp", 10))) - suite.Equal(types.CodeCdpNotAvailable, err.Result().Code) + err = suite.keeper.WithdrawCollateral(suite.ctx, suite.addrs[1], suite.addrs[1], cs(c("xrp", 10))) + suite.Equal(types.CodeCdpNotFound, err.Result().Code) +} + +func (suite *SeizeTestSuite) TestSeizeCollateralMultiDeposit() { + sk := suite.app.GetSupplyKeeper() + cdp, _ := suite.keeper.GetCDP(suite.ctx, "xrp", uint64(2)) + err := suite.keeper.DepositCollateral(suite.ctx, suite.addrs[1], suite.addrs[0], cs(c("xrp", 6999000000))) + suite.NoError(err) + cdp, _ = suite.keeper.GetCDP(suite.ctx, "xrp", uint64(2)) + deposits := suite.keeper.GetDeposits(suite.ctx, cdp.ID) + suite.Equal(2, len(deposits)) + p := cdp.Principal[0].Amount + cl := cdp.Collateral[0].Amount + tpb := suite.keeper.GetTotalPrincipal(suite.ctx, "xrp", "usdx") + err = suite.keeper.SeizeCollateral(suite.ctx, cdp) + suite.NoError(err) + tpa := suite.keeper.GetTotalPrincipal(suite.ctx, "xrp", "usdx") + suite.Equal(tpb.Sub(tpa), p) + auctionMacc := sk.GetModuleAccount(suite.ctx, auction.ModuleName) + suite.Equal(cs(c("debt", p.Int64()), c("xrp", cl.Int64())), auctionMacc.GetCoins()) + ak := suite.app.GetAccountKeeper() + acc := ak.GetAccount(suite.ctx, suite.addrs[1]) + suite.Equal(p.Int64(), acc.GetCoins().AmountOf("usdx").Int64()) + err = suite.keeper.WithdrawCollateral(suite.ctx, suite.addrs[1], suite.addrs[1], cs(c("xrp", 10))) + suite.Equal(types.CodeCdpNotFound, err.Result().Code) } func (suite *SeizeTestSuite) TestLiquidateCdps() { diff --git a/x/cdp/types/deposit.go b/x/cdp/types/deposit.go index 5e6b0fae..1cafcb59 100644 --- a/x/cdp/types/deposit.go +++ b/x/cdp/types/deposit.go @@ -8,50 +8,22 @@ import ( // Deposit defines an amount of coins deposited by an account to a cdp type Deposit struct { - CdpID uint64 `json:"cdp_id" yaml:"cdp_id"` // cdpID of the cdp - Depositor sdk.AccAddress `json:"depositor" yaml:"depositor"` // Address of the depositor - Amount sdk.Coins `json:"amount" yaml:"amount"` // Deposit amount - InLiquidation bool `json:"in_liquidation" yaml:"in_liquidation"` -} - -// DepositStatus is a type alias that represents a deposit status as a byte -type DepositStatus byte - -// Valid Deposit statuses -const ( - StatusNil DepositStatus = 0x00 - StatusLiquidated DepositStatus = 0x01 -) - -// AsByte returns the status as byte -func (ds DepositStatus) AsByte() byte { - return byte(ds) -} - -// StatusFromByte returns the status from its byte representation -func StatusFromByte(b byte) DepositStatus { - switch b { - case 0x00: - return StatusNil - case 0x01: - return StatusLiquidated - default: - panic(fmt.Sprintf("unrecognized deposit status, %v", b)) - } + CdpID uint64 `json:"cdp_id" yaml:"cdp_id"` // cdpID of the cdp + Depositor sdk.AccAddress `json:"depositor" yaml:"depositor"` // Address of the depositor + Amount sdk.Coins `json:"amount" yaml:"amount"` // Deposit amount } // NewDeposit creates a new Deposit object func NewDeposit(cdpID uint64, depositor sdk.AccAddress, amount sdk.Coins) Deposit { - return Deposit{cdpID, depositor, amount, false} + return Deposit{cdpID, depositor, amount} } // String implements fmt.Stringer func (d Deposit) String() string { return fmt.Sprintf(`Deposit for CDP %d: Depositor: %s - Amount: %s - In Liquidation: %t`, - d.CdpID, d.Depositor, d.Amount, d.InLiquidation) + Amount: %s`, + d.CdpID, d.Depositor, d.Amount) } // Deposits a collection of Deposit objects @@ -65,9 +37,6 @@ func (ds Deposits) String() string { out := fmt.Sprintf("Deposits for CDP %d:", ds[0].CdpID) for _, dep := range ds { out += fmt.Sprintf("\n %s: %s", dep.Depositor, dep.Amount) - if dep.InLiquidation { - out += fmt.Sprintf("(in liquidation)") - } } return out } @@ -81,3 +50,13 @@ func (d Deposit) Equals(comp Deposit) bool { func (d Deposit) Empty() bool { return d.Equals(Deposit{}) } + +func (ds Deposits) SumCollateral() (sum sdk.Int) { + sum = sdk.ZeroInt() + for _, d := range ds { + if !d.Amount.IsZero() { + sum = sum.Add(d.Amount[0].Amount) + } + } + return +} diff --git a/x/cdp/types/events.go b/x/cdp/types/events.go index ba58c9d6..1884084b 100644 --- a/x/cdp/types/events.go +++ b/x/cdp/types/events.go @@ -2,15 +2,17 @@ package types // Event types for cdp module const ( - EventTypeCreateCdp = "create_cdp" - EventTypeCdpDeposit = "cdp_deposit" - EventTypeCdpDraw = "cdp_draw" - EventTypeCdpRepay = "cdp_repayment" - EventTypeCdpClose = "cdp_close" - EventTypeCdpWithdrawal = "cdp_withdrawal" - EventTypeCdpLiquidation = "cdp_liquidation" + EventTypeCreateCdp = "create_cdp" + EventTypeCdpDeposit = "cdp_deposit" + EventTypeCdpDraw = "cdp_draw" + EventTypeCdpRepay = "cdp_repayment" + EventTypeCdpClose = "cdp_close" + EventTypeCdpWithdrawal = "cdp_withdrawal" + EventTypeCdpLiquidation = "cdp_liquidation" + EventTypeBeginBlockerFatal = "cdp_begin_block_error" AttributeKeyCdpID = "cdp_id" AttributeKeyDepositor = "depositor" AttributeValueCategory = "cdp" + AttributeKeyError = "error_message" ) diff --git a/x/cdp/types/expected_keepers.go b/x/cdp/types/expected_keepers.go index c57ad94f..402458dc 100644 --- a/x/cdp/types/expected_keepers.go +++ b/x/cdp/types/expected_keepers.go @@ -32,3 +32,10 @@ type PricefeedKeeper interface { SetPrice(sdk.Context, sdk.AccAddress, string, sdk.Dec, time.Time) (pftypes.PostedPrice, sdk.Error) SetCurrentPrices(sdk.Context, string) sdk.Error } + +// AuctionKeeper expected interface for the auction keeper (noalias) +type AuctionKeeper interface { + StartSurplusAuction(ctx sdk.Context, seller string, lot sdk.Coin, bidDenom string) (uint64, sdk.Error) + StartDebtAuction(ctx sdk.Context, buyer string, bid sdk.Coin, initialLot sdk.Coin, debt sdk.Coin) (uint64, sdk.Error) + StartCollateralAuction(ctx sdk.Context, seller string, lot sdk.Coin, maxBid sdk.Coin, lotReturnAddrs []sdk.AccAddress, lotReturnWeights []sdk.Int, debt sdk.Coin) (uint64, sdk.Error) +} diff --git a/x/cdp/types/genesis.go b/x/cdp/types/genesis.go index 5c4ebf61..8737bce5 100644 --- a/x/cdp/types/genesis.go +++ b/x/cdp/types/genesis.go @@ -13,6 +13,7 @@ type GenesisState struct { Deposits Deposits `json:"deposits" yaml:"deposits"` StartingCdpID uint64 `json:"starting_cdp_id" yaml:"starting_cdp_id"` DebtDenom string `json:"debt_denom" yaml:"debt_denom"` + GovDenom string `json:"gov_denom" yaml:"gov_denom"` PreviousBlockTime time.Time `json:"previous_block_time" yaml:"previous_block_time"` } @@ -24,6 +25,7 @@ func DefaultGenesisState() GenesisState { Deposits: Deposits{}, StartingCdpID: DefaultCdpStartingID, DebtDenom: DefaultDebtDenom, + GovDenom: DefaultGovDenom, PreviousBlockTime: DefaultPreviousBlockTime, } } diff --git a/x/cdp/types/keys.go b/x/cdp/types/keys.go index a128e387..180ad38e 100644 --- a/x/cdp/types/keys.go +++ b/x/cdp/types/keys.go @@ -51,10 +51,11 @@ var ( CollateralRatioIndexPrefix = []byte{0x02} CdpIDKey = []byte{0x03} DebtDenomKey = []byte{0x04} - DepositKeyPrefix = []byte{0x05} - PrincipalKeyPrefix = []byte{0x06} - AccumulatorKeyPrefix = []byte{0x07} - PreviousBlockTimeKey = []byte{0x08} + GovDenomKey = []byte{0x05} + DepositKeyPrefix = []byte{0x06} + PrincipalKeyPrefix = []byte{0x07} + AccumulatorKeyPrefix = []byte{0x08} + PreviousBlockTimeKey = []byte{0x09} ) var lenPositiveDec = len(SortableDecBytes(sdk.OneDec())) @@ -95,28 +96,25 @@ func SplitDenomIterKey(key []byte) byte { } // DepositKey key of a specific deposit in the store -func DepositKey(status DepositStatus, cdpID uint64, depositor sdk.AccAddress) []byte { - return createKey([]byte{status.AsByte()}, sep, GetCdpIDBytes(cdpID), sep, depositor) +func DepositKey(cdpID uint64, depositor sdk.AccAddress) []byte { + return createKey(GetCdpIDBytes(cdpID), sep, depositor) } // SplitDepositKey returns the component parts of a deposit key -func SplitDepositKey(key []byte) (DepositStatus, uint64, sdk.AccAddress) { - status := StatusFromByte(key[0]) - cdpID := GetCdpIDFromBytes(key[2:10]) - addr := key[11:] - return status, cdpID, addr +func SplitDepositKey(key []byte) (uint64, sdk.AccAddress) { + cdpID := GetCdpIDFromBytes(key[0:8]) + addr := key[9:] + return cdpID, addr } // DepositIterKey returns the prefix key for iterating over deposits to a cdp -func DepositIterKey(status DepositStatus, cdpID uint64) []byte { - return createKey([]byte{status.AsByte()}, sep, GetCdpIDBytes(cdpID)) +func DepositIterKey(cdpID uint64) []byte { + return GetCdpIDBytes(cdpID) } // SplitDepositIterKey returns the component parts of a key for iterating over deposits on a cdp -func SplitDepositIterKey(key []byte) (status DepositStatus, cdpID uint64) { - status = StatusFromByte(key[0]) - cdpID = GetCdpIDFromBytes(key[2:]) - return status, cdpID +func SplitDepositIterKey(key []byte) (cdpID uint64) { + return GetCdpIDFromBytes(key) } // CollateralRatioBytes returns the liquidation ratio as sortable bytes diff --git a/x/cdp/types/keys_test.go b/x/cdp/types/keys_test.go index 51d4de26..935f6415 100644 --- a/x/cdp/types/keys_test.go +++ b/x/cdp/types/keys_test.go @@ -20,18 +20,16 @@ func TestKeys(t *testing.T) { db = SplitDenomIterKey(denomKey) require.Equal(t, byte(0x01), db) - depositKey := DepositKey(StatusNil, 2, addr) - status, id, a := SplitDepositKey(depositKey) + depositKey := DepositKey(2, addr) + id, a := SplitDepositKey(depositKey) require.Equal(t, 2, int(id)) require.Equal(t, a, addr) - require.Equal(t, StatusNil, status) - depositIterKey := DepositIterKey(StatusLiquidated, 2) - status, id = SplitDepositIterKey(depositIterKey) + depositIterKey := DepositIterKey(2) + id = SplitDepositIterKey(depositIterKey) require.Equal(t, 2, int(id)) - require.Equal(t, StatusLiquidated, status) - require.Panics(t, func() { SplitDepositIterKey(append([]byte{0x03}, GetCdpIDBytes(2)...)) }) + require.Panics(t, func() { SplitDepositIterKey([]byte{0x03}) }) collateralKey := CollateralRatioKey(0x01, 2, sdk.MustNewDecFromStr("1.50")) db, id, ratio := SplitCollateralRatioKey(collateralKey) diff --git a/x/cdp/types/params.go b/x/cdp/types/params.go index c0a43271..4dee6ef7 100644 --- a/x/cdp/types/params.go +++ b/x/cdp/types/params.go @@ -15,12 +15,17 @@ var ( KeyCollateralParams = []byte("CollateralParams") KeyDebtParams = []byte("DebtParams") KeyCircuitBreaker = []byte("CircuitBreaker") + KeyDebtThreshold = []byte("DebtThreshold") + KeySurplusThreshold = []byte("SurplusThreshold") DefaultGlobalDebt = sdk.Coins{} DefaultCircuitBreaker = false DefaultCollateralParams = CollateralParams{} DefaultDebtParams = DebtParams{} DefaultCdpStartingID = uint64(1) DefaultDebtDenom = "debt" + DefaultGovDenom = "ukava" + DefaultSurplusThreshold = sdk.NewInt(1000) + DefaultDebtThreshold = sdk.NewInt(1000) DefaultPreviousBlockTime = tmtime.Canonical(time.Unix(0, 0)) minCollateralPrefix = 0 maxCollateralPrefix = 255 @@ -28,10 +33,12 @@ var ( // Params governance parameters for cdp module type Params struct { - CollateralParams CollateralParams `json:"collateral_params" yaml:"collateral_params"` - DebtParams DebtParams `json:"debt_params" yaml:"debt_params"` - GlobalDebtLimit sdk.Coins `json:"global_debt_limit" yaml:"global_debt_limit"` - CircuitBreaker bool `json:"circuit_breaker" yaml:"circuit_breaker"` + CollateralParams CollateralParams `json:"collateral_params" yaml:"collateral_params"` + DebtParams DebtParams `json:"debt_params" yaml:"debt_params"` + GlobalDebtLimit sdk.Coins `json:"global_debt_limit" yaml:"global_debt_limit"` + SurplusAuctionThreshold sdk.Int `json:"surplus_auction_threshold" yaml:"surplus_auction_threshold"` + DebtAuctionThreshold sdk.Int `json:"debt_auction_threshold" yaml:"debt_auction_threshold"` + CircuitBreaker bool `json:"circuit_breaker" yaml:"circuit_breaker"` } // String implements fmt.Stringer @@ -40,35 +47,41 @@ func (p Params) String() string { Global Debt Limit: %s Collateral Params: %s Debt Params: %s + Surplus Auction Threshold: %s + Debt Auction Threshold: %s Circuit Breaker: %t`, - p.GlobalDebtLimit, p.CollateralParams, p.DebtParams, p.CircuitBreaker, + p.GlobalDebtLimit, p.CollateralParams, p.DebtParams, p.SurplusAuctionThreshold, p.DebtAuctionThreshold, p.CircuitBreaker, ) } // NewParams returns a new params object -func NewParams(debtLimit sdk.Coins, collateralParams CollateralParams, debtParams DebtParams, breaker bool) Params { +func NewParams(debtLimit sdk.Coins, collateralParams CollateralParams, debtParams DebtParams, surplusThreshold sdk.Int, debtThreshold sdk.Int, breaker bool) Params { return Params{ - GlobalDebtLimit: debtLimit, - CollateralParams: collateralParams, - DebtParams: debtParams, - CircuitBreaker: breaker, + GlobalDebtLimit: debtLimit, + CollateralParams: collateralParams, + DebtParams: debtParams, + DebtAuctionThreshold: debtThreshold, + SurplusAuctionThreshold: surplusThreshold, + CircuitBreaker: breaker, } } // DefaultParams returns default params for cdp module func DefaultParams() Params { - return NewParams(DefaultGlobalDebt, DefaultCollateralParams, DefaultDebtParams, DefaultCircuitBreaker) + return NewParams(DefaultGlobalDebt, DefaultCollateralParams, DefaultDebtParams, DefaultSurplusThreshold, DefaultDebtThreshold, DefaultCircuitBreaker) } // CollateralParam governance parameters for each collateral type within the cdp module type CollateralParam struct { - Denom string `json:"denom" yaml:"denom"` // Coin name of collateral type - LiquidationRatio sdk.Dec `json:"liquidation_ratio" yaml:"liquidation_ratio"` // The ratio (Collateral (priced in stable coin) / Debt) under which a CDP will be liquidated - DebtLimit sdk.Coins `json:"debt_limit" yaml:"debt_limit"` // Maximum amount of debt allowed to be drawn from this collateral type - StabilityFee sdk.Dec `json:"stability_fee" yaml:"stability_fee"` // per second stability fee for loans opened using this collateral - Prefix byte `json:"prefix" yaml:"prefix"` - MarketID string `json:"market_id" yaml:"market_id"` // marketID for fetching price of the asset from the pricefeed - ConversionFactor sdk.Int `json:"conversion_factor" yaml:"conversion_factor"` // factor for converting internal units to one base unit of collateral + Denom string `json:"denom" yaml:"denom"` // Coin name of collateral type + LiquidationRatio sdk.Dec `json:"liquidation_ratio" yaml:"liquidation_ratio"` // The ratio (Collateral (priced in stable coin) / Debt) under which a CDP will be liquidated + DebtLimit sdk.Coins `json:"debt_limit" yaml:"debt_limit"` // Maximum amount of debt allowed to be drawn from this collateral type + StabilityFee sdk.Dec `json:"stability_fee" yaml:"stability_fee"` // per second stability fee for loans opened using this collateral + AuctionSize sdk.Int // Max amount of collateral to sell off in any one auction. + LiquidationPenalty sdk.Dec // percentage penalty (between [0, 1]) applied to a cdp if it is liquidated + Prefix byte `json:"prefix" yaml:"prefix"` + MarketID string `json:"market_id" yaml:"market_id"` // marketID for fetching price of the asset from the pricefeed + ConversionFactor sdk.Int `json:"conversion_factor" yaml:"conversion_factor"` // factor for converting internal units to one base unit of collateral } // String implements fmt.Stringer @@ -77,11 +90,13 @@ func (cp CollateralParam) String() string { Denom: %s Liquidation Ratio: %s Stability Fee: %s + Liquidation Penalty: %s Debt Limit: %s + Auction Size: %s Prefix: %b Market ID: %s Conversion Factor: %s`, - cp.Denom, cp.LiquidationRatio, cp.StabilityFee, cp.DebtLimit, cp.Prefix, cp.MarketID, cp.ConversionFactor) + cp.Denom, cp.LiquidationRatio, cp.StabilityFee, cp.LiquidationPenalty, cp.DebtLimit, cp.AuctionSize, cp.Prefix, cp.MarketID, cp.ConversionFactor) } // CollateralParams array of CollateralParam @@ -140,6 +155,8 @@ func (p *Params) ParamSetPairs() params.ParamSetPairs { {Key: KeyCollateralParams, Value: &p.CollateralParams}, {Key: KeyDebtParams, Value: &p.DebtParams}, {Key: KeyCircuitBreaker, Value: &p.CircuitBreaker}, + {Key: KeySurplusThreshold, Value: &p.SurplusAuctionThreshold}, + {Key: KeyDebtThreshold, Value: &p.DebtAuctionThreshold}, } } @@ -203,6 +220,12 @@ func (p Params) Validate() error { return fmt.Errorf("collateral debt limit for %s exceeds global debt limit: \n\tglobal debt limit: %s\n\tcollateral debt limits: %s", cp.Denom, p.GlobalDebtLimit, cp.DebtLimit) } + if cp.LiquidationPenalty.LT(sdk.ZeroDec()) || cp.LiquidationPenalty.GT(sdk.OneDec()) { + return fmt.Errorf("liquidation penalty should be between 0 and 1, is %s for %s", cp.LiquidationPenalty, cp.Denom) + } + if !cp.AuctionSize.IsPositive() { + return fmt.Errorf("auction size should be positive, is %s for %s", cp.AuctionSize, cp.Denom) + } } if collateralParamsDebtLimit.IsAnyGT(p.GlobalDebtLimit) { return fmt.Errorf("collateral debt limit exceeds global debt limit:\n\tglobal debt limit: %s\n\tcollateral debt limits: %s", @@ -212,5 +235,12 @@ func (p Params) Validate() error { if p.GlobalDebtLimit.IsAnyNegative() { return fmt.Errorf("global debt limit should be positive for all debt tokens, is %s", p.GlobalDebtLimit) } + + if !p.SurplusAuctionThreshold.IsPositive() { + return fmt.Errorf("surplus auction threshold should be positive, is %s", p.SurplusAuctionThreshold) + } + if !p.DebtAuctionThreshold.IsPositive() { + return fmt.Errorf("debt auction threshold should be positive, is %s", p.DebtAuctionThreshold) + } return nil }