mirror of
https://github.com/0glabs/0g-chain.git
synced 2024-11-10 10:05:18 +00:00
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:
parent
ba80b508ab
commit
9b1bf55be7
3
.gitignore
vendored
3
.gitignore
vendored
@ -14,3 +14,6 @@
|
||||
# Exclude build files
|
||||
vendor
|
||||
build
|
||||
|
||||
# IDE files
|
||||
*.vscode
|
15
app/app.go
15
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
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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())
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -40,12 +40,16 @@ func NewCDPGenState(asset string, liquidationRatio sdk.Dec) app.GenesisState {
|
||||
cdpGenesis := cdp.GenesisState{
|
||||
Params: cdp.Params{
|
||||
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
|
||||
LiquidationPenalty: d("0.05"),
|
||||
AuctionSize: i(1000000000),
|
||||
Prefix: 0x20,
|
||||
ConversionFactor: i(6),
|
||||
MarketID: asset + ":usd",
|
||||
@ -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,
|
||||
}
|
||||
@ -98,12 +103,16 @@ func NewCDPGenStateMulti() app.GenesisState {
|
||||
cdpGenesis := cdp.GenesisState{
|
||||
Params: cdp.Params{
|
||||
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
|
||||
LiquidationPenalty: d("0.05"),
|
||||
AuctionSize: i(7000000000),
|
||||
Prefix: 0x20,
|
||||
MarketID: "xrp:usd",
|
||||
ConversionFactor: i(6),
|
||||
@ -113,6 +122,8 @@ func NewCDPGenStateMulti() app.GenesisState {
|
||||
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),
|
||||
@ -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,6 +225,9 @@ 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"},
|
||||
}
|
||||
}
|
||||
|
||||
@ -211,6 +235,8 @@ func baseGenState() cdp.GenesisState {
|
||||
return cdp.GenesisState{
|
||||
Params: cdp.Params{
|
||||
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
231
x/cdp/keeper/auctions.go
Normal 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
|
||||
}
|
76
x/cdp/keeper/auctions_test.go
Normal file
76
x/cdp/keeper/auctions_test.go
Normal 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))
|
||||
}
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -40,12 +40,16 @@ func NewCDPGenState(asset string, liquidationRatio sdk.Dec) app.GenesisState {
|
||||
cdpGenesis := cdp.GenesisState{
|
||||
Params: cdp.Params{
|
||||
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
|
||||
LiquidationPenalty: d("0.05"),
|
||||
AuctionSize: i(100),
|
||||
Prefix: 0x20,
|
||||
ConversionFactor: i(6),
|
||||
MarketID: asset + ":usd",
|
||||
@ -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,
|
||||
}
|
||||
@ -98,12 +103,16 @@ func NewCDPGenStateMulti() app.GenesisState {
|
||||
cdpGenesis := cdp.GenesisState{
|
||||
Params: cdp.Params{
|
||||
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
|
||||
LiquidationPenalty: d("0.05"),
|
||||
AuctionSize: i(7000000000),
|
||||
Prefix: 0x20,
|
||||
MarketID: "xrp:usd",
|
||||
ConversionFactor: i(6),
|
||||
@ -113,6 +122,8 @@ func NewCDPGenStateMulti() app.GenesisState {
|
||||
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),
|
||||
@ -137,6 +148,7 @@ func NewCDPGenStateMulti() app.GenesisState {
|
||||
},
|
||||
StartingCdpID: cdp.DefaultCdpStartingID,
|
||||
DebtDenom: cdp.DefaultDebtDenom,
|
||||
GovDenom: cdp.DefaultGovDenom,
|
||||
CDPs: cdp.CDPs{},
|
||||
PreviousBlockTime: cdp.DefaultPreviousBlockTime,
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -15,19 +15,34 @@ 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
|
||||
deposits := k.GetDeposits(ctx, cdp.ID)
|
||||
for _, dep := range deposits {
|
||||
if !dep.InLiquidation {
|
||||
dep.InLiquidation = true
|
||||
// TODO implement liquidation penalty
|
||||
|
||||
// Move debt coins from cdp to liquidator account
|
||||
deposits := k.GetDeposits(ctx, cdp.ID)
|
||||
debt := sdk.ZeroInt()
|
||||
for _, pc := range cdp.Principal {
|
||||
debt = debt.Add(pc.Amount)
|
||||
}
|
||||
for _, dc := range cdp.AccumulatedFees {
|
||||
debt = debt.Add(dc.Amount)
|
||||
}
|
||||
debtCoin := sdk.NewCoin(k.GetDebtDenom(ctx), debt)
|
||||
err := k.supplyKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, types.LiquidatorMacc, sdk.NewCoins(debtCoin))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// liquidate deposits and send collateral from cdp to liquidator
|
||||
for _, dep := range deposits {
|
||||
ctx.EventManager().EmitEvent(
|
||||
sdk.NewEvent(
|
||||
types.EventTypeCdpLiquidation,
|
||||
@ -36,29 +51,15 @@ func (k Keeper) SeizeCollateral(ctx sdk.Context, cdp types.CDP) {
|
||||
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)
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return
|
||||
k.DeleteDeposit(ctx, dep.CdpID, dep.Depositor)
|
||||
}
|
||||
}
|
||||
|
||||
// Transfer debt coins from cdp module account to liquidator module account
|
||||
debtAmt := sdk.ZeroInt()
|
||||
for _, dc := range cdp.Principal {
|
||||
debtAmt = debtAmt.Add(dc.Amount)
|
||||
}
|
||||
for _, dc := range cdp.AccumulatedFees {
|
||||
debtAmt = debtAmt.Add(dc.Amount)
|
||||
}
|
||||
debtCoins := sdk.NewCoins(sdk.NewCoin(k.GetDebtDenom(ctx), debtAmt))
|
||||
err := k.supplyKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, types.LiquidatorMacc, debtCoins)
|
||||
err = k.AuctionCollateral(ctx, deposits, debt, cdp.Principal[0].Denom)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
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
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -11,47 +11,19 @@ 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))
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
@ -9,8 +9,10 @@ const (
|
||||
EventTypeCdpClose = "cdp_close"
|
||||
EventTypeCdpWithdrawal = "cdp_withdrawal"
|
||||
EventTypeCdpLiquidation = "cdp_liquidation"
|
||||
EventTypeBeginBlockerFatal = "cdp_begin_block_error"
|
||||
|
||||
AttributeKeyCdpID = "cdp_id"
|
||||
AttributeKeyDepositor = "depositor"
|
||||
AttributeValueCategory = "cdp"
|
||||
AttributeKeyError = "error_message"
|
||||
)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
@ -31,6 +36,8 @@ 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"`
|
||||
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"`
|
||||
}
|
||||
|
||||
@ -40,24 +47,28 @@ 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,
|
||||
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
|
||||
@ -66,6 +77,8 @@ type CollateralParam struct {
|
||||
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
|
||||
@ -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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user