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
This commit is contained in:
Kevin Davis 2020-01-15 15:19:33 +01:00 committed by GitHub
parent ba80b508ab
commit 9b1bf55be7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 706 additions and 336 deletions

3
.gitignore vendored
View File

@ -14,3 +14,6 @@
# Exclude build files
vendor
build
# IDE files
*.vscode

View File

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

View File

@ -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 {

View File

@ -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,

View File

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

View File

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

View File

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

View File

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

View File

@ -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:
$ <appcli> query cdp cdps atom
$ <appcli> query cdp cdps uatom
`),
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {

View File

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

View File

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

231
x/cdp/keeper/auctions.go Normal file
View File

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

View File

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

View File

@ -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 {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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() {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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