R4R: CDP types and methods (#275)

* 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
This commit is contained in:
Kevin Davis 2020-01-12 16:35:34 +01:00 committed by GitHub
parent e1c11d411a
commit d849d690e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
81 changed files with 5135 additions and 2879 deletions

2
.gitignore vendored
View File

@ -8,7 +8,7 @@
# Test binary, build with `go test -c` # Test binary, build with `go test -c`
*.test *.test
# Output of the go coverage tool, specifically when used with LiteIDE # Output of the go coverage tool
*.out *.out
# Exclude build files # Exclude build files

View File

@ -6,7 +6,6 @@ import (
"github.com/kava-labs/kava/x/auction" "github.com/kava-labs/kava/x/auction"
"github.com/kava-labs/kava/x/cdp" "github.com/kava-labs/kava/x/cdp"
"github.com/kava-labs/kava/x/liquidator"
"github.com/kava-labs/kava/x/pricefeed" "github.com/kava-labs/kava/x/pricefeed"
validatorvesting "github.com/kava-labs/kava/x/validator-vesting" validatorvesting "github.com/kava-labs/kava/x/validator-vesting"
@ -61,7 +60,6 @@ var (
supply.AppModuleBasic{}, supply.AppModuleBasic{},
auction.AppModuleBasic{}, auction.AppModuleBasic{},
cdp.AppModuleBasic{}, cdp.AppModuleBasic{},
//liquidator.AppModuleBasic{},
pricefeed.AppModuleBasic{}, pricefeed.AppModuleBasic{},
) )
@ -75,7 +73,8 @@ var (
gov.ModuleName: {supply.Burner}, gov.ModuleName: {supply.Burner},
validatorvesting.ModuleName: {supply.Burner}, validatorvesting.ModuleName: {supply.Burner},
auction.ModuleName: nil, auction.ModuleName: nil,
liquidator.ModuleName: {supply.Minter, supply.Burner}, cdp.ModuleName: {supply.Minter, supply.Burner},
cdp.LiquidatorMacc: {supply.Minter, supply.Burner},
} }
) )
@ -104,7 +103,6 @@ type App struct {
vvKeeper validatorvesting.Keeper vvKeeper validatorvesting.Keeper
auctionKeeper auction.Keeper auctionKeeper auction.Keeper
cdpKeeper cdp.Keeper cdpKeeper cdp.Keeper
liquidatorKeeper liquidator.Keeper
pricefeedKeeper pricefeed.Keeper pricefeedKeeper pricefeed.Keeper
// the module manager // the module manager
@ -129,7 +127,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
bam.MainStoreKey, auth.StoreKey, staking.StoreKey, bam.MainStoreKey, auth.StoreKey, staking.StoreKey,
supply.StoreKey, mint.StoreKey, distr.StoreKey, slashing.StoreKey, supply.StoreKey, mint.StoreKey, distr.StoreKey, slashing.StoreKey,
gov.StoreKey, params.StoreKey, validatorvesting.StoreKey, gov.StoreKey, params.StoreKey, validatorvesting.StoreKey,
auction.StoreKey, cdp.StoreKey, liquidator.StoreKey, pricefeed.StoreKey, auction.StoreKey, cdp.StoreKey, pricefeed.StoreKey,
) )
tkeys := sdk.NewTransientStoreKeys(params.TStoreKey) tkeys := sdk.NewTransientStoreKeys(params.TStoreKey)
@ -153,7 +151,6 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
crisisSubspace := app.paramsKeeper.Subspace(crisis.DefaultParamspace) crisisSubspace := app.paramsKeeper.Subspace(crisis.DefaultParamspace)
auctionSubspace := app.paramsKeeper.Subspace(auction.DefaultParamspace) auctionSubspace := app.paramsKeeper.Subspace(auction.DefaultParamspace)
cdpSubspace := app.paramsKeeper.Subspace(cdp.DefaultParamspace) cdpSubspace := app.paramsKeeper.Subspace(cdp.DefaultParamspace)
//liquidatorSubspace := app.paramsKeeper.Subspace(liquidator.DefaultParamspace)
pricefeedSubspace := app.paramsKeeper.Subspace(pricefeed.DefaultParamspace) pricefeedSubspace := app.paramsKeeper.Subspace(pricefeed.DefaultParamspace)
// add keepers // add keepers
@ -231,24 +228,19 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
keys[pricefeed.StoreKey], keys[pricefeed.StoreKey],
pricefeedSubspace, pricefeedSubspace,
pricefeed.DefaultCodespace) 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.cdpKeeper = cdp.NewKeeper(
app.cdc, app.cdc,
keys[cdp.StoreKey], keys[cdp.StoreKey],
cdpSubspace, cdpSubspace,
app.pricefeedKeeper, app.pricefeedKeeper,
app.bankKeeper) app.supplyKeeper,
cdp.DefaultCodespace)
app.auctionKeeper = auction.NewKeeper( app.auctionKeeper = auction.NewKeeper(
app.cdc, app.cdc,
keys[auction.StoreKey], keys[auction.StoreKey],
app.supplyKeeper, app.supplyKeeper,
auctionSubspace) auctionSubspace)
// app.liquidatorKeeper = liquidator.NewKeeper(
// app.cdc,
// keys[liquidator.StoreKey],
// liquidatorSubspace,
// app.cdpKeeper,
// app.auctionKeeper,
// app.cdpKeeper) // CDP keeper standing in for bank
// register the staking hooks // register the staking hooks
// NOTE: stakingKeeper above is passed by reference, so that it will contain these hooks // NOTE: stakingKeeper above is passed by reference, so that it will contain these hooks
@ -271,7 +263,6 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
validatorvesting.NewAppModule(app.vvKeeper, app.accountKeeper), validatorvesting.NewAppModule(app.vvKeeper, app.accountKeeper),
auction.NewAppModule(app.auctionKeeper), auction.NewAppModule(app.auctionKeeper),
cdp.NewAppModule(app.cdpKeeper, app.pricefeedKeeper), cdp.NewAppModule(app.cdpKeeper, app.pricefeedKeeper),
//liquidator.NewAppModule(app.liquidatorKeeper),
pricefeed.NewAppModule(app.pricefeedKeeper), pricefeed.NewAppModule(app.pricefeedKeeper),
) )
@ -291,7 +282,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
auth.ModuleName, validatorvesting.ModuleName, distr.ModuleName, auth.ModuleName, validatorvesting.ModuleName, distr.ModuleName,
staking.ModuleName, bank.ModuleName, slashing.ModuleName, staking.ModuleName, bank.ModuleName, slashing.ModuleName,
gov.ModuleName, mint.ModuleName, supply.ModuleName, crisis.ModuleName, genutil.ModuleName, gov.ModuleName, mint.ModuleName, supply.ModuleName, crisis.ModuleName, genutil.ModuleName,
pricefeed.ModuleName, cdp.ModuleName, auction.ModuleName, //liquidator.ModuleName, // TODO is this order ok? pricefeed.ModuleName, cdp.ModuleName, auction.ModuleName, // TODO is this order ok?
) )
app.mm.RegisterInvariants(&app.crisisKeeper) app.mm.RegisterInvariants(&app.crisisKeeper)

View File

@ -29,7 +29,6 @@ import (
"github.com/kava-labs/kava/x/auction" "github.com/kava-labs/kava/x/auction"
"github.com/kava-labs/kava/x/cdp" "github.com/kava-labs/kava/x/cdp"
"github.com/kava-labs/kava/x/liquidator"
"github.com/kava-labs/kava/x/pricefeed" "github.com/kava-labs/kava/x/pricefeed"
validatorvesting "github.com/kava-labs/kava/x/validator-vesting" validatorvesting "github.com/kava-labs/kava/x/validator-vesting"
) )
@ -67,7 +66,6 @@ func (tApp TestApp) GetParamsKeeper() params.Keeper { return tApp.params
func (tApp TestApp) GetVVKeeper() validatorvesting.Keeper { return tApp.vvKeeper } func (tApp TestApp) GetVVKeeper() validatorvesting.Keeper { return tApp.vvKeeper }
func (tApp TestApp) GetAuctionKeeper() auction.Keeper { return tApp.auctionKeeper } func (tApp TestApp) GetAuctionKeeper() auction.Keeper { return tApp.auctionKeeper }
func (tApp TestApp) GetCDPKeeper() cdp.Keeper { return tApp.cdpKeeper } func (tApp TestApp) GetCDPKeeper() cdp.Keeper { return tApp.cdpKeeper }
func (tApp TestApp) GetLiquidatorKeeper() liquidator.Keeper { return tApp.liquidatorKeeper }
func (tApp TestApp) GetPriceFeedKeeper() pricefeed.Keeper { return tApp.pricefeedKeeper } 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 // This calls InitChain on the app using the default genesis state, overwitten with any passed in genesis states

View File

@ -12,7 +12,7 @@ import (
"github.com/kava-labs/kava/app" "github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/auction" "github.com/kava-labs/kava/x/auction"
"github.com/kava-labs/kava/x/liquidator" "github.com/kava-labs/kava/x/cdp"
) )
func TestKeeper_EndBlocker(t *testing.T) { func TestKeeper_EndBlocker(t *testing.T) {
@ -21,7 +21,7 @@ func TestKeeper_EndBlocker(t *testing.T) {
buyer := addrs[0] buyer := addrs[0]
returnAddrs := addrs[1:] returnAddrs := addrs[1:]
returnWeights := []sdk.Int{sdk.NewInt(1)} returnWeights := []sdk.Int{sdk.NewInt(1)}
sellerModName := liquidator.ModuleName sellerModName := cdp.LiquidatorMacc
tApp := app.NewTestApp() tApp := app.NewTestApp()
sellerAcc := supply.NewEmptyModuleAccount(sellerModName) sellerAcc := supply.NewEmptyModuleAccount(sellerModName)

View File

@ -13,14 +13,14 @@ import (
"github.com/kava-labs/kava/app" "github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/auction/types" "github.com/kava-labs/kava/x/auction/types"
"github.com/kava-labs/kava/x/liquidator" "github.com/kava-labs/kava/x/cdp"
) )
func TestSurplusAuctionBasic(t *testing.T) { func TestSurplusAuctionBasic(t *testing.T) {
// Setup // Setup
_, addrs := app.GeneratePrivKeyAddressPairs(1) _, addrs := app.GeneratePrivKeyAddressPairs(1)
buyer := addrs[0] buyer := addrs[0]
sellerModName := liquidator.ModuleName sellerModName := cdp.LiquidatorMacc
sellerAddr := supply.NewModuleAddress(sellerModName) sellerAddr := supply.NewModuleAddress(sellerModName)
tApp := app.NewTestApp() tApp := app.NewTestApp()
@ -64,7 +64,7 @@ func TestDebtAuctionBasic(t *testing.T) {
// Setup // Setup
_, addrs := app.GeneratePrivKeyAddressPairs(1) _, addrs := app.GeneratePrivKeyAddressPairs(1)
seller := addrs[0] seller := addrs[0]
buyerModName := liquidator.ModuleName buyerModName := cdp.LiquidatorMacc
buyerAddr := supply.NewModuleAddress(buyerModName) buyerAddr := supply.NewModuleAddress(buyerModName)
tApp := app.NewTestApp() tApp := app.NewTestApp()
@ -104,7 +104,7 @@ func TestCollateralAuctionBasic(t *testing.T) {
buyer := addrs[0] buyer := addrs[0]
returnAddrs := addrs[1:] returnAddrs := addrs[1:]
returnWeights := is(30, 20, 10) returnWeights := is(30, 20, 10)
sellerModName := liquidator.ModuleName sellerModName := cdp.LiquidatorMacc
sellerAddr := supply.NewModuleAddress(sellerModName) sellerAddr := supply.NewModuleAddress(sellerModName)
tApp := app.NewTestApp() tApp := app.NewTestApp()
@ -174,7 +174,7 @@ func TestStartSurplusAuction(t *testing.T) {
{ {
"normal", "normal",
someTime, someTime,
args{liquidator.ModuleName, c("stable", 10), "gov"}, args{cdp.LiquidatorMacc, c("stable", 10), "gov"},
true, true,
}, },
{ {
@ -186,13 +186,13 @@ func TestStartSurplusAuction(t *testing.T) {
{ {
"not enough coins", "not enough coins",
someTime, someTime,
args{liquidator.ModuleName, c("stable", 101), "gov"}, args{cdp.LiquidatorMacc, c("stable", 101), "gov"},
false, false,
}, },
{ {
"incorrect denom", "incorrect denom",
someTime, someTime,
args{liquidator.ModuleName, c("notacoin", 10), "gov"}, args{cdp.LiquidatorMacc, c("notacoin", 10), "gov"},
false, false,
}, },
} }
@ -202,7 +202,7 @@ func TestStartSurplusAuction(t *testing.T) {
initialLiquidatorCoins := cs(c("stable", 100)) initialLiquidatorCoins := cs(c("stable", 100))
tApp := app.NewTestApp() tApp := app.NewTestApp()
liqAcc := supply.NewEmptyModuleAccount(liquidator.ModuleName, supply.Burner) liqAcc := supply.NewEmptyModuleAccount(cdp.LiquidatorMacc, supply.Burner)
require.NoError(t, liqAcc.SetCoins(initialLiquidatorCoins)) require.NoError(t, liqAcc.SetCoins(initialLiquidatorCoins))
tApp.InitializeFromGenesisStates( tApp.InitializeFromGenesisStates(
NewAuthGenStateFromAccs(authexported.GenesisAccounts{liqAcc}), NewAuthGenStateFromAccs(authexported.GenesisAccounts{liqAcc}),
@ -215,7 +215,7 @@ func TestStartSurplusAuction(t *testing.T) {
// check // check
sk := tApp.GetSupplyKeeper() sk := tApp.GetSupplyKeeper()
liquidatorCoins := sk.GetModuleAccount(ctx, liquidator.ModuleName).GetCoins() liquidatorCoins := sk.GetModuleAccount(ctx, cdp.LiquidatorMacc).GetCoins()
actualAuc, found := keeper.GetAuction(ctx, id) actualAuc, found := keeper.GetAuction(ctx, id)
if tc.expectPass { if tc.expectPass {

25
x/cdp/abci.go Normal file
View File

@ -0,0 +1,25 @@
package cdp
import (
sdk "github.com/cosmos/cosmos-sdk/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
func BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock, k Keeper) {
params := k.GetParams(ctx)
previousBlockTime, found := k.GetPreviousBlockTime(ctx)
if !found {
previousBlockTime = ctx.BlockTime()
}
timeElapsed := sdk.NewInt(ctx.BlockTime().Unix() - previousBlockTime.Unix())
for _, cp := range params.CollateralParams {
for _, dp := range params.DebtParams {
k.HandleNewDebt(ctx, cp.Denom, dp.Denom, timeElapsed)
}
k.LiquidateCdps(ctx, cp.MarketID, cp.Denom, cp.LiquidationRatio)
}
k.SetPreviousBlockTime(ctx, ctx.BlockTime())
return
}

126
x/cdp/abci_test.go Normal file
View File

@ -0,0 +1,126 @@
package cdp_test
import (
"math/rand"
"testing"
"time"
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/cdp"
"github.com/stretchr/testify/suite"
abci "github.com/tendermint/tendermint/abci/types"
tmtime "github.com/tendermint/tendermint/types/time"
)
type ModuleTestSuite struct {
suite.Suite
keeper cdp.Keeper
addrs []sdk.AccAddress
app app.TestApp
cdps cdp.CDPs
ctx sdk.Context
liquidations liquidationTracker
}
type liquidationTracker struct {
xrp []uint64
btc []uint64
debt int64
}
func (suite *ModuleTestSuite) SetupTest() {
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
cdps := make(cdp.CDPs, 100)
_, addrs := app.GeneratePrivKeyAddressPairs(100)
coins := []sdk.Coins{}
tracker := liquidationTracker{}
for j := 0; j < 100; j++ {
coins = append(coins, cs(c("btc", 100000000), c("xrp", 10000000000)))
}
authGS := app.NewAuthGenState(
addrs, coins)
tApp.InitializeFromGenesisStates(
authGS,
NewPricefeedGenStateMulti(),
NewCDPGenStateMulti(),
)
suite.ctx = ctx
suite.app = tApp
suite.keeper = tApp.GetCDPKeeper()
for j := 0; j < 100; j++ {
collateral := "xrp"
amount := 10000000000
debt := simulation.RandIntBetween(rand.New(rand.NewSource(int64(j))), 750000000, 1249000000)
if j%2 == 0 {
collateral = "btc"
amount = 100000000
debt = simulation.RandIntBetween(rand.New(rand.NewSource(int64(j))), 2700000000, 5332000000)
if debt >= 4000000000 {
tracker.btc = append(tracker.btc, uint64(j+1))
tracker.debt += int64(debt)
}
} else {
if debt >= 1000000000 {
tracker.xrp = append(tracker.xrp, uint64(j+1))
tracker.debt += int64(debt)
}
}
suite.Nil(suite.keeper.AddCdp(suite.ctx, addrs[j], cs(c(collateral, int64(amount))), cs(c("usdx", int64(debt)))))
c, f := suite.keeper.GetCDP(suite.ctx, collateral, uint64(j+1))
suite.True(f)
cdps[j] = c
}
suite.cdps = cdps
suite.addrs = addrs
suite.liquidations = tracker
}
func (suite *ModuleTestSuite) setPrice(price sdk.Dec, market string) {
pfKeeper := suite.app.GetPriceFeedKeeper()
pfKeeper.SetPrice(suite.ctx, sdk.AccAddress{}, market, price, suite.ctx.BlockTime().Add(time.Hour*3))
err := pfKeeper.SetCurrentPrices(suite.ctx, market)
suite.NoError(err)
pp, err := pfKeeper.GetCurrentPrice(suite.ctx, market)
suite.NoError(err)
suite.Equal(price, pp.Price)
}
func (suite *ModuleTestSuite) TestBeginBlock() {
sk := suite.app.GetSupplyKeeper()
acc := sk.GetModuleAccount(suite.ctx, cdp.ModuleName)
originalXrpCollateral := acc.GetCoins().AmountOf("xrp")
suite.setPrice(d("0.2"), "xrp:usd")
cdp.BeginBlocker(suite.ctx, abci.RequestBeginBlock{Header: suite.ctx.BlockHeader()}, suite.keeper)
acc = sk.GetModuleAccount(suite.ctx, cdp.ModuleName)
finalXrpCollateral := acc.GetCoins().AmountOf("xrp")
seizedXrpCollateral := originalXrpCollateral.Sub(finalXrpCollateral)
xrpLiquidations := int(seizedXrpCollateral.Quo(i(10000000000)).Int64())
suite.Equal(len(suite.liquidations.xrp), xrpLiquidations)
acc = sk.GetModuleAccount(suite.ctx, cdp.ModuleName)
originalBtcCollateral := acc.GetCoins().AmountOf("btc")
suite.setPrice(d("6000"), "btc:usd")
cdp.BeginBlocker(suite.ctx, abci.RequestBeginBlock{Header: suite.ctx.BlockHeader()}, suite.keeper)
acc = sk.GetModuleAccount(suite.ctx, cdp.ModuleName)
finalBtcCollateral := acc.GetCoins().AmountOf("btc")
seizedBtcCollateral := originalBtcCollateral.Sub(finalBtcCollateral)
btcLiquidations := int(seizedBtcCollateral.Quo(i(100000000)).Int64())
suite.Equal(len(suite.liquidations.btc), btcLiquidations)
acc = sk.GetModuleAccount(suite.ctx, cdp.LiquidatorMacc)
suite.Equal(suite.liquidations.debt, acc.GetCoins().AmountOf("debt").Int64())
}
func TestModuleTestSuite(t *testing.T) {
suite.Run(t, new(ModuleTestSuite))
}

View File

@ -11,51 +11,151 @@ import (
) )
const ( const (
StatusNil = types.StatusNil
StatusLiquidated = types.StatusLiquidated
DefaultCodespace = types.DefaultCodespace
CodeCdpAlreadyExists = types.CodeCdpAlreadyExists
CodeCollateralLengthInvalid = types.CodeCollateralLengthInvalid
CodeCollateralNotSupported = types.CodeCollateralNotSupported
CodeDebtNotSupported = types.CodeDebtNotSupported
CodeExceedsDebtLimit = types.CodeExceedsDebtLimit
CodeInvalidCollateralRatio = types.CodeInvalidCollateralRatio
CodeCdpNotFound = types.CodeCdpNotFound
CodeDepositNotFound = types.CodeDepositNotFound
CodeInvalidDepositDenom = types.CodeInvalidDepositDenom
CodeInvalidPaymentDenom = types.CodeInvalidPaymentDenom
CodeDepositNotAvailable = types.CodeDepositNotAvailable
CodeInvalidCollateralDenom = types.CodeInvalidCollateralDenom
CodeInvalidWithdrawAmount = types.CodeInvalidWithdrawAmount
CodeCdpNotAvailable = types.CodeCdpNotAvailable
CodeBelowDebtFloor = types.CodeBelowDebtFloor
EventTypeCreateCdp = types.EventTypeCreateCdp
EventTypeCdpDeposit = types.EventTypeCdpDeposit
EventTypeCdpDraw = types.EventTypeCdpDraw
EventTypeCdpRepay = types.EventTypeCdpRepay
EventTypeCdpClose = types.EventTypeCdpClose
EventTypeCdpWithdrawal = types.EventTypeCdpWithdrawal
EventTypeCdpLiquidation = types.EventTypeCdpLiquidation
AttributeKeyCdpID = types.AttributeKeyCdpID
AttributeKeyDepositor = types.AttributeKeyDepositor
AttributeValueCategory = types.AttributeValueCategory
LiquidatorMacc = types.LiquidatorMacc
ModuleName = types.ModuleName ModuleName = types.ModuleName
StoreKey = types.StoreKey StoreKey = types.StoreKey
RouterKey = types.RouterKey RouterKey = types.RouterKey
QuerierRoute = types.QuerierRoute
DefaultParamspace = types.DefaultParamspace DefaultParamspace = types.DefaultParamspace
QueryGetCdp = types.QueryGetCdp
QueryGetCdps = types.QueryGetCdps QueryGetCdps = types.QueryGetCdps
QueryGetCdpsByCollateralization = types.QueryGetCdpsByCollateralization
QueryGetParams = types.QueryGetParams QueryGetParams = types.QueryGetParams
RestOwner = types.RestOwner RestOwner = types.RestOwner
RestCollateralDenom = types.RestCollateralDenom RestCollateralDenom = types.RestCollateralDenom
RestUnderCollateralizedAt = types.RestUnderCollateralizedAt RestRatio = types.RestRatio
GovDenom = types.GovDenom
) )
var ( var (
// functions aliases // functions aliases
NewCDP = types.NewCDP
RegisterCodec = types.RegisterCodec RegisterCodec = types.RegisterCodec
StatusFromByte = types.StatusFromByte
NewDeposit = types.NewDeposit
ErrCdpAlreadyExists = types.ErrCdpAlreadyExists
ErrInvalidCollateralLength = types.ErrInvalidCollateralLength
ErrCollateralNotSupported = types.ErrCollateralNotSupported
ErrDebtNotSupported = types.ErrDebtNotSupported
ErrExceedsDebtLimit = types.ErrExceedsDebtLimit
ErrInvalidCollateralRatio = types.ErrInvalidCollateralRatio
ErrCdpNotFound = types.ErrCdpNotFound
ErrDepositNotFound = types.ErrDepositNotFound
ErrInvalidDepositDenom = types.ErrInvalidDepositDenom
ErrInvalidPaymentDenom = types.ErrInvalidPaymentDenom
ErrDepositNotAvailable = types.ErrDepositNotAvailable
ErrInvalidCollateralDenom = types.ErrInvalidCollateralDenom
ErrInvalidWithdrawAmount = types.ErrInvalidWithdrawAmount
ErrCdpNotAvailable = types.ErrCdpNotAvailable
ErrBelowDebtFloor = types.ErrBelowDebtFloor
DefaultGenesisState = types.DefaultGenesisState DefaultGenesisState = types.DefaultGenesisState
ValidateGenesis = types.ValidateGenesis GetCdpIDBytes = types.GetCdpIDBytes
NewMsgCreateOrModifyCDP = types.NewMsgCreateOrModifyCDP GetCdpIDFromBytes = types.GetCdpIDFromBytes
ParamKeyTable = types.ParamKeyTable CdpKey = types.CdpKey
SplitCdpKey = types.SplitCdpKey
DenomIterKey = types.DenomIterKey
SplitDenomIterKey = types.SplitDenomIterKey
DepositKey = types.DepositKey
SplitDepositKey = types.SplitDepositKey
DepositIterKey = types.DepositIterKey
SplitDepositIterKey = types.SplitDepositIterKey
CollateralRatioBytes = types.CollateralRatioBytes
CollateralRatioKey = types.CollateralRatioKey
SplitCollateralRatioKey = types.SplitCollateralRatioKey
CollateralRatioIterKey = types.CollateralRatioIterKey
SplitCollateralRatioIterKey = types.SplitCollateralRatioIterKey
NewMsgCreateCDP = types.NewMsgCreateCDP
NewMsgDeposit = types.NewMsgDeposit
NewMsgWithdraw = types.NewMsgWithdraw
NewMsgDrawDebt = types.NewMsgDrawDebt
NewMsgRepayDebt = types.NewMsgRepayDebt
NewParams = types.NewParams
DefaultParams = types.DefaultParams DefaultParams = types.DefaultParams
ParamKeyTable = types.ParamKeyTable
NewQueryCdpsParams = types.NewQueryCdpsParams
NewQueryCdpParams = types.NewQueryCdpParams
NewQueryCdpsByRatioParams = types.NewQueryCdpsByRatioParams
ValidSortableDec = types.ValidSortableDec
SortableDecBytes = types.SortableDecBytes
ParseDecBytes = types.ParseDecBytes
RelativePow = types.RelativePow
NewKeeper = keeper.NewKeeper NewKeeper = keeper.NewKeeper
NewQuerier = keeper.NewQuerier NewQuerier = keeper.NewQuerier
// variable aliases // variable aliases
ModuleCdc = types.ModuleCdc ModuleCdc = types.ModuleCdc
CdpIDKeyPrefix = types.CdpIDKeyPrefix
CdpKeyPrefix = types.CdpKeyPrefix
CollateralRatioIndexPrefix = types.CollateralRatioIndexPrefix
CdpIDKey = types.CdpIDKey
DebtDenomKey = types.DebtDenomKey
DepositKeyPrefix = types.DepositKeyPrefix
PrincipalKeyPrefix = types.PrincipalKeyPrefix
AccumulatorKeyPrefix = types.AccumulatorKeyPrefix
PreviousBlockTimeKey = types.PreviousBlockTimeKey
KeyGlobalDebtLimit = types.KeyGlobalDebtLimit KeyGlobalDebtLimit = types.KeyGlobalDebtLimit
KeyCollateralParams = types.KeyCollateralParams KeyCollateralParams = types.KeyCollateralParams
KeyStableDenoms = types.KeyStableDenoms KeyDebtParams = types.KeyDebtParams
LiquidatorAccountAddress = keeper.LiquidatorAccountAddress KeyCircuitBreaker = types.KeyCircuitBreaker
DefaultGlobalDebt = types.DefaultGlobalDebt
DefaultCircuitBreaker = types.DefaultCircuitBreaker
DefaultCollateralParams = types.DefaultCollateralParams
DefaultDebtParams = types.DefaultDebtParams
DefaultCdpStartingID = types.DefaultCdpStartingID
DefaultDebtDenom = types.DefaultDebtDenom
DefaultPreviousBlockTime = types.DefaultPreviousBlockTime
MaxSortableDec = types.MaxSortableDec
) )
type ( type (
BankKeeper = types.BankKeeper
PricefeedKeeper = types.PricefeedKeeper
GenesisState = types.GenesisState
MsgCreateOrModifyCDP = types.MsgCreateOrModifyCDP
MsgTransferCDP = types.MsgTransferCDP
CdpParams = types.CdpParams
CollateralParams = types.CollateralParams
QueryCdpsParams = types.QueryCdpsParams
ModifyCdpRequestBody = types.ModifyCdpRequestBody
CDP = types.CDP CDP = types.CDP
CDPs = types.CDPs CDPs = types.CDPs
ByCollateralRatio = types.ByCollateralRatio Deposit = types.Deposit
CollateralState = types.CollateralState DepositStatus = types.DepositStatus
Deposits = types.Deposits
SupplyKeeper = types.SupplyKeeper
PricefeedKeeper = types.PricefeedKeeper
GenesisState = types.GenesisState
MsgCreateCDP = types.MsgCreateCDP
MsgDeposit = types.MsgDeposit
MsgWithdraw = types.MsgWithdraw
MsgDrawDebt = types.MsgDrawDebt
MsgRepayDebt = types.MsgRepayDebt
MsgTransferCDP = types.MsgTransferCDP
Params = types.Params
CollateralParam = types.CollateralParam
CollateralParams = types.CollateralParams
DebtParam = types.DebtParam
DebtParams = types.DebtParams
QueryCdpsParams = types.QueryCdpsParams
QueryCdpParams = types.QueryCdpParams
QueryCdpsByRatioParams = types.QueryCdpsByRatioParams
Keeper = keeper.Keeper Keeper = keeper.Keeper
LiquidatorModuleAccount = keeper.LiquidatorModuleAccount
) )

View File

@ -1,55 +0,0 @@
package cdp_test
import (
"testing"
"github.com/cosmos/cosmos-sdk/simapp"
sdk "github.com/cosmos/cosmos-sdk/types"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/cdp"
)
func TestApp_CreateModifyDeleteCDP(t *testing.T) {
// Setup
tApp := app.NewTestApp()
privKeys, addrs := app.GeneratePrivKeyAddressPairs(1)
testAddr := addrs[0]
testPrivKey := privKeys[0]
tApp.InitializeFromGenesisStates(
app.NewAuthGenState(addrs, []sdk.Coins{cs(c("xrp", 100))}),
NewPFGenState("xrp", d("1.00")),
NewCDPGenState("xrp", d("1.5")),
)
// check balance
ctx := tApp.NewContext(false, abci.Header{})
tApp.CheckBalance(t, ctx, testAddr, cs(c("xrp", 100)))
tApp.EndBlock(abci.RequestEndBlock{})
tApp.Commit()
// Create CDP
msgs := []sdk.Msg{cdp.NewMsgCreateOrModifyCDP(testAddr, "xrp", i(10), i(5))}
simapp.SignCheckDeliver(t, tApp.Codec(), tApp.BaseApp, abci.Header{Height: tApp.LastBlockHeight() + 1}, msgs, []uint64{0}, []uint64{0}, true, true, testPrivKey)
// check balance
ctx = tApp.NewContext(true, abci.Header{})
tApp.CheckBalance(t, ctx, testAddr, cs(c("usdx", 5), c("xrp", 90)))
// Modify CDP
msgs = []sdk.Msg{cdp.NewMsgCreateOrModifyCDP(testAddr, "xrp", i(40), i(5))}
simapp.SignCheckDeliver(t, tApp.Codec(), tApp.BaseApp, abci.Header{Height: tApp.LastBlockHeight() + 1}, msgs, []uint64{0}, []uint64{1}, true, true, testPrivKey)
// check balance
ctx = tApp.NewContext(true, abci.Header{})
tApp.CheckBalance(t, ctx, testAddr, cs(c("usdx", 10), c("xrp", 50)))
// Delete CDP
msgs = []sdk.Msg{cdp.NewMsgCreateOrModifyCDP(testAddr, "xrp", i(-50), i(-10))}
simapp.SignCheckDeliver(t, tApp.Codec(), tApp.BaseApp, abci.Header{Height: tApp.LastBlockHeight() + 1}, msgs, []uint64{0}, []uint64{2}, true, true, testPrivKey)
// check balance
ctx = tApp.NewContext(true, abci.Header{})
tApp.CheckBalance(t, ctx, testAddr, cs(c("xrp", 100)))
}

View File

@ -2,6 +2,7 @@ package cli
import ( import (
"fmt" "fmt"
"strings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -22,17 +23,17 @@ func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
} }
cdpQueryCmd.AddCommand(client.GetCommands( cdpQueryCmd.AddCommand(client.GetCommands(
GetCmdGetCdp(queryRoute, cdc), QueryCdpCmd(queryRoute, cdc),
GetCmdGetCdps(queryRoute, cdc), QueryCdpsByDenomCmd(queryRoute, cdc),
GetCmdGetUnderCollateralizedCdps(queryRoute, cdc), QueryCdpsByDenomAndRatioCmd(queryRoute, cdc),
GetCmdGetParams(queryRoute, cdc), QueryParamsCmd(queryRoute, cdc),
)...) )...)
return cdpQueryCmd return cdpQueryCmd
} }
// GetCmdGetCdp queries the latest info about a particular cdp // QueryCdpCmd returns the command handler for querying a particular cdp
func GetCmdGetCdp(queryRoute string, cdc *codec.Codec) *cobra.Command { func QueryCdpCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
return &cobra.Command{ return &cobra.Command{
Use: "cdp [ownerAddress] [collateralType]", Use: "cdp [ownerAddress] [collateralType]",
Short: "get info about a cdp", Short: "get info about a cdp",
@ -47,7 +48,6 @@ func GetCmdGetCdp(queryRoute string, cdc *codec.Codec) *cobra.Command {
} }
collateralType := args[1] // TODO validation? collateralType := args[1] // TODO validation?
bz, err := cdc.MarshalJSON(types.QueryCdpsParams{ bz, err := cdc.MarshalJSON(types.QueryCdpsParams{
Owner: ownerAddress,
CollateralDenom: collateralType, CollateralDenom: collateralType,
}) })
if err != nil { if err != nil {
@ -55,7 +55,7 @@ func GetCmdGetCdp(queryRoute string, cdc *codec.Codec) *cobra.Command {
} }
// Query // Query
route := fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetCdps) route := fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetCdp)
res, _, err := cliCtx.QueryWithData(route, bz) res, _, err := cliCtx.QueryWithData(route, bz)
if err != nil { if err != nil {
fmt.Printf("error when getting cdp info - %s", err) fmt.Printf("error when getting cdp info - %s", err)
@ -64,28 +64,28 @@ func GetCmdGetCdp(queryRoute string, cdc *codec.Codec) *cobra.Command {
} }
// Decode and print results // Decode and print results
var cdps types.CDPs var cdp types.CDP
cdc.MustUnmarshalJSON(res, &cdps) cdc.MustUnmarshalJSON(res, &cdp)
if len(cdps) != 1 { return cliCtx.PrintOutput(cdp)
panic("Unexpected number of CDPs returned from querier. This shouldn't happen.")
}
return cliCtx.PrintOutput(cdps[0])
}, },
} }
} }
// GetCmdGetCdps queries the store for all cdps for a collateral type // QueryCdpsByDenomCmd returns the command handler for querying cdps for a collateral type
func GetCmdGetCdps(queryRoute string, cdc *codec.Codec) *cobra.Command { func QueryCdpsByDenomCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
return &cobra.Command{ return &cobra.Command{
Use: "cdps [collateralType]", Use: "cdps [collateralType]",
Short: "get info about many cdps", Short: "Query cdps by collateral type",
Long: "Get all CDPs. Specify a collateral type to get only CDPs with that 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
`),
Args: cobra.MaximumNArgs(1), Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc) cliCtx := context.NewCLIContext().WithCodec(cdc)
// Prepare params for querier // Prepare params for querier
bz, err := cdc.MarshalJSON(types.QueryCdpsParams{CollateralDenom: args[0]}) // denom="" returns all CDPs // TODO will this fail if there are no args? bz, err := cdc.MarshalJSON(types.QueryCdpsParams{CollateralDenom: args[0]})
if err != nil { if err != nil {
return err return err
} }
@ -105,11 +105,14 @@ func GetCmdGetCdps(queryRoute string, cdc *codec.Codec) *cobra.Command {
} }
} }
func GetCmdGetUnderCollateralizedCdps(queryRoute string, cdc *codec.Codec) *cobra.Command { // QueryCdpsByDenomAndRatioCmd returns the command handler for querying cdps
// by specified collateral type and collateralization ratio
func QueryCdpsByDenomAndRatioCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
return &cobra.Command{ return &cobra.Command{
Use: "bad-cdps [collateralType] [price]", Use: "cdps [collateralType] [ratio]",
Short: "get under collateralized CDPs", Short: "get cdps with matching collateral type and below the specified ratio",
Long: "Get all CDPS of a particular collateral type that will be under collateralized at the specified price. Pass in the current price to get currently under collateralized CDPs.", Long: strings.TrimSpace(`Get all CDPS of a particular collateral type with collateralization
ratio below the specified input.`),
Args: cobra.ExactArgs(2), Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc) cliCtx := context.NewCLIContext().WithCodec(cdc)
@ -117,11 +120,11 @@ func GetCmdGetUnderCollateralizedCdps(queryRoute string, cdc *codec.Codec) *cobr
// Prepare params for querier // Prepare params for querier
price, errSdk := sdk.NewDecFromStr(args[1]) price, errSdk := sdk.NewDecFromStr(args[1])
if errSdk != nil { if errSdk != nil {
return fmt.Errorf(errSdk.Error()) // TODO check this returns useful output return fmt.Errorf(errSdk.Error())
} }
bz, err := cdc.MarshalJSON(types.QueryCdpsParams{ bz, err := cdc.MarshalJSON(types.QueryCdpsByRatioParams{
CollateralDenom: args[0], CollateralDenom: args[0],
UnderCollateralizedAt: price, Ratio: price,
}) })
if err != nil { if err != nil {
return err return err
@ -142,7 +145,8 @@ func GetCmdGetUnderCollateralizedCdps(queryRoute string, cdc *codec.Codec) *cobr
} }
} }
func GetCmdGetParams(queryRoute string, cdc *codec.Codec) *cobra.Command { // QueryParamsCmd returns the command handler for cdp parameter querying
func QueryParamsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
return &cobra.Command{ return &cobra.Command{
Use: "params", Use: "params",
Short: "get the cdp module parameters", Short: "get the cdp module parameters",
@ -153,13 +157,13 @@ func GetCmdGetParams(queryRoute string, cdc *codec.Codec) *cobra.Command {
// Query // Query
route := fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetParams) route := fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetParams)
res, _, err := cliCtx.QueryWithData(route, nil) // TODO use cliCtx.QueryStore? res, _, err := cliCtx.QueryWithData(route, nil)
if err != nil { if err != nil {
return err return err
} }
// Decode and print results // Decode and print results
var out types.CdpParams var out types.QueryCdpParams
cdc.MustUnmarshalJSON(res, &out) cdc.MustUnmarshalJSON(res, &out)
return cliCtx.PrintOutput(out) return cliCtx.PrintOutput(out)
}, },

View File

@ -1,8 +1,6 @@
package cli package cli
import ( import (
"fmt"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client"
@ -13,11 +11,9 @@ import (
"github.com/cosmos/cosmos-sdk/x/auth/client/utils" "github.com/cosmos/cosmos-sdk/x/auth/client/utils"
"github.com/kava-labs/kava/x/cdp/types" "github.com/kava-labs/kava/x/cdp/types"
) )
// GetTxCmd returns the transaction commands for this module // GetTxCmd returns the transaction commands for this module
// TODO: Tests, see: https://github.com/cosmos/cosmos-sdk/blob/18de630d0ae1887113e266982b51c2bf1f662edb/x/staking/client/cli/tx_test.go
func GetTxCmd(cdc *codec.Codec) *cobra.Command { func GetTxCmd(cdc *codec.Codec) *cobra.Command {
cdpTxCmd := &cobra.Command{ cdpTxCmd := &cobra.Command{
Use: "cdp", Use: "cdp",
@ -25,33 +21,151 @@ func GetTxCmd(cdc *codec.Codec) *cobra.Command {
} }
cdpTxCmd.AddCommand(client.PostCommands( cdpTxCmd.AddCommand(client.PostCommands(
GetCmdModifyCdp(cdc), GetCmdCreateCdp(cdc),
GetCmdDeposit(cdc),
GetCmdWithdraw(cdc),
GetCmdDraw(cdc),
GetCmdRepay(cdc),
)...) )...)
return cdpTxCmd return cdpTxCmd
} }
// GetCmdModifyCdp cli command for creating and modifying cdps. // GetCmdCreateCdp returns the command handler for creating a cdp
func GetCmdModifyCdp(cdc *codec.Codec) *cobra.Command { func GetCmdCreateCdp(cdc *codec.Codec) *cobra.Command {
return &cobra.Command{ return &cobra.Command{
Use: "modifycdp [ownerAddress] [collateralType] [collateralChange] [debtChange]", Use: "create [ownerAddress] [collateralChange] [debtChange]",
Short: "create or modify a cdp", Short: "create a new cdp",
Args: cobra.ExactArgs(4), Args: cobra.ExactArgs(3),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc) cliCtx := context.NewCLIContext().WithCodec(cdc)
txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc)) txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc))
collateralChange, ok := sdk.NewIntFromString(args[2]) collateral, err := sdk.ParseCoins(args[1])
if !ok { if err != nil {
fmt.Printf("invalid collateral amount - %s \n", string(args[2])) return err
return nil
} }
debtChange, ok := sdk.NewIntFromString(args[3]) debt, err := sdk.ParseCoins(args[2])
if !ok { if err != nil {
fmt.Printf("invalid debt amount - %s \n", string(args[3])) return err
return nil
} }
msg := types.NewMsgCreateOrModifyCDP(cliCtx.GetFromAddress(), args[1], collateralChange, debtChange) msg := types.NewMsgCreateCDP(cliCtx.GetFromAddress(), collateral, debt)
err := msg.ValidateBasic() err = msg.ValidateBasic()
if err != nil {
return err
}
return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
},
}
}
// GetCmdDeposit cli command for depositing to a cdp.
func GetCmdDeposit(cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "deposit [ownerAddress] [depositorAddress] [collateralChange]",
Short: "deposit to an existing cdp",
Args: cobra.ExactArgs(3),
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc))
collateral, err := sdk.ParseCoins(args[2])
if err != nil {
return err
}
owner, err := sdk.AccAddressFromBech32(args[0])
if err != nil {
return err
}
depositor, err := sdk.AccAddressFromBech32(args[1])
if err != nil {
return err
}
msg := types.NewMsgDeposit(owner, depositor, collateral)
err = msg.ValidateBasic()
if err != nil {
return err
}
return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
},
}
}
// GetCmdWithdraw cli command for withdrawing from a cdp.
func GetCmdWithdraw(cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "withdraw [ownerAddress] [depositorAddress] [collateralChange]",
Short: "withdraw from an existing cdp",
Args: cobra.ExactArgs(3),
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc))
collateral, err := sdk.ParseCoins(args[2])
if err != nil {
return err
}
owner, err := sdk.AccAddressFromBech32(args[0])
if err != nil {
return err
}
depositor, err := sdk.AccAddressFromBech32(args[1])
if err != nil {
return err
}
msg := types.NewMsgWithdraw(owner, depositor, collateral)
err = msg.ValidateBasic()
if err != nil {
return err
}
return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
},
}
}
// GetCmdDraw cli command for depositing to a cdp.
func GetCmdDraw(cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "draw [ownerAddress] [collateralDenom] [debtChange]",
Short: "draw debt off an existing cdp",
Args: cobra.ExactArgs(3),
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc))
debt, err := sdk.ParseCoins(args[2])
if err != nil {
return err
}
owner, err := sdk.AccAddressFromBech32(args[0])
if err != nil {
return err
}
msg := types.NewMsgDrawDebt(owner, args[1], debt)
err = msg.ValidateBasic()
if err != nil {
return err
}
return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
},
}
}
// GetCmdRepay cli command for depositing to a cdp.
func GetCmdRepay(cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "repay [ownerAddress] [collateralDenom] [payment]",
Short: "repay debt from an existing cdp",
Args: cobra.ExactArgs(3),
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc))
payment, err := sdk.ParseCoins(args[2])
if err != nil {
return err
}
owner, err := sdk.AccAddressFromBech32(args[0])
if err != nil {
return err
}
msg := types.NewMsgRepayDebt(owner, args[1], payment)
err = msg.ValidateBasic()
if err != nil { if err != nil {
return err return err
} }

View File

@ -1,51 +0,0 @@
package client
import (
"github.com/cosmos/cosmos-sdk/client"
cdpcmd "github.com/kava-labs/kava/x/cdp/client/cli"
"github.com/spf13/cobra"
amino "github.com/tendermint/go-amino"
)
// ModuleClient exports all client functionality from this module
type ModuleClient struct {
storeKey string
cdc *amino.Codec
}
// NewModuleClient creates client for the module
func NewModuleClient(storeKey string, cdc *amino.Codec) ModuleClient {
return ModuleClient{storeKey, cdc}
}
// GetQueryCmd returns the cli query commands for this module
func (mc ModuleClient) GetQueryCmd() *cobra.Command {
// Group nameservice queries under a subcommand
cdpQueryCmd := &cobra.Command{
Use: "cdp",
Short: "Querying commands for the cdp module",
}
cdpQueryCmd.AddCommand(client.GetCommands(
cdpcmd.GetCmdGetCdp(mc.storeKey, mc.cdc),
cdpcmd.GetCmdGetCdps(mc.storeKey, mc.cdc),
cdpcmd.GetCmdGetUnderCollateralizedCdps(mc.storeKey, mc.cdc),
cdpcmd.GetCmdGetParams(mc.storeKey, mc.cdc),
)...)
return cdpQueryCmd
}
// GetTxCmd returns the transaction commands for this module
func (mc ModuleClient) GetTxCmd() *cobra.Command {
cdpTxCmd := &cobra.Command{
Use: "cdp",
Short: "cdp transactions subcommands",
}
cdpTxCmd.AddCommand(client.PostCommands(
cdpcmd.GetCmdModifyCdp(mc.cdc),
)...)
return cdpTxCmd
}

View File

@ -14,66 +14,91 @@ import (
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
) )
/* // define routes that get registered by the main application
API Design:
Currently CDPs do not have IDs so standard REST uri conventions (ie GET /cdps/{cdp-id}) don't work too well.
Get one or more cdps
GET /cdps?collateralDenom={denom}&owner={address}&underCollateralizedAt={price}
Modify a CDP (idempotent). Create is not separated out because conceptually all CDPs already exist (just with zero collateral and debt). // TODO is making this idempotent actually useful?
PUT /cdps
Get the module params, including authorized collateral denoms.
GET /params
*/
// RegisterRoutes - Central function to define routes that get registered by the main application
func registerQueryRoutes(cliCtx context.CLIContext, r *mux.Router) { func registerQueryRoutes(cliCtx context.CLIContext, r *mux.Router) {
r.HandleFunc("/cdps", getCdpsHandlerFn(cliCtx)).Methods("GET") r.HandleFunc(fmt.Sprintf("/cdp/{%s}/{%s}", types.RestOwner, types.RestCollateralDenom), queryCdpHandlerFn(cliCtx)).Methods("GET")
r.HandleFunc(fmt.Sprintf("/cdp/{%s}", types.RestCollateralDenom), queryCdpsHandlerFn(cliCtx)).Methods("GET")
r.HandleFunc(fmt.Sprintf("/cdp/{%s}/ratio/{%s}", types.RestCollateralDenom, types.RestRatio), queryCdpsByRatioHandlerFn(cliCtx)).Methods("GET")
r.HandleFunc("/cdps/params", getParamsHandlerFn(cliCtx)).Methods("GET") r.HandleFunc("/cdps/params", getParamsHandlerFn(cliCtx)).Methods("GET")
} }
func getCdpsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { func queryCdpHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
// get parameters from the URL vars := mux.Vars(r)
ownerBech32 := r.URL.Query().Get(types.RestOwner) ownerBech32 := vars[types.RestOwner]
collateralDenom := r.URL.Query().Get(types.RestCollateralDenom) collateralDenom := vars[types.RestCollateralDenom]
priceString := r.URL.Query().Get(types.RestUnderCollateralizedAt)
// Construct querier params
querierParams := types.QueryCdpsParams{}
if len(ownerBech32) != 0 {
owner, err := sdk.AccAddressFromBech32(ownerBech32) owner, err := sdk.AccAddressFromBech32(ownerBech32)
if err != nil { if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return return
} }
querierParams.Owner = owner
}
if len(collateralDenom) != 0 { params := types.NewQueryCdpParams(owner, collateralDenom)
// TODO validate denom
querierParams.CollateralDenom = collateralDenom
}
if len(priceString) != 0 { bz, err := cliCtx.Codec.MarshalJSON(params)
price, err := sdk.NewDecFromStr(priceString)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
querierParams.UnderCollateralizedAt = price
}
querierParamsBz, err := cliCtx.Codec.MarshalJSON(querierParams)
if err != nil { if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return return
} }
// Get the CDPs res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s", types.QueryGetCdp), bz)
res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/cdp/%s", types.QueryGetCdps), querierParamsBz) if err != nil {
rest.WriteErrorResponse(w, http.StatusNotFound, err.Error())
return
}
cliCtx = cliCtx.WithHeight(height)
rest.PostProcessResponse(w, cliCtx, res)
}
}
func queryCdpsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
collateralDenom := vars[types.RestCollateralDenom]
params := types.NewQueryCdpsParams(collateralDenom)
bz, err := cliCtx.Codec.MarshalJSON(params)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s", types.QueryGetCdps), bz)
if err != nil {
rest.WriteErrorResponse(w, http.StatusNotFound, err.Error())
return
}
cliCtx = cliCtx.WithHeight(height)
rest.PostProcessResponse(w, cliCtx, res)
}
}
func queryCdpsByRatioHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
collateralDenom := vars[types.RestCollateralDenom]
ratioStr := vars[types.RestRatio]
ratioDec, sdkError := sdk.NewDecFromStr(ratioStr)
if sdkError != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, sdkError.Error())
return
}
params := types.NewQueryCdpsByRatioParams(collateralDenom, ratioDec)
bz, err := cliCtx.Codec.MarshalJSON(params)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s", types.QueryGetCdps), bz)
if err != nil { if err != nil {
rest.WriteErrorResponse(w, http.StatusNotFound, err.Error()) rest.WriteErrorResponse(w, http.StatusNotFound, err.Error())
return return

View File

@ -4,6 +4,8 @@ import (
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/cosmos/cosmos-sdk/client/context" "github.com/cosmos/cosmos-sdk/client/context"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/rest"
) )
// RegisterRoutes - Central function to define routes that get registered by the main application // RegisterRoutes - Central function to define routes that get registered by the main application
@ -11,3 +13,43 @@ func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router) {
registerQueryRoutes(cliCtx, r) registerQueryRoutes(cliCtx, r)
registerTxRoutes(cliCtx, r) registerTxRoutes(cliCtx, r)
} }
// PostCdpReq defines the properties of cdp request's body.
type PostCdpReq struct {
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
Owner sdk.AccAddress `json:"owner" yaml:"owner"`
Collateral sdk.Coins `json:"collateral" yaml:"collateral"`
Principal sdk.Coins `json:"principal" yaml:"principal"`
}
// PostDepositReq defines the properties of cdp request's body.
type PostDepositReq struct {
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
Owner sdk.AccAddress `json:"owner" yaml:"owner"`
Depositor sdk.AccAddress `json:"depositor" yaml:"depositor"`
Collateral sdk.Coins `json:"collateral" yaml:"collateral"`
}
// PostWithdrawalReq defines the properties of cdp request's body.
type PostWithdrawalReq struct {
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
Owner sdk.AccAddress `json:"owner" yaml:"owner"`
Depositor sdk.AccAddress `json:"depositor" yaml:"depositor"`
Collateral sdk.Coins `json:"collateral" yaml:"collateral"`
}
// PostDrawReq defines the properties of cdp request's body.
type PostDrawReq struct {
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
Owner sdk.AccAddress `json:"owner" yaml:"owner"`
Denom string `json:"denom" yaml:"denom"`
Principal sdk.Coins `json:"principal" yaml:"principal"`
}
// PostRepayReq defines the properties of cdp request's body.
type PostRepayReq struct {
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
Owner sdk.AccAddress `json:"owner" yaml:"owner"`
Denom string `json:"denom" yaml:"denom"`
Payment sdk.Coins `json:"principal" yaml:"principal"`
}

View File

@ -1,7 +1,6 @@
package rest package rest
import ( import (
"fmt"
"net/http" "net/http"
"github.com/gorilla/mux" "github.com/gorilla/mux"
@ -15,13 +14,18 @@ import (
) )
func registerTxRoutes(cliCtx context.CLIContext, r *mux.Router) { func registerTxRoutes(cliCtx context.CLIContext, r *mux.Router) {
r.HandleFunc("/cdps", modifyCdpHandlerFn(cliCtx)).Methods("PUT") r.HandleFunc("/cdp", postCdpHandlerFn(cliCtx)).Methods("PUT")
r.HandleFunc("/cdp/{owner}/{denom}/deposits", postDepositHandlerFn(cliCtx)).Methods("PUT")
r.HandleFunc("/cdp/{owner}/{denom}/withdraw", postWithdrawHandlerFn(cliCtx)).Methods("PUT")
r.HandleFunc("/cdp/{owner}/{denom}/draw", postDrawHandlerFn(cliCtx)).Methods("PUT")
r.HandleFunc("/cdp/{owner}/{denom}/wipe", postRepayHandlerFn(cliCtx)).Methods("PUT")
} }
func modifyCdpHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { func postCdpHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
// Decode PUT request body // Decode PUT request body
var requestBody types.ModifyCdpRequestBody var requestBody PostCdpReq
if !rest.ReadRESTReq(w, r, cliCtx.Codec, &requestBody) { if !rest.ReadRESTReq(w, r, cliCtx.Codec, &requestBody) {
return return
} }
@ -30,39 +34,99 @@ func modifyCdpHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return return
} }
// Get the stored CDP
querierParams := types.QueryCdpsParams{
Owner: requestBody.Cdp.Owner,
CollateralDenom: requestBody.Cdp.CollateralDenom,
}
querierParamsBz, err := cliCtx.Codec.MarshalJSON(querierParams)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/cdp/%s", types.QueryGetCdps), querierParamsBz)
if err != nil {
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
cliCtx = cliCtx.WithHeight(height)
var cdps types.CDPs
err = cliCtx.Codec.UnmarshalJSON(res, &cdps)
if len(cdps) != 1 || err != nil {
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
// Calculate CDP updates
collateralDelta := requestBody.Cdp.CollateralAmount.Sub(cdps[0].CollateralAmount)
debtDelta := requestBody.Cdp.Debt.Sub(cdps[0].Debt)
// Create and return msg // Create and return msg
msg := types.NewMsgCreateOrModifyCDP( msg := types.NewMsgCreateCDP(
requestBody.Cdp.Owner, requestBody.Owner,
requestBody.Cdp.CollateralDenom, requestBody.Collateral,
collateralDelta, requestBody.Principal,
debtDelta, )
utils.WriteGenerateStdTxResponse(w, cliCtx, requestBody.BaseReq, []sdk.Msg{msg})
}
}
func postDepositHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Decode PUT request body
var requestBody PostDepositReq
if !rest.ReadRESTReq(w, r, cliCtx.Codec, &requestBody) {
return
}
requestBody.BaseReq = requestBody.BaseReq.Sanitize()
if !requestBody.BaseReq.ValidateBasic(w) {
return
}
// Create and return msg
msg := types.NewMsgDeposit(
requestBody.Owner,
requestBody.Depositor,
requestBody.Collateral,
)
utils.WriteGenerateStdTxResponse(w, cliCtx, requestBody.BaseReq, []sdk.Msg{msg})
}
}
func postWithdrawHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Decode PUT request body
var requestBody PostWithdrawalReq
if !rest.ReadRESTReq(w, r, cliCtx.Codec, &requestBody) {
return
}
requestBody.BaseReq = requestBody.BaseReq.Sanitize()
if !requestBody.BaseReq.ValidateBasic(w) {
return
}
// Create and return msg
msg := types.NewMsgWithdraw(
requestBody.Owner,
requestBody.Depositor,
requestBody.Collateral,
)
utils.WriteGenerateStdTxResponse(w, cliCtx, requestBody.BaseReq, []sdk.Msg{msg})
}
}
func postDrawHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Decode PUT request body
var requestBody PostDrawReq
if !rest.ReadRESTReq(w, r, cliCtx.Codec, &requestBody) {
return
}
requestBody.BaseReq = requestBody.BaseReq.Sanitize()
if !requestBody.BaseReq.ValidateBasic(w) {
return
}
// Create and return msg
msg := types.NewMsgDrawDebt(
requestBody.Owner,
requestBody.Denom,
requestBody.Principal,
)
utils.WriteGenerateStdTxResponse(w, cliCtx, requestBody.BaseReq, []sdk.Msg{msg})
}
}
func postRepayHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Decode PUT request body
var requestBody PostRepayReq
if !rest.ReadRESTReq(w, r, cliCtx.Codec, &requestBody) {
return
}
requestBody.BaseReq = requestBody.BaseReq.Sanitize()
if !requestBody.BaseReq.ValidateBasic(w) {
return
}
// Create and return msg
msg := types.NewMsgRepayDebt(
requestBody.Owner,
requestBody.Denom,
requestBody.Payment,
) )
utils.WriteGenerateStdTxResponse(w, cliCtx, requestBody.BaseReq, []sdk.Msg{msg}) utils.WriteGenerateStdTxResponse(w, cliCtx, requestBody.BaseReq, []sdk.Msg{msg})
} }

View File

@ -6,42 +6,77 @@ import (
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
) )
func InitGenesis(ctx sdk.Context, k Keeper, pk PricefeedKeeper, data GenesisState) { // InitGenesis sets initial genesis state for cdp module
// validate denoms - check that any collaterals in the CdpParams are in the pricefeed, pricefeed needs to initgenesis before cdp func InitGenesis(ctx sdk.Context, k Keeper, pk PricefeedKeeper, gs GenesisState) {
collateralMap := make(map[string]int) if err := gs.Validate(); err != nil {
ap := pk.GetParams(ctx) panic(fmt.Sprintf("failed to validate %s genesis state: %s", ModuleName, err))
for _, m := range ap.Markets {
collateralMap[m.MarketID] = 1
} }
for _, col := range data.Params.CollateralParams { // validate denoms - check that any collaterals in the params are in the pricefeed,
_, found := collateralMap[col.Denom] // pricefeed MUST call InitGenesis before cdp
collateralMap := make(map[string]int)
ap := pk.GetParams(ctx)
for _, a := range ap.Markets {
collateralMap[a.MarketID] = 1
}
for _, col := range gs.Params.CollateralParams {
_, found := collateralMap[col.MarketID]
if !found { if !found {
panic(fmt.Sprintf("%s collateral not found in pricefeed", col.Denom)) panic(fmt.Sprintf("%s collateral not found in pricefeed", col.Denom))
} }
} }
k.SetParams(ctx, data.Params) k.SetParams(ctx, gs.Params)
for _, cdp := range data.CDPs { // set the per second fee rate for each collateral type
for _, cp := range gs.Params.CollateralParams {
for _, dp := range gs.Params.DebtParams {
k.SetTotalPrincipal(ctx, cp.Denom, dp.Denom, sdk.ZeroInt())
}
k.SetFeeRate(ctx, cp.Denom, cp.StabilityFee)
}
// add cdps
for _, cdp := range gs.CDPs {
if cdp.ID == gs.StartingCdpID {
panic(fmt.Sprintf("starting cdp id is assigned to an existing cdp: %s", cdp))
}
k.SetCDP(ctx, cdp) k.SetCDP(ctx, cdp)
k.IndexCdpByOwner(ctx, cdp)
ratio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees))
k.IndexCdpByCollateralRatio(ctx, cdp.Collateral[0].Denom, cdp.ID, ratio)
k.IncrementTotalPrincipal(ctx, cdp.Collateral[0].Denom, cdp.Principal)
} }
k.SetGlobalDebt(ctx, data.GlobalDebt) k.SetNextCdpID(ctx, gs.StartingCdpID)
k.SetDebtDenom(ctx, gs.DebtDenom)
for _, d := range gs.Deposits {
k.SetDeposit(ctx, d)
}
// only set the previous block time if it's different than default
if !gs.PreviousBlockTime.Equal(DefaultPreviousBlockTime) {
k.SetPreviousBlockTime(ctx, gs.PreviousBlockTime)
}
} }
// ExportGenesis export genesis state for cdp module
func ExportGenesis(ctx sdk.Context, k Keeper) GenesisState { func ExportGenesis(ctx sdk.Context, k Keeper) GenesisState {
params := k.GetParams(ctx) params := k.GetParams(ctx)
cdps, err := k.GetCDPs(ctx, "", sdk.Dec{}) cdps := k.GetAllCdps(ctx)
if err != nil { cdpID := k.GetNextCdpID(ctx)
panic(err) previousBlockTime, found := k.GetPreviousBlockTime(ctx)
if !found {
previousBlockTime = DefaultPreviousBlockTime
} }
debt := k.GetGlobalDebt(ctx) debtDenom := k.GetDebtDenom(ctx)
return GenesisState{ return GenesisState{
Params: params, Params: params,
GlobalDebt: debt, StartingCdpID: cdpID,
CDPs: cdps, CDPs: cdps,
PreviousBlockTime: previousBlockTime,
DebtDenom: debtDenom,
} }
} }

61
x/cdp/genesis_test.go Normal file
View File

@ -0,0 +1,61 @@
package cdp_test
import (
"testing"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/cdp"
"github.com/stretchr/testify/suite"
)
type GenesisTestSuite struct {
suite.Suite
ctx sdk.Context
keeper cdp.Keeper
}
func (suite *GenesisTestSuite) TestInvalidGenState() {
tApp := app.NewTestApp()
for _, gs := range badGenStates() {
appGS := app.GenesisState{"cdp": cdp.ModuleCdc.MustMarshalJSON(gs.Genesis)}
suite.Panics(func() {
tApp.InitializeFromGenesisStates(
NewPricefeedGenStateMulti(),
appGS,
)
}, gs.Reason)
}
}
func (suite *GenesisTestSuite) TestValidGenState() {
tApp := app.NewTestApp()
suite.NotPanics(func() {
tApp.InitializeFromGenesisStates(
NewPricefeedGenStateMulti(),
NewCDPGenStateMulti(),
)
})
cdpGS := NewCDPGenStateMulti()
gs := cdp.GenesisState{}
cdp.ModuleCdc.UnmarshalJSON(cdpGS["cdp"], &gs)
gs.CDPs = cdps()
gs.StartingCdpID = uint64(5)
appGS := app.GenesisState{"cdp": cdp.ModuleCdc.MustMarshalJSON(gs)}
suite.NotPanics(func() {
tApp.InitializeFromGenesisStates(
NewPricefeedGenStateMulti(),
appGS,
)
})
}
func TestGenesisTestSuite(t *testing.T) {
suite.Run(t, new(GenesisTestSuite))
}

View File

@ -6,25 +6,108 @@ import (
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
) )
// Handle all cdp messages. // NewHandler creates an sdk.Handler for cdp messages
func NewHandler(keeper Keeper) sdk.Handler { func NewHandler(k Keeper) sdk.Handler {
return func(ctx sdk.Context, msg sdk.Msg) sdk.Result { return func(ctx sdk.Context, msg sdk.Msg) sdk.Result {
switch msg := msg.(type) { switch msg := msg.(type) {
case MsgCreateOrModifyCDP: case MsgCreateCDP:
return handleMsgCreateOrModifyCDP(ctx, keeper, msg) return handleMsgCreateCDP(ctx, k, msg)
case MsgDeposit:
return handleMsgDeposit(ctx, k, msg)
case MsgWithdraw:
return handleMsgWithdraw(ctx, k, msg)
case MsgDrawDebt:
return handleMsgDrawDebt(ctx, k, msg)
case MsgRepayDebt:
return handleMsgRepayDebt(ctx, k, msg)
default: default:
errMsg := fmt.Sprintf("Unrecognized cdp msg type: %T", msg) errMsg := fmt.Sprintf("unrecognized cdp msg type: %T", msg)
return sdk.ErrUnknownRequest(errMsg).Result() return sdk.ErrUnknownRequest(errMsg).Result()
} }
} }
} }
func handleMsgCreateOrModifyCDP(ctx sdk.Context, keeper Keeper, msg MsgCreateOrModifyCDP) sdk.Result { func handleMsgCreateCDP(ctx sdk.Context, k Keeper, msg MsgCreateCDP) sdk.Result {
err := k.AddCdp(ctx, msg.Sender, msg.Collateral, msg.Principal)
err := keeper.ModifyCDP(ctx, msg.Sender, msg.CollateralDenom, msg.CollateralChange, msg.DebtChange)
if err != nil { if err != nil {
return err.Result() return err.Result()
} }
return sdk.Result{} ctx.EventManager().EmitEvent(
sdk.NewEvent(
sdk.EventTypeMessage,
sdk.NewAttribute(sdk.AttributeKeyModule, AttributeValueCategory),
sdk.NewAttribute(sdk.AttributeKeySender, msg.Sender.String()),
),
)
id, _ := k.GetCdpID(ctx, msg.Sender, msg.Collateral[0].Denom)
return sdk.Result{
Data: GetCdpIDBytes(id),
Events: ctx.EventManager().Events(),
}
}
func handleMsgDeposit(ctx sdk.Context, k Keeper, msg MsgDeposit) sdk.Result {
err := k.DepositCollateral(ctx, msg.Owner, msg.Depositor, msg.Collateral)
if err != nil {
return err.Result()
}
ctx.EventManager().EmitEvent(
sdk.NewEvent(
sdk.EventTypeMessage,
sdk.NewAttribute(sdk.AttributeKeyModule, AttributeValueCategory),
sdk.NewAttribute(sdk.AttributeKeySender, msg.Depositor.String()),
),
)
return sdk.Result{Events: ctx.EventManager().Events()}
}
func handleMsgWithdraw(ctx sdk.Context, k Keeper, msg MsgWithdraw) sdk.Result {
err := k.WithdrawCollateral(ctx, msg.Owner, msg.Depositor, msg.Collateral)
if err != nil {
return err.Result()
}
ctx.EventManager().EmitEvent(
sdk.NewEvent(
sdk.EventTypeMessage,
sdk.NewAttribute(sdk.AttributeKeyModule, AttributeValueCategory),
sdk.NewAttribute(sdk.AttributeKeySender, msg.Depositor.String()),
),
)
return sdk.Result{Events: ctx.EventManager().Events()}
}
func handleMsgDrawDebt(ctx sdk.Context, k Keeper, msg MsgDrawDebt) sdk.Result {
err := k.AddPrincipal(ctx, msg.Sender, msg.CdpDenom, msg.Principal)
if err != nil {
return err.Result()
}
ctx.EventManager().EmitEvent(
sdk.NewEvent(
sdk.EventTypeMessage,
sdk.NewAttribute(sdk.AttributeKeyModule, AttributeValueCategory),
sdk.NewAttribute(sdk.AttributeKeySender, msg.Sender.String()),
),
)
return sdk.Result{Events: ctx.EventManager().Events()}
}
func handleMsgRepayDebt(ctx sdk.Context, k Keeper, msg MsgRepayDebt) sdk.Result {
err := k.RepayPrincipal(ctx, msg.Sender, msg.CdpDenom, msg.Payment)
if err != nil {
return err.Result()
}
ctx.EventManager().EmitEvent(
sdk.NewEvent(
sdk.EventTypeMessage,
sdk.NewAttribute(sdk.AttributeKeyModule, AttributeValueCategory),
sdk.NewAttribute(sdk.AttributeKeySender, msg.Sender.String()),
),
)
return sdk.Result{Events: ctx.EventManager().Events()}
} }

64
x/cdp/handler_test.go Normal file
View File

@ -0,0 +1,64 @@
package cdp_test
import (
"strings"
"testing"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/cdp"
abci "github.com/tendermint/tendermint/abci/types"
tmtime "github.com/tendermint/tendermint/types/time"
"github.com/stretchr/testify/suite"
)
type HandlerTestSuite struct {
suite.Suite
ctx sdk.Context
app app.TestApp
handler sdk.Handler
keeper cdp.Keeper
}
func (suite *HandlerTestSuite) SetupTest() {
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
tApp.InitializeFromGenesisStates(
NewPricefeedGenStateMulti(),
NewCDPGenStateMulti(),
)
keeper := tApp.GetCDPKeeper()
suite.handler = cdp.NewHandler(keeper)
suite.app = tApp
suite.keeper = keeper
suite.ctx = ctx
}
func (suite *HandlerTestSuite) TestMsgCreateCdp() {
_, addrs := app.GeneratePrivKeyAddressPairs(1)
ak := suite.app.GetAccountKeeper()
acc := ak.NewAccountWithAddress(suite.ctx, addrs[0])
acc.SetCoins(cs(c("xrp", 200000000), c("btc", 500000000)))
ak.SetAccount(suite.ctx, acc)
msg := cdp.NewMsgCreateCDP(
addrs[0],
cs(c("xrp", 200000000)),
cs(c("usdx", 10000000)),
)
res := suite.handler(suite.ctx, msg)
suite.True(res.IsOK())
suite.Equal(cdp.GetCdpIDBytes(uint64(1)), res.Data)
}
func (suite *HandlerTestSuite) TestInvalidMsg() {
res := suite.handler(suite.ctx, sdk.NewTestMsg())
suite.False(res.IsOK())
suite.True(strings.Contains(res.Log, "unrecognized cdp msg type"))
}
func TestHandlerTestSuite(t *testing.T) {
suite.Run(t, new(HandlerTestSuite))
}

View File

@ -8,26 +8,25 @@ import (
"github.com/kava-labs/kava/app" "github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/cdp" "github.com/kava-labs/kava/x/cdp"
"github.com/kava-labs/kava/x/pricefeed" "github.com/kava-labs/kava/x/pricefeed"
tmtime "github.com/tendermint/tendermint/types/time"
) )
// Avoid cluttering test cases with long function name // Avoid cluttering test cases with long function names
func i(in int64) sdk.Int { return sdk.NewInt(in) } func i(in int64) sdk.Int { return sdk.NewInt(in) }
func d(str string) sdk.Dec { return sdk.MustNewDecFromStr(str) } func d(str string) sdk.Dec { return sdk.MustNewDecFromStr(str) }
func c(denom string, amount int64) sdk.Coin { return sdk.NewInt64Coin(denom, amount) } func c(denom string, amount int64) sdk.Coin { return sdk.NewInt64Coin(denom, amount) }
func cs(coins ...sdk.Coin) sdk.Coins { return sdk.NewCoins(coins...) } func cs(coins ...sdk.Coin) sdk.Coins { return sdk.NewCoins(coins...) }
func NewPFGenState(asset string, price sdk.Dec) app.GenesisState { func NewPricefeedGenState(asset string, price sdk.Dec) app.GenesisState {
quote := "usd"
ap := pricefeed.Params{
Markets: []pricefeed.Market{
pricefeed.Market{MarketID: asset, BaseAsset: asset, QuoteAsset: quote, Oracles: pricefeed.Oracles{}, Active: true},
},
}
pfGenesis := pricefeed.GenesisState{ pfGenesis := pricefeed.GenesisState{
Params: ap, Params: pricefeed.Params{
Markets: []pricefeed.Market{
pricefeed.Market{MarketID: asset + ":usd", BaseAsset: asset, QuoteAsset: "usd", Oracles: pricefeed.Oracles{}, Active: true},
},
},
PostedPrices: []pricefeed.PostedPrice{ PostedPrices: []pricefeed.PostedPrice{
pricefeed.PostedPrice{ pricefeed.PostedPrice{
MarketID: asset, MarketID: asset + ":usd",
OracleAddress: sdk.AccAddress{}, OracleAddress: sdk.AccAddress{},
Price: price, Price: price,
Expiry: time.Now().Add(1 * time.Hour), Expiry: time.Now().Add(1 * time.Hour),
@ -39,18 +38,219 @@ func NewPFGenState(asset string, price sdk.Dec) app.GenesisState {
func NewCDPGenState(asset string, liquidationRatio sdk.Dec) app.GenesisState { func NewCDPGenState(asset string, liquidationRatio sdk.Dec) app.GenesisState {
cdpGenesis := cdp.GenesisState{ cdpGenesis := cdp.GenesisState{
Params: cdp.CdpParams{ Params: cdp.Params{
GlobalDebtLimit: sdk.NewInt(1000000), GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000)),
CollateralParams: []cdp.CollateralParams{ CollateralParams: cdp.CollateralParams{
{ {
Denom: asset, Denom: asset,
LiquidationRatio: liquidationRatio, LiquidationRatio: liquidationRatio,
DebtLimit: sdk.NewInt(500000), DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000)),
StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), // %5 apr
Prefix: 0x20,
ConversionFactor: i(6),
MarketID: asset + ":usd",
},
},
DebtParams: cdp.DebtParams{
{
Denom: "usdx",
ReferenceAsset: "usd",
DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000)),
ConversionFactor: i(6),
DebtFloor: i(10000000),
}, },
}, },
}, },
GlobalDebt: sdk.ZeroInt(), StartingCdpID: cdp.DefaultCdpStartingID,
DebtDenom: cdp.DefaultDebtDenom,
CDPs: cdp.CDPs{}, CDPs: cdp.CDPs{},
PreviousBlockTime: cdp.DefaultPreviousBlockTime,
} }
return app.GenesisState{cdp.ModuleName: cdp.ModuleCdc.MustMarshalJSON(cdpGenesis)} return app.GenesisState{cdp.ModuleName: cdp.ModuleCdc.MustMarshalJSON(cdpGenesis)}
} }
func NewPricefeedGenStateMulti() app.GenesisState {
pfGenesis := pricefeed.GenesisState{
Params: pricefeed.Params{
Markets: []pricefeed.Market{
pricefeed.Market{MarketID: "btc:usd", BaseAsset: "btc", QuoteAsset: "usd", Oracles: pricefeed.Oracles{}, Active: true},
pricefeed.Market{MarketID: "xrp:usd", BaseAsset: "xrp", QuoteAsset: "usd", Oracles: pricefeed.Oracles{}, Active: true},
},
},
PostedPrices: []pricefeed.PostedPrice{
pricefeed.PostedPrice{
MarketID: "btc:usd",
OracleAddress: sdk.AccAddress{},
Price: sdk.MustNewDecFromStr("8000.00"),
Expiry: time.Now().Add(1 * time.Hour),
},
pricefeed.PostedPrice{
MarketID: "xrp:usd",
OracleAddress: sdk.AccAddress{},
Price: sdk.MustNewDecFromStr("0.25"),
Expiry: time.Now().Add(1 * time.Hour),
},
},
}
return app.GenesisState{pricefeed.ModuleName: pricefeed.ModuleCdc.MustMarshalJSON(pfGenesis)}
}
func NewCDPGenStateMulti() app.GenesisState {
cdpGenesis := cdp.GenesisState{
Params: cdp.Params{
GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000), sdk.NewInt64Coin("susd", 1000000000000)),
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: "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),
},
},
DebtParams: cdp.DebtParams{
{
Denom: "usdx",
ReferenceAsset: "usd",
DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000)),
ConversionFactor: i(6),
DebtFloor: i(10000000),
},
{
Denom: "susd",
ReferenceAsset: "usd",
DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("susd", 1000000000000)),
ConversionFactor: i(6),
DebtFloor: i(10000000),
},
},
},
StartingCdpID: cdp.DefaultCdpStartingID,
DebtDenom: cdp.DefaultDebtDenom,
CDPs: cdp.CDPs{},
PreviousBlockTime: cdp.DefaultPreviousBlockTime,
}
return app.GenesisState{cdp.ModuleName: cdp.ModuleCdc.MustMarshalJSON(cdpGenesis)}
}
func cdps() (cdps cdp.CDPs) {
_, addrs := app.GeneratePrivKeyAddressPairs(3)
c1 := cdp.NewCDP(uint64(1), addrs[0], sdk.NewCoins(sdk.NewCoin("xrp", sdk.NewInt(10000000))), sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(8000000))), tmtime.Canonical(time.Now()))
c2 := cdp.NewCDP(uint64(2), addrs[1], sdk.NewCoins(sdk.NewCoin("xrp", sdk.NewInt(100000000))), sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(10000000))), tmtime.Canonical(time.Now()))
c3 := cdp.NewCDP(uint64(3), addrs[1], sdk.NewCoins(sdk.NewCoin("btc", sdk.NewInt(1000000000))), sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(10000000))), tmtime.Canonical(time.Now()))
c4 := cdp.NewCDP(uint64(4), addrs[2], sdk.NewCoins(sdk.NewCoin("xrp", sdk.NewInt(1000000000))), sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(500000000))), tmtime.Canonical(time.Now()))
cdps = append(cdps, c1, c2, c3, c4)
return
}
type badGenState struct {
Genesis cdp.GenesisState
Reason string
}
func badGenStates() []badGenState {
g1 := baseGenState()
g1.Params.CollateralParams[0].Denom = "btc"
g2 := baseGenState()
g2.Params.CollateralParams[0].Prefix = 0x21
g3 := baseGenState()
g3.Params.CollateralParams[0].DebtLimit = sdk.NewCoins(sdk.NewInt64Coin("usdx", 500000000000), sdk.NewInt64Coin("lol", 500000000000))
g4 := baseGenState()
g4.Params.CollateralParams[0].DebtLimit = sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000001))
g5 := baseGenState()
g5.Params.CollateralParams[0].DebtLimit = sdk.NewCoins(sdk.NewInt64Coin("usdx", 500000000001), sdk.NewInt64Coin("susd", 500000000000))
g6 := baseGenState()
g6.Params.DebtParams[0].Denom = "susd"
g7 := baseGenState()
g7.Params.DebtParams[0].DebtLimit = sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000001))
g8 := baseGenState()
g8.Params.DebtParams = append(g8.Params.DebtParams, cdp.DebtParam{
Denom: "lol",
ReferenceAsset: "usd",
DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("lol", 1000000000000)),
})
g9 := baseGenState()
g9.DebtDenom = ""
g10 := baseGenState()
g10.PreviousBlockTime = time.Time{}
return []badGenState{
badGenState{Genesis: g1, Reason: "duplicate collateral denom"},
badGenState{Genesis: g2, Reason: "duplicate collateral prefix"},
badGenState{Genesis: g3, Reason: "duplicate collateral prefix"},
badGenState{Genesis: g4, Reason: "single collateral exceeds debt limit"},
badGenState{Genesis: g5, Reason: "combined collateral exceeds debt limit"},
badGenState{Genesis: g6, Reason: "duplicate debt denom"},
badGenState{Genesis: g7, Reason: "debt limit exceeds global debt limit"},
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"},
}
}
func baseGenState() cdp.GenesisState {
return cdp.GenesisState{
Params: cdp.Params{
GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000), sdk.NewInt64Coin("susd", 1000000000000)),
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: "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),
},
},
DebtParams: cdp.DebtParams{
{
Denom: "usdx",
ReferenceAsset: "usd",
DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000)),
ConversionFactor: i(6),
DebtFloor: i(10000000),
},
{
Denom: "susd",
ReferenceAsset: "usd",
DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("susd", 1000000000000)),
ConversionFactor: i(6),
DebtFloor: i(10000000),
},
},
},
StartingCdpID: cdp.DefaultCdpStartingID,
DebtDenom: cdp.DefaultDebtDenom,
CDPs: cdp.CDPs{},
PreviousBlockTime: cdp.DefaultPreviousBlockTime,
}
}

View File

@ -1,60 +0,0 @@
package keeper_test
import (
"testing"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/require"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/kava-labs/kava/app"
keep "github.com/kava-labs/kava/x/cdp/keeper"
)
// Test the bank functionality of the CDP keeper
func TestKeeper_AddSubtractGetCoins(t *testing.T) {
_, addrs := app.GeneratePrivKeyAddressPairs(1)
normalAddr := addrs[0]
tests := []struct {
name string
address sdk.AccAddress
shouldAdd bool
amount sdk.Coins
expectedCoins sdk.Coins
}{
{"addNormalAddress", normalAddr, true, cs(c("usdx", 53)), cs(c("usdx", 153), c("kava", 100))},
{"subNormalAddress", normalAddr, false, cs(c("usdx", 53)), cs(c("usdx", 47), c("kava", 100))},
{"addLiquidatorStable", keep.LiquidatorAccountAddress, true, cs(c("usdx", 53)), cs(c("usdx", 153))},
{"subLiquidatorStable", keep.LiquidatorAccountAddress, false, cs(c("usdx", 53)), cs(c("usdx", 47))},
{"addLiquidatorGov", keep.LiquidatorAccountAddress, true, cs(c("kava", 53)), cs(c("usdx", 100))}, // no change to balance
{"subLiquidatorGov", keep.LiquidatorAccountAddress, false, cs(c("kava", 53)), cs(c("usdx", 100))}, // no change to balance
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// setup app with an account
tApp := app.NewTestApp()
tApp.InitializeFromGenesisStates(
app.NewAuthGenState([]sdk.AccAddress{normalAddr}, []sdk.Coins{cs(c("usdx", 100), c("kava", 100))}),
)
// create a new context and setup the liquidator account
ctx := tApp.NewContext(false, abci.Header{})
keeper := tApp.GetCDPKeeper()
keeper.SetLiquidatorModuleAccount(ctx, keep.LiquidatorModuleAccount{cs(c("usdx", 100))}) // set gov coin "balance" to zero
// perform the test action
var err sdk.Error
if tc.shouldAdd {
_, err = keeper.AddCoins(ctx, tc.address, tc.amount)
} else {
_, err = keeper.SubtractCoins(ctx, tc.address, tc.amount)
}
// check balances are as expected
require.NoError(t, err)
require.Equal(t, tc.expectedCoins, keeper.GetCoins(ctx, tc.address))
})
}
}

421
x/cdp/keeper/cdp.go Normal file
View File

@ -0,0 +1,421 @@
package keeper
import (
"bytes"
"fmt"
"github.com/cosmos/cosmos-sdk/store/prefix"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/cdp/types"
)
// AddCdp adds a cdp for a specific owner and collateral type
func (k Keeper) AddCdp(ctx sdk.Context, owner sdk.AccAddress, collateral sdk.Coins, principal sdk.Coins) sdk.Error {
// validation
err := k.ValidateCollateral(ctx, collateral)
if err != nil {
return err
}
_, found := k.GetCdpByOwnerAndDenom(ctx, owner, collateral[0].Denom)
if found {
return types.ErrCdpAlreadyExists(k.codespace, owner, collateral[0].Denom)
}
err = k.ValidatePrincipalAdd(ctx, principal)
if err != nil {
return err
}
err = k.ValidateCollateralizationRatio(ctx, collateral, principal, sdk.NewCoins())
if err != nil {
return err
}
// send coins from the owners account to the cdp module
id := k.GetNextCdpID(ctx)
cdp := types.NewCDP(id, owner, collateral, principal, ctx.BlockHeader().Time)
deposit := types.NewDeposit(cdp.ID, owner, collateral)
err = k.supplyKeeper.SendCoinsFromAccountToModule(ctx, owner, types.ModuleName, collateral)
if err != nil {
return err
}
// mint the principal and send to the owners account
err = k.supplyKeeper.MintCoins(ctx, types.ModuleName, principal)
if err != nil {
panic(err)
}
err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, owner, principal)
if err != nil {
panic(err)
}
// mint the corresponding amount of debt coins
err = k.MintDebtCoins(ctx, types.ModuleName, k.GetDebtDenom(ctx), principal)
if err != nil {
panic(err)
}
// emit events for cdp creation, deposit, and draw
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeCreateCdp,
sdk.NewAttribute(types.AttributeKeyCdpID, fmt.Sprintf("%d", cdp.ID)),
),
)
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeCdpDeposit,
sdk.NewAttribute(sdk.AttributeKeyAmount, collateral.String()),
sdk.NewAttribute(types.AttributeKeyCdpID, fmt.Sprintf("%d", cdp.ID)),
),
)
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeCdpDraw,
sdk.NewAttribute(sdk.AttributeKeyAmount, principal.String()),
sdk.NewAttribute(types.AttributeKeyCdpID, fmt.Sprintf("%d", cdp.ID)),
),
)
// update total principal for input collateral type
k.IncrementTotalPrincipal(ctx, collateral[0].Denom, principal)
// set the cdp, deposit, and indexes in the store
collateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, collateral, principal)
k.SetCdpAndCollateralRatioIndex(ctx, cdp, collateralToDebtRatio)
k.IndexCdpByOwner(ctx, cdp)
k.SetDeposit(ctx, deposit)
k.SetNextCdpID(ctx, id+1)
return nil
}
// SetCdpAndCollateralRatioIndex sets the cdp and collateral ratio index in the store
func (k Keeper) SetCdpAndCollateralRatioIndex(ctx sdk.Context, cdp types.CDP, ratio sdk.Dec) {
k.SetCDP(ctx, cdp)
k.IndexCdpByCollateralRatio(ctx, cdp.Collateral[0].Denom, cdp.ID, ratio)
}
// MintDebtCoins mints debt coins in the cdp module account
func (k Keeper) MintDebtCoins(ctx sdk.Context, moduleAccount string, denom string, principalCoins sdk.Coins) sdk.Error {
coinsToMint := sdk.NewCoins()
for _, sc := range principalCoins {
coinsToMint = coinsToMint.Add(sdk.NewCoins(sdk.NewCoin(denom, sc.Amount)))
}
err := k.supplyKeeper.MintCoins(ctx, moduleAccount, coinsToMint)
if err != nil {
return err
}
return nil
}
// BurnDebtCoins burns debts coins from the cdp module account
func (k Keeper) BurnDebtCoins(ctx sdk.Context, moduleAccount string, denom string, paymentCoins sdk.Coins) sdk.Error {
coinsToBurn := sdk.NewCoins()
for _, pc := range paymentCoins {
coinsToBurn = coinsToBurn.Add(sdk.NewCoins(sdk.NewCoin(denom, pc.Amount)))
}
err := k.supplyKeeper.BurnCoins(ctx, moduleAccount, coinsToBurn)
if err != nil {
return err
}
return nil
}
// GetCdpID returns the id of the cdp corresponding to a specific owner and collateral denom
func (k Keeper) GetCdpID(ctx sdk.Context, owner sdk.AccAddress, denom string) (uint64, bool) {
cdpIDs, found := k.GetCdpIdsByOwner(ctx, owner)
if !found {
return 0, false
}
for _, id := range cdpIDs {
_, found = k.GetCDP(ctx, denom, id)
if found {
return id, true
}
}
return 0, false
}
// GetCdpIdsByOwner returns all the ids of cdps corresponding to a particular owner
func (k Keeper) GetCdpIdsByOwner(ctx sdk.Context, owner sdk.AccAddress) ([]uint64, bool) {
store := prefix.NewStore(ctx.KVStore(k.key), types.CdpIDKeyPrefix)
bz := store.Get(owner)
// TODO figure out why this is necessary
if bz == nil || bytes.Equal(bz, []byte{0}) {
return []uint64{}, false
}
var cdpIDs []uint64
k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &cdpIDs)
return cdpIDs, true
}
// GetCdpByOwnerAndDenom queries cdps owned by owner and returns the cdp with matching denom
func (k Keeper) GetCdpByOwnerAndDenom(ctx sdk.Context, owner sdk.AccAddress, denom string) (types.CDP, bool) {
cdpIDs, found := k.GetCdpIdsByOwner(ctx, owner)
if !found {
return types.CDP{}, false
}
for _, id := range cdpIDs {
cdp, found := k.GetCDP(ctx, denom, id)
if found {
return cdp, true
}
}
return types.CDP{}, false
}
// GetCDP returns the cdp associated with a particular collateral denom and id
func (k Keeper) GetCDP(ctx sdk.Context, collateralDenom string, cdpID uint64) (types.CDP, bool) {
// get store
store := prefix.NewStore(ctx.KVStore(k.key), types.CdpKeyPrefix)
db, _ := k.GetDenomPrefix(ctx, collateralDenom)
// get CDP
bz := store.Get(types.CdpKey(db, cdpID))
// unmarshal
if bz == nil {
return types.CDP{}, false
}
var cdp types.CDP
k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &cdp)
return cdp, true
}
// SetCDP sets a cdp in the store
func (k Keeper) SetCDP(ctx sdk.Context, cdp types.CDP) {
store := prefix.NewStore(ctx.KVStore(k.key), types.CdpKeyPrefix)
db, _ := k.GetDenomPrefix(ctx, cdp.Collateral[0].Denom)
bz := k.cdc.MustMarshalBinaryLengthPrefixed(cdp)
store.Set(types.CdpKey(db, cdp.ID), bz)
return
}
// DeleteCDP deletes a cdp from the store
func (k Keeper) DeleteCDP(ctx sdk.Context, cdp types.CDP) {
store := prefix.NewStore(ctx.KVStore(k.key), types.CdpKeyPrefix)
db, _ := k.GetDenomPrefix(ctx, cdp.Collateral[0].Denom)
store.Delete(types.CdpKey(db, cdp.ID))
}
// GetAllCdps returns all cdps from the store
func (k Keeper) GetAllCdps(ctx sdk.Context) (cdps types.CDPs) {
k.IterateAllCdps(ctx, func(cdp types.CDP) bool {
cdps = append(cdps, cdp)
return false
})
return
}
// GetAllCdpsByDenom returns all cdps of a particular collateral type from the store
func (k Keeper) GetAllCdpsByDenom(ctx sdk.Context, denom string) (cdps types.CDPs) {
k.IterateCdpsByDenom(ctx, denom, func(cdp types.CDP) bool {
cdps = append(cdps, cdp)
return false
})
return
}
// GetAllCdpsByDenomAndRatio returns all cdps of a particular collateral type and below a certain collateralization ratio
func (k Keeper) GetAllCdpsByDenomAndRatio(ctx sdk.Context, denom string, targetRatio sdk.Dec) (cdps types.CDPs) {
k.IterateCdpsByCollateralRatio(ctx, denom, targetRatio, func(cdp types.CDP) bool {
cdps = append(cdps, cdp)
return false
})
return
}
// SetNextCdpID sets the highest cdp id in the store
func (k Keeper) SetNextCdpID(ctx sdk.Context, id uint64) {
store := prefix.NewStore(ctx.KVStore(k.key), types.CdpIDKey)
store.Set([]byte{}, types.GetCdpIDBytes(id))
}
// GetNextCdpID returns the highest cdp id from the store
func (k Keeper) GetNextCdpID(ctx sdk.Context) (id uint64) {
store := prefix.NewStore(ctx.KVStore(k.key), types.CdpIDKey)
bz := store.Get([]byte{})
if bz == nil {
panic("starting cdp id not set in genesis")
}
id = types.GetCdpIDFromBytes(bz)
return
}
// IndexCdpByOwner sets the cdp id in the store, indexed by the owner
func (k Keeper) IndexCdpByOwner(ctx sdk.Context, cdp types.CDP) {
store := prefix.NewStore(ctx.KVStore(k.key), types.CdpIDKeyPrefix)
cdpIDs, found := k.GetCdpIdsByOwner(ctx, cdp.Owner)
if !found {
idBytes := k.cdc.MustMarshalBinaryLengthPrefixed([]uint64{cdp.ID})
store.Set(cdp.Owner, idBytes)
return
}
for _, id := range cdpIDs {
if id == cdp.ID {
return
}
cdpIDs = append(cdpIDs, cdp.ID)
store.Set(cdp.Owner, k.cdc.MustMarshalBinaryLengthPrefixed(cdpIDs))
}
}
// RemoveCdpOwnerIndex deletes the cdp id from the store's index of cdps by owner
func (k Keeper) RemoveCdpOwnerIndex(ctx sdk.Context, cdp types.CDP) {
store := prefix.NewStore(ctx.KVStore(k.key), types.CdpIDKeyPrefix)
cdpIDs, found := k.GetCdpIdsByOwner(ctx, cdp.Owner)
if !found {
return
}
updatedCdpIds := []uint64{}
for _, id := range cdpIDs {
if id != cdp.ID {
updatedCdpIds = append(updatedCdpIds, id)
}
}
if len(updatedCdpIds) == 0 {
store.Delete(cdp.Owner)
}
store.Set(cdp.Owner, k.cdc.MustMarshalBinaryLengthPrefixed(updatedCdpIds))
}
// IndexCdpByCollateralRatio sets the cdp id in the store, indexed by the collateral type and collateral to debt ratio
func (k Keeper) IndexCdpByCollateralRatio(ctx sdk.Context, denom string, id uint64, collateralRatio sdk.Dec) {
store := prefix.NewStore(ctx.KVStore(k.key), types.CollateralRatioIndexPrefix)
db, _ := k.GetDenomPrefix(ctx, denom)
store.Set(types.CollateralRatioKey(db, id, collateralRatio), types.GetCdpIDBytes(id))
}
// RemoveCdpCollateralRatioIndex deletes the cdp id from the store's index of cdps by collateral type and collateral to debt ratio
func (k Keeper) RemoveCdpCollateralRatioIndex(ctx sdk.Context, denom string, id uint64, collateralRatio sdk.Dec) {
store := prefix.NewStore(ctx.KVStore(k.key), types.CollateralRatioIndexPrefix)
db, _ := k.GetDenomPrefix(ctx, denom)
store.Delete(types.CollateralRatioKey(db, id, collateralRatio))
}
// GetDebtDenom returns the denom of debt in the system
func (k Keeper) GetDebtDenom(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 == "" {
panic("debt denom not set in genesis")
}
store := prefix.NewStore(ctx.KVStore(k.key), types.DebtDenomKey)
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 {
return types.ErrInvalidCollateralLength(k.codespace, len(collateral))
}
_, found := k.GetCollateral(ctx, collateral[0].Denom)
if !found {
return types.ErrCollateralNotSupported(k.codespace, collateral[0].Denom)
}
return nil
}
// ValidatePrincipalAdd validates that an asset is valid for use as debt when creating a new cdp
func (k Keeper) ValidatePrincipalAdd(ctx sdk.Context, principal sdk.Coins) sdk.Error {
for _, dc := range principal {
dp, found := k.GetDebt(ctx, dc.Denom)
if !found {
return types.ErrDebtNotSupported(k.codespace, dc.Denom)
}
if sdk.NewCoins(dc).IsAnyGT(dp.DebtLimit) {
return types.ErrExceedsDebtLimit(k.codespace, sdk.NewCoins(dc), dp.DebtLimit)
}
if dc.Amount.LT(dp.DebtFloor) {
return types.ErrBelowDebtFloor(k.codespace, sdk.NewCoins(dc), dp.DebtFloor)
}
}
return nil
}
// ValidatePrincipalDraw validates that an asset is valid for use as debt when drawing debt off an existing cdp
func (k Keeper) ValidatePrincipalDraw(ctx sdk.Context, principal sdk.Coins) sdk.Error {
for _, dc := range principal {
dp, found := k.GetDebt(ctx, dc.Denom)
if !found {
return types.ErrDebtNotSupported(k.codespace, dc.Denom)
}
if sdk.NewCoins(dc).IsAnyGT(dp.DebtLimit) {
return types.ErrExceedsDebtLimit(k.codespace, sdk.NewCoins(dc), dp.DebtLimit)
}
}
return nil
}
// ValidateCollateralizationRatio validate that adding the input principal doesn't put the cdp below the liquidation ratio
func (k Keeper) ValidateCollateralizationRatio(ctx sdk.Context, collateral sdk.Coins, principal sdk.Coins, fees sdk.Coins) sdk.Error {
//
collateralizationRatio, err := k.CalculateCollateralizationRatio(ctx, collateral, principal, fees)
if err != nil {
return err
}
liquidationRatio := k.getLiquidationRatio(ctx, collateral[0].Denom)
if collateralizationRatio.LT(liquidationRatio) {
return types.ErrInvalidCollateralRatio(k.codespace, collateral[0].Denom, collateralizationRatio, liquidationRatio)
}
return nil
}
// CalculateCollateralToDebtRatio returns the collateral to debt ratio of the input collateral and debt amounts
func (k Keeper) CalculateCollateralToDebtRatio(ctx sdk.Context, collateral sdk.Coins, debt sdk.Coins) sdk.Dec {
debtTotal := sdk.ZeroDec()
for _, dc := range debt {
debtBaseUnits := k.convertDebtToBaseUnits(ctx, dc)
debtTotal = debtTotal.Add(debtBaseUnits)
}
if debtTotal.IsZero() || debtTotal.GTE(types.MaxSortableDec) {
return types.MaxSortableDec.Sub(sdk.SmallestDec())
}
collateralBaseUnits := k.convertCollateralToBaseUnits(ctx, collateral[0])
return collateralBaseUnits.Quo(debtTotal)
}
// CalculateCollateralizationRatio returns the collateralization ratio of the input collateral to the input debt plus fees
func (k Keeper) CalculateCollateralizationRatio(ctx sdk.Context, collateral sdk.Coins, principal sdk.Coins, fees sdk.Coins) (sdk.Dec, sdk.Error) {
marketID := k.getMarketID(ctx, collateral[0].Denom)
price, err := k.pricefeedKeeper.GetCurrentPrice(ctx, marketID)
if err != nil {
return sdk.Dec{}, err
}
collateralBaseUnits := k.convertCollateralToBaseUnits(ctx, collateral[0])
collateralValue := collateralBaseUnits.Mul(price.Price)
principalTotal := sdk.ZeroDec()
for _, pc := range principal {
prinicpalBaseUnits := k.convertDebtToBaseUnits(ctx, pc)
principalTotal = principalTotal.Add(prinicpalBaseUnits)
}
for _, fc := range fees {
feeBaseUnits := k.convertDebtToBaseUnits(ctx, fc)
principalTotal = principalTotal.Add(feeBaseUnits)
}
collateralRatio := collateralValue.Quo(principalTotal)
return collateralRatio, nil
}
// converts the input collateral to base units (ie multiplies the input by 10^(-ConversionFactor))
func (k Keeper) convertCollateralToBaseUnits(ctx sdk.Context, collateral sdk.Coin) (baseUnits sdk.Dec) {
cp, _ := k.GetCollateral(ctx, collateral.Denom)
return sdk.NewDecFromInt(collateral.Amount).Mul(sdk.NewDecFromIntWithPrec(sdk.OneInt(), cp.ConversionFactor.Int64()))
}
// converts the input debt to base units (ie multiplies the input by 10^(-ConversionFactor))
func (k Keeper) convertDebtToBaseUnits(ctx sdk.Context, debt sdk.Coin) (baseUnits sdk.Dec) {
dp, _ := k.GetDebt(ctx, debt.Denom)
return sdk.NewDecFromInt(debt.Amount).Mul(sdk.NewDecFromIntWithPrec(sdk.OneInt(), dp.ConversionFactor.Int64()))
}

299
x/cdp/keeper/cdp_test.go Normal file
View File

@ -0,0 +1,299 @@
package keeper_test
import (
"testing"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/app"
"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 CdpTestSuite struct {
suite.Suite
keeper keeper.Keeper
app app.TestApp
ctx sdk.Context
}
func (suite *CdpTestSuite) 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 *CdpTestSuite) TestAddCdp() {
_, addrs := app.GeneratePrivKeyAddressPairs(1)
ak := suite.app.GetAccountKeeper()
acc := ak.NewAccountWithAddress(suite.ctx, addrs[0])
acc.SetCoins(cs(c("xrp", 200000000), c("btc", 500000000)))
ak.SetAccount(suite.ctx, acc)
err := suite.keeper.AddCdp(suite.ctx, addrs[0], cs(c("xrp", 200000000)), cs(c("usdx", 26000000)))
suite.Equal(types.CodeInvalidCollateralRatio, err.Result().Code)
err = suite.keeper.AddCdp(suite.ctx, addrs[0], cs(c("xrp", 500000000)), cs(c("usdx", 26000000)))
suite.Error(err)
err = suite.keeper.AddCdp(suite.ctx, addrs[0], cs(c("xrp", 200000000)), cs(c("xusd", 10000000)))
suite.Equal(types.CodeDebtNotSupported, err.Result().Code)
ctx := suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(time.Hour * 2))
pk := suite.app.GetPriceFeedKeeper()
_ = pk.SetCurrentPrices(ctx, "xrp:usd")
err = suite.keeper.AddCdp(ctx, addrs[0], cs(c("xrp", 100000000)), cs(c("usdx", 10000000)))
suite.Error(err)
_ = pk.SetCurrentPrices(suite.ctx, "xrp:usd")
err = suite.keeper.AddCdp(suite.ctx, addrs[0], cs(c("xrp", 100000000)), cs(c("usdx", 10000000)))
suite.NoError(err)
id := suite.keeper.GetNextCdpID(suite.ctx)
suite.Equal(uint64(2), id)
tp := suite.keeper.GetTotalPrincipal(suite.ctx, "xrp", "usdx")
suite.Equal(i(10000000), tp)
sk := suite.app.GetSupplyKeeper()
macc := sk.GetModuleAccount(suite.ctx, types.ModuleName)
suite.Equal(cs(c("debt", 10000000), c("xrp", 100000000)), macc.GetCoins())
acc = ak.GetAccount(suite.ctx, addrs[0])
suite.Equal(cs(c("usdx", 10000000), c("xrp", 100000000), c("btc", 500000000)), acc.GetCoins())
err = suite.keeper.AddCdp(suite.ctx, addrs[0], cs(c("btc", 500000000)), cs(c("usdx", 26667000000)))
suite.Equal(types.CodeInvalidCollateralRatio, err.Result().Code)
err = suite.keeper.AddCdp(suite.ctx, addrs[0], cs(c("btc", 500000000)), cs(c("usdx", 100000000)))
suite.NoError(err)
id = suite.keeper.GetNextCdpID(suite.ctx)
suite.Equal(uint64(3), id)
tp = suite.keeper.GetTotalPrincipal(suite.ctx, "btc", "usdx")
suite.Equal(i(100000000), tp)
macc = sk.GetModuleAccount(suite.ctx, types.ModuleName)
suite.Equal(cs(c("debt", 110000000), c("xrp", 100000000), c("btc", 500000000)), macc.GetCoins())
acc = ak.GetAccount(suite.ctx, addrs[0])
suite.Equal(cs(c("usdx", 110000000), c("xrp", 100000000)), acc.GetCoins())
err = suite.keeper.AddCdp(suite.ctx, addrs[0], cs(c("lol", 100)), cs(c("usdx", 10)))
suite.Equal(types.CodeCollateralNotSupported, err.Result().Code)
err = suite.keeper.AddCdp(suite.ctx, addrs[0], cs(c("xrp", 100)), cs(c("usdx", 10)))
suite.Equal(types.CodeCdpAlreadyExists, err.Result().Code)
}
func (suite *CdpTestSuite) TestGetSetDenomByte() {
_, found := suite.keeper.GetDenomPrefix(suite.ctx, "lol")
suite.False(found)
db, found := suite.keeper.GetDenomPrefix(suite.ctx, "xrp")
suite.True(found)
suite.Equal(byte(0x20), db)
}
func (suite *CdpTestSuite) TestGetDebtDenom() {
suite.Panics(func() { suite.keeper.SetDebtDenom(suite.ctx, "") })
t := suite.keeper.GetDebtDenom(suite.ctx)
suite.Equal("debt", t)
suite.keeper.SetDebtDenom(suite.ctx, "lol")
t = suite.keeper.GetDebtDenom(suite.ctx)
suite.Equal("lol", t)
}
func (suite *CdpTestSuite) TestGetNextCdpID() {
id := suite.keeper.GetNextCdpID(suite.ctx)
suite.Equal(types.DefaultCdpStartingID, id)
}
func (suite *CdpTestSuite) TestGetSetCdp() {
_, addrs := app.GeneratePrivKeyAddressPairs(1)
cdp := types.NewCDP(types.DefaultCdpStartingID, addrs[0], cs(c("xrp", 1)), cs(c("usdx", 1)), tmtime.Canonical(time.Now()))
suite.keeper.SetCDP(suite.ctx, cdp)
t, found := suite.keeper.GetCDP(suite.ctx, "xrp", types.DefaultCdpStartingID)
suite.True(found)
suite.Equal(cdp, t)
_, found = suite.keeper.GetCDP(suite.ctx, "xrp", uint64(2))
suite.False(found)
suite.keeper.DeleteCDP(suite.ctx, cdp)
_, found = suite.keeper.GetCDP(suite.ctx, "btc", types.DefaultCdpStartingID)
suite.False(found)
}
func (suite *CdpTestSuite) TestGetSetCdpId() {
_, addrs := app.GeneratePrivKeyAddressPairs(2)
cdp := types.NewCDP(types.DefaultCdpStartingID, addrs[0], cs(c("xrp", 1)), cs(c("usdx", 1)), tmtime.Canonical(time.Now()))
suite.keeper.SetCDP(suite.ctx, cdp)
suite.keeper.IndexCdpByOwner(suite.ctx, cdp)
id, found := suite.keeper.GetCdpID(suite.ctx, addrs[0], "xrp")
suite.True(found)
suite.Equal(types.DefaultCdpStartingID, id)
_, found = suite.keeper.GetCdpID(suite.ctx, addrs[0], "lol")
suite.False(found)
_, found = suite.keeper.GetCdpID(suite.ctx, addrs[1], "xrp")
suite.False(found)
}
func (suite *CdpTestSuite) TestGetSetCdpByOwnerAndDenom() {
_, addrs := app.GeneratePrivKeyAddressPairs(2)
cdp := types.NewCDP(types.DefaultCdpStartingID, addrs[0], cs(c("xrp", 1)), cs(c("usdx", 1)), tmtime.Canonical(time.Now()))
suite.keeper.SetCDP(suite.ctx, cdp)
suite.keeper.IndexCdpByOwner(suite.ctx, cdp)
t, found := suite.keeper.GetCdpByOwnerAndDenom(suite.ctx, addrs[0], "xrp")
suite.True(found)
suite.Equal(cdp, t)
_, found = suite.keeper.GetCdpByOwnerAndDenom(suite.ctx, addrs[0], "lol")
suite.False(found)
_, found = suite.keeper.GetCdpByOwnerAndDenom(suite.ctx, addrs[1], "xrp")
suite.False(found)
suite.NotPanics(func() { suite.keeper.IndexCdpByOwner(suite.ctx, cdp) })
}
func (suite *CdpTestSuite) TestCalculateCollateralToDebtRatio() {
_, addrs := app.GeneratePrivKeyAddressPairs(1)
cdp := types.NewCDP(types.DefaultCdpStartingID, addrs[0], cs(c("xrp", 3)), cs(c("usdx", 1)), tmtime.Canonical(time.Now()))
cr := suite.keeper.CalculateCollateralToDebtRatio(suite.ctx, cdp.Collateral, cdp.Principal)
suite.Equal(sdk.MustNewDecFromStr("3.0"), cr)
cdp = types.NewCDP(types.DefaultCdpStartingID, addrs[0], cs(c("xrp", 1)), cs(c("usdx", 2)), tmtime.Canonical(time.Now()))
cr = suite.keeper.CalculateCollateralToDebtRatio(suite.ctx, cdp.Collateral, cdp.Principal)
suite.Equal(sdk.MustNewDecFromStr("0.5"), cr)
cdp = types.NewCDP(types.DefaultCdpStartingID, addrs[0], cs(c("xrp", 3)), cs(c("usdx", 1), c("susd", 2)), tmtime.Canonical(time.Now()))
cr = suite.keeper.CalculateCollateralToDebtRatio(suite.ctx, cdp.Collateral, cdp.Principal)
suite.Equal(sdk.MustNewDecFromStr("1"), cr)
}
func (suite *CdpTestSuite) TestSetCdpByCollateralRatio() {
_, addrs := app.GeneratePrivKeyAddressPairs(1)
cdp := types.NewCDP(types.DefaultCdpStartingID, addrs[0], cs(c("xrp", 3)), cs(c("usdx", 1)), tmtime.Canonical(time.Now()))
cr := suite.keeper.CalculateCollateralToDebtRatio(suite.ctx, cdp.Collateral, cdp.Principal)
suite.NotPanics(func() { suite.keeper.IndexCdpByCollateralRatio(suite.ctx, cdp.Collateral[0].Denom, cdp.ID, cr) })
}
func (suite *CdpTestSuite) TestIterateCdps() {
cdps := cdps()
for _, c := range cdps {
suite.keeper.SetCDP(suite.ctx, c)
suite.keeper.IndexCdpByOwner(suite.ctx, c)
cr := suite.keeper.CalculateCollateralToDebtRatio(suite.ctx, c.Collateral, c.Principal)
suite.keeper.IndexCdpByCollateralRatio(suite.ctx, c.Collateral[0].Denom, c.ID, cr)
}
t := suite.keeper.GetAllCdps(suite.ctx)
suite.Equal(4, len(t))
}
func (suite *CdpTestSuite) TestIterateCdpsByDenom() {
cdps := cdps()
for _, c := range cdps {
suite.keeper.SetCDP(suite.ctx, c)
suite.keeper.IndexCdpByOwner(suite.ctx, c)
cr := suite.keeper.CalculateCollateralToDebtRatio(suite.ctx, c.Collateral, c.Principal)
suite.keeper.IndexCdpByCollateralRatio(suite.ctx, c.Collateral[0].Denom, c.ID, cr)
}
xrpCdps := suite.keeper.GetAllCdpsByDenom(suite.ctx, "xrp")
suite.Equal(3, len(xrpCdps))
btcCdps := suite.keeper.GetAllCdpsByDenom(suite.ctx, "btc")
suite.Equal(1, len(btcCdps))
suite.keeper.DeleteCDP(suite.ctx, cdps[0])
suite.keeper.RemoveCdpOwnerIndex(suite.ctx, cdps[0])
xrpCdps = suite.keeper.GetAllCdpsByDenom(suite.ctx, "xrp")
suite.Equal(2, len(xrpCdps))
suite.keeper.DeleteCDP(suite.ctx, cdps[1])
suite.keeper.RemoveCdpOwnerIndex(suite.ctx, cdps[1])
ids, found := suite.keeper.GetCdpIdsByOwner(suite.ctx, cdps[1].Owner)
suite.True(found)
suite.Equal(1, len(ids))
suite.Equal(uint64(3), ids[0])
}
func (suite *CdpTestSuite) TestIterateCdpsByCollateralRatio() {
cdps := cdps()
for _, c := range cdps {
suite.keeper.SetCDP(suite.ctx, c)
suite.keeper.IndexCdpByOwner(suite.ctx, c)
cr := suite.keeper.CalculateCollateralToDebtRatio(suite.ctx, c.Collateral, c.Principal)
suite.keeper.IndexCdpByCollateralRatio(suite.ctx, c.Collateral[0].Denom, c.ID, cr)
}
xrpCdps := suite.keeper.GetAllCdpsByDenomAndRatio(suite.ctx, "xrp", d("1.25"))
suite.Equal(0, len(xrpCdps))
xrpCdps = suite.keeper.GetAllCdpsByDenomAndRatio(suite.ctx, "xrp", d("1.25").Add(sdk.SmallestDec()))
suite.Equal(1, len(xrpCdps))
xrpCdps = suite.keeper.GetAllCdpsByDenomAndRatio(suite.ctx, "xrp", d("2.0").Add(sdk.SmallestDec()))
suite.Equal(2, len(xrpCdps))
xrpCdps = suite.keeper.GetAllCdpsByDenomAndRatio(suite.ctx, "xrp", d("100.0").Add(sdk.SmallestDec()))
suite.Equal(3, len(xrpCdps))
suite.keeper.DeleteCDP(suite.ctx, cdps[0])
suite.keeper.RemoveCdpOwnerIndex(suite.ctx, cdps[0])
cr := suite.keeper.CalculateCollateralToDebtRatio(suite.ctx, cdps[0].Collateral, cdps[0].Principal)
suite.keeper.RemoveCdpCollateralRatioIndex(suite.ctx, cdps[0].Collateral[0].Denom, cdps[0].ID, cr)
xrpCdps = suite.keeper.GetAllCdpsByDenomAndRatio(suite.ctx, "xrp", d("2.0").Add(sdk.SmallestDec()))
suite.Equal(1, len(xrpCdps))
}
func (suite *CdpTestSuite) TestValidateCollateral() {
c := sdk.NewCoins(sdk.NewCoin("xrp", sdk.NewInt(1)))
err := suite.keeper.ValidateCollateral(suite.ctx, c)
suite.NoError(err)
c = sdk.NewCoins(sdk.NewCoin("lol", sdk.NewInt(1)))
err = suite.keeper.ValidateCollateral(suite.ctx, c)
suite.Equal(types.CodeCollateralNotSupported, err.Result().Code)
c = sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(1)), sdk.NewCoin("xrp", sdk.NewInt(1)))
err = suite.keeper.ValidateCollateral(suite.ctx, c)
suite.Equal(types.CodeCollateralLengthInvalid, err.Result().Code)
}
func (suite *CdpTestSuite) TestValidatePrincipal() {
d := sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(10000000)))
err := suite.keeper.ValidatePrincipalAdd(suite.ctx, d)
suite.NoError(err)
d = sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(10000000)), sdk.NewCoin("susd", sdk.NewInt(10000000)))
err = suite.keeper.ValidatePrincipalAdd(suite.ctx, d)
suite.NoError(err)
d = sdk.NewCoins(sdk.NewCoin("xusd", sdk.NewInt(1)))
err = suite.keeper.ValidatePrincipalAdd(suite.ctx, d)
suite.Equal(types.CodeDebtNotSupported, err.Result().Code)
d = sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(1000000000001)))
err = suite.keeper.ValidatePrincipalAdd(suite.ctx, d)
suite.Equal(types.CodeExceedsDebtLimit, err.Result().Code)
}
func (suite *CdpTestSuite) TestCalculateCollateralizationRatio() {
c := cdps()[1]
suite.keeper.SetCDP(suite.ctx, c)
suite.keeper.IndexCdpByOwner(suite.ctx, c)
cr := suite.keeper.CalculateCollateralToDebtRatio(suite.ctx, c.Collateral, c.Principal)
suite.keeper.IndexCdpByCollateralRatio(suite.ctx, c.Collateral[0].Denom, c.ID, cr)
cr, err := suite.keeper.CalculateCollateralizationRatio(suite.ctx, c.Collateral, c.Principal, c.AccumulatedFees)
suite.NoError(err)
suite.Equal(d("2.5"), cr)
c.AccumulatedFees = sdk.NewCoins(sdk.NewCoin("usdx", i(10000000)))
cr, err = suite.keeper.CalculateCollateralizationRatio(suite.ctx, c.Collateral, c.Principal, c.AccumulatedFees)
suite.NoError(err)
suite.Equal(d("1.25"), cr)
}
func (suite *CdpTestSuite) TestMintBurnDebtCoins() {
cd := cdps()[1]
err := suite.keeper.MintDebtCoins(suite.ctx, types.ModuleName, suite.keeper.GetDebtDenom(suite.ctx), cd.Principal)
suite.NoError(err)
err = suite.keeper.MintDebtCoins(suite.ctx, "notamodule", suite.keeper.GetDebtDenom(suite.ctx), cd.Principal)
suite.Error(err)
sk := suite.app.GetSupplyKeeper()
acc := sk.GetModuleAccount(suite.ctx, types.ModuleName)
suite.Equal(cs(c("debt", 10000000)), acc.GetCoins())
err = suite.keeper.BurnDebtCoins(suite.ctx, types.ModuleName, suite.keeper.GetDebtDenom(suite.ctx), cd.Principal)
suite.NoError(err)
err = suite.keeper.BurnDebtCoins(suite.ctx, "notamodule", suite.keeper.GetDebtDenom(suite.ctx), cd.Principal)
suite.Error(err)
sk = suite.app.GetSupplyKeeper()
acc = sk.GetModuleAccount(suite.ctx, types.ModuleName)
suite.Equal(sdk.Coins(nil), acc.GetCoins())
}
func TestCdpTestSuite(t *testing.T) {
suite.Run(t, new(CdpTestSuite))
}

223
x/cdp/keeper/deposit.go Normal file
View File

@ -0,0 +1,223 @@
package keeper
import (
"fmt"
"github.com/cosmos/cosmos-sdk/store/prefix"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/cdp/types"
)
// DepositCollateral adds collateral to a cdp
func (k Keeper) DepositCollateral(ctx sdk.Context, owner sdk.AccAddress, depositor sdk.AccAddress, collateral sdk.Coins) sdk.Error {
err := k.ValidateCollateral(ctx, collateral)
if err != nil {
return err
}
cdp, found := k.GetCdpByOwnerAndDenom(ctx, owner, collateral[0].Denom)
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)
if found {
deposit.Amount = deposit.Amount.Add(collateral)
} else {
deposit = types.NewDeposit(cdp.ID, depositor, collateral)
}
err = k.supplyKeeper.SendCoinsFromAccountToModule(ctx, depositor, types.ModuleName, collateral)
if err != nil {
return err
}
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeCdpDeposit,
sdk.NewAttribute(sdk.AttributeKeyAmount, collateral.String()),
sdk.NewAttribute(types.AttributeKeyCdpID, fmt.Sprintf("%d", cdp.ID)),
),
)
k.SetDeposit(ctx, deposit)
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)
oldCollateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees))
k.RemoveCdpCollateralRatioIndex(ctx, cdp.Collateral[0].Denom, cdp.ID, oldCollateralToDebtRatio)
cdp.AccumulatedFees = cdp.AccumulatedFees.Add(fees)
cdp.FeesUpdated = ctx.BlockTime()
cdp.Collateral = cdp.Collateral.Add(collateral)
collateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, collateral, cdp.Principal.Add(cdp.AccumulatedFees))
k.SetCdpAndCollateralRatioIndex(ctx, cdp, collateralToDebtRatio)
return nil
}
// WithdrawCollateral removes collateral from a cdp if it does not put the cdp below the liquidation ratio
func (k Keeper) WithdrawCollateral(ctx sdk.Context, owner sdk.AccAddress, depositor sdk.AccAddress, collateral sdk.Coins) sdk.Error {
err := k.ValidateCollateral(ctx, collateral)
if err != nil {
return err
}
cdp, found := k.GetCdpByOwnerAndDenom(ctx, owner, collateral[0].Denom)
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)
if !found {
return types.ErrDepositNotFound(k.codespace, depositor, cdp.ID)
}
if collateral.IsAnyGT(deposit.Amount) {
return types.ErrInvalidWithdrawAmount(k.codespace, collateral, deposit.Amount)
}
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)
collateralizationRatio, err := k.CalculateCollateralizationRatio(ctx, cdp.Collateral.Sub(collateral), cdp.Principal, cdp.AccumulatedFees.Add(fees))
if err != nil {
return err
}
liquidationRatio := k.getLiquidationRatio(ctx, collateral[0].Denom)
if collateralizationRatio.LT(liquidationRatio) {
return types.ErrInvalidCollateralRatio(k.codespace, collateral[0].Denom, collateralizationRatio, liquidationRatio)
}
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeCdpWithdrawal,
sdk.NewAttribute(sdk.AttributeKeyAmount, collateral.String()),
sdk.NewAttribute(types.AttributeKeyCdpID, fmt.Sprintf("%d", cdp.ID)),
),
)
err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, depositor, collateral)
if err != nil {
panic(err)
}
oldCollateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees))
k.RemoveCdpCollateralRatioIndex(ctx, cdp.Collateral[0].Denom, cdp.ID, oldCollateralToDebtRatio)
cdp.AccumulatedFees = cdp.AccumulatedFees.Add(fees)
cdp.FeesUpdated = ctx.BlockTime()
cdp.Collateral = cdp.Collateral.Sub(collateral)
collateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, collateral, cdp.Principal.Add(cdp.AccumulatedFees))
k.SetCdpAndCollateralRatioIndex(ctx, cdp, collateralToDebtRatio)
deposit.Amount = deposit.Amount.Sub(collateral)
if deposit.Amount.IsZero() {
k.DeleteDeposit(ctx, types.StatusNil, 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) {
store := prefix.NewStore(ctx.KVStore(k.key), types.DepositKeyPrefix)
bz := store.Get(types.DepositKey(status, cdpID, depositor))
if bz == nil {
return deposit, false
}
k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &deposit)
return deposit, true
}
// SetDeposit sets the deposit in the store
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)
}
// DeleteDeposit deletes a deposit from the store
func (k Keeper) DeleteDeposit(ctx sdk.Context, status types.DepositStatus, cdpID uint64, depositor sdk.AccAddress) {
store := prefix.NewStore(ctx.KVStore(k.key), types.DepositKeyPrefix)
store.Delete(types.DepositKey(status, 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))
defer iterator.Close()
for ; iterator.Valid(); iterator.Next() {
var deposit types.Deposit
k.cdc.MustUnmarshalBinaryLengthPrefixed(iterator.Value(), &deposit)
if cb(deposit) {
break
}
}
}
// GetDeposits returns all the deposits to a cdp
func (k Keeper) GetDeposits(ctx sdk.Context, cdpID uint64) (deposits types.Deposits) {
k.IterateDeposits(ctx, cdpID, func(deposit types.Deposit) bool {
deposits = append(deposits, deposit)
return false
})
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

@ -0,0 +1,154 @@
package keeper_test
import (
"testing"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/app"
"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 DepositTestSuite struct {
suite.Suite
keeper keeper.Keeper
app app.TestApp
ctx sdk.Context
addrs []sdk.AccAddress
}
func (suite *DepositTestSuite) SetupTest() {
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
_, addrs := app.GeneratePrivKeyAddressPairs(10)
authGS := app.NewAuthGenState(
addrs[0:2],
[]sdk.Coins{
cs(c("xrp", 500000000), c("btc", 500000000)),
cs(c("xrp", 200000000))})
tApp.InitializeFromGenesisStates(
authGS,
NewPricefeedGenStateMulti(),
NewCDPGenStateMulti(),
)
keeper := tApp.GetCDPKeeper()
suite.app = tApp
suite.keeper = keeper
suite.ctx = ctx
suite.addrs = addrs
err := suite.keeper.AddCdp(suite.ctx, addrs[0], cs(c("xrp", 400000000)), cs(c("usdx", 10000000)))
suite.NoError(err)
}
func (suite *DepositTestSuite) TestGetSetDeposit() {
d, found := suite.keeper.GetDeposit(suite.ctx, types.StatusNil, 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.False(found)
ds = suite.keeper.GetDeposits(suite.ctx, uint64(1))
suite.Equal(0, len(ds))
}
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])
suite.True(found)
td := types.NewDeposit(uint64(1), suite.addrs[0], cs(c("xrp", 410000000)))
suite.True(d.Equals(td))
ds := suite.keeper.GetDeposits(suite.ctx, uint64(1))
suite.Equal(1, len(ds))
suite.True(ds[0].Equals(td))
cd, _ := suite.keeper.GetCDP(suite.ctx, "xrp", uint64(1))
suite.Equal(cs(c("xrp", 410000000)), cd.Collateral)
ak := suite.app.GetAccountKeeper()
acc := ak.GetAccount(suite.ctx, suite.addrs[0])
suite.Equal(i(90000000), acc.GetCoins().AmountOf("xrp"))
err = suite.keeper.DepositCollateral(suite.ctx, suite.addrs[0], suite.addrs[0], cs(c("btc", 1)))
suite.Equal(types.CodeCdpNotFound, err.Result().Code)
err = suite.keeper.DepositCollateral(suite.ctx, suite.addrs[1], suite.addrs[0], cs(c("xrp", 1)))
suite.Equal(types.CodeCdpNotFound, err.Result().Code)
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])
suite.True(found)
td = types.NewDeposit(uint64(1), suite.addrs[1], cs(c("xrp", 10000000)))
suite.True(d.Equals(td))
ds = suite.keeper.GetDeposits(suite.ctx, uint64(1))
suite.Equal(2, len(ds))
suite.True(ds[1].Equals(td))
}
func (suite *DepositTestSuite) TestWithdrawCollateral() {
err := suite.keeper.WithdrawCollateral(suite.ctx, suite.addrs[0], suite.addrs[0], cs(c("xrp", 321000000)))
suite.Equal(types.CodeInvalidCollateralRatio, err.Result().Code)
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)
err = suite.keeper.WithdrawCollateral(suite.ctx, suite.addrs[0], suite.addrs[0], cs(c("xrp", 320000000)))
suite.Equal(types.CodeInvalidCollateralRatio, err.Result().Code)
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])
td := types.NewDeposit(uint64(1), suite.addrs[0], cs(c("xrp", 390000000)))
suite.True(d.Equals(td))
ak := suite.app.GetAccountKeeper()
acc := ak.GetAccount(suite.ctx, suite.addrs[0])
suite.Equal(i(110000000), acc.GetCoins().AmountOf("xrp"))
err = suite.keeper.WithdrawCollateral(suite.ctx, suite.addrs[0], suite.addrs[1], cs(c("xrp", 10000000)))
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))
}

216
x/cdp/keeper/draw.go Normal file
View File

@ -0,0 +1,216 @@
package keeper
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/cdp/types"
)
// AddPrincipal adds debt to a cdp if the additional debt does not put the cdp below the liquidation ratio
func (k Keeper) AddPrincipal(ctx sdk.Context, owner sdk.AccAddress, denom string, principal sdk.Coins) sdk.Error {
// validation
cdp, found := k.GetCdpByOwnerAndDenom(ctx, owner, denom)
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)
if err != nil {
return err
}
// fee calculation
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)
err = k.ValidateCollateralizationRatio(ctx, cdp.Collateral, cdp.Principal.Add(principal), cdp.AccumulatedFees.Add(fees))
if err != nil {
return err
}
// mint the principal and send it to the cdp owner
err = k.supplyKeeper.MintCoins(ctx, types.ModuleName, principal)
if err != nil {
panic(err)
}
err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, owner, principal)
if err != nil {
panic(err)
}
// mint the corresponding amount of debt coins in the cdp module account
err = k.MintDebtCoins(ctx, types.ModuleName, k.GetDebtDenom(ctx), principal)
if err != nil {
panic(err)
}
// emit cdp draw event
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeCdpDraw,
sdk.NewAttribute(sdk.AttributeKeyAmount, principal.String()),
sdk.NewAttribute(types.AttributeKeyCdpID, fmt.Sprintf("%d", cdp.ID)),
),
)
// remove old collateral:debt index
oldCollateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees))
k.RemoveCdpCollateralRatioIndex(ctx, denom, cdp.ID, oldCollateralToDebtRatio)
// update cdp state
cdp.Principal = cdp.Principal.Add(principal)
cdp.AccumulatedFees = cdp.AccumulatedFees.Add(fees)
cdp.FeesUpdated = ctx.BlockTime()
// increment total principal for the input collateral type
k.IncrementTotalPrincipal(ctx, cdp.Collateral[0].Denom, principal)
// set cdp state and indexes in the store
collateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees))
k.SetCdpAndCollateralRatioIndex(ctx, cdp, collateralToDebtRatio)
return nil
}
// RepayPrincipal removes debt from the cdp
// If all debt is repaid, the collateral is returned to depositors and the cdp is removed from the store
func (k Keeper) RepayPrincipal(ctx sdk.Context, owner sdk.AccAddress, denom string, payment sdk.Coins) sdk.Error {
// validation
cdp, found := k.GetCdpByOwnerAndDenom(ctx, owner, denom)
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)
if err != nil {
return err
}
// calculate 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)
// calculate fee and principal payment
feePayment, principalPayment := k.calculatePayment(ctx, cdp.AccumulatedFees.Add(fees), payment)
// send the payment from the sender to the cpd module
err = k.supplyKeeper.SendCoinsFromAccountToModule(ctx, owner, types.ModuleName, payment)
if err != nil {
return err
}
// burn the payment coins
err = k.supplyKeeper.BurnCoins(ctx, types.ModuleName, payment)
if err != nil {
panic(err)
}
// burn the corresponding amount of debt coins
err = k.BurnDebtCoins(ctx, types.ModuleName, k.GetDebtDenom(ctx), payment)
if err != nil {
panic(err)
}
// emit repayment event
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeCdpRepay,
sdk.NewAttribute(sdk.AttributeKeyAmount, payment.String()),
sdk.NewAttribute(types.AttributeKeyCdpID, fmt.Sprintf("%d", cdp.ID)),
),
)
// remove the old collateral:debt ratio index
oldCollateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees))
k.RemoveCdpCollateralRatioIndex(ctx, denom, cdp.ID, oldCollateralToDebtRatio)
// update cdp state
if !principalPayment.IsZero() {
cdp.Principal = cdp.Principal.Sub(principalPayment)
}
cdp.AccumulatedFees = cdp.AccumulatedFees.Add(fees).Sub(feePayment)
cdp.FeesUpdated = ctx.BlockTime()
// decrement the total principal for the input collateral type
k.DecrementTotalPrincipal(ctx, denom, payment)
// if the debt is fully paid, return collateral to depositors,
// and remove the cdp and indexes from the store
if cdp.Principal.IsZero() && cdp.AccumulatedFees.IsZero() {
k.ReturnCollateral(ctx, cdp)
k.DeleteCDP(ctx, cdp)
k.RemoveCdpOwnerIndex(ctx, cdp)
// emit cdp close event
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeCdpClose,
sdk.NewAttribute(types.AttributeKeyCdpID, fmt.Sprintf("%d", cdp.ID)),
),
)
return nil
}
// set cdp state and update indexes
collateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees))
k.SetCdpAndCollateralRatioIndex(ctx, cdp, collateralToDebtRatio)
return nil
}
// ValidatePaymentCoins validates that the input coins are valid for repaying debt
func (k Keeper) ValidatePaymentCoins(ctx sdk.Context, cdp types.CDP, payment sdk.Coins) sdk.Error {
subset := payment.DenomsSubsetOf(cdp.Principal)
if !subset {
var paymentDenoms []string
var principalDenoms []string
for _, pc := range cdp.Principal {
principalDenoms = append(principalDenoms, pc.Denom)
}
for _, pc := range payment {
paymentDenoms = append(paymentDenoms, pc.Denom)
}
return types.ErrInvalidPaymentDenom(k.codespace, cdp.ID, principalDenoms, paymentDenoms)
}
return nil
}
// ReturnCollateral returns collateral to depositors on a cdp and removes deposits from the store
func (k Keeper) ReturnCollateral(ctx sdk.Context, cdp types.CDP) {
deposits := k.GetDeposits(ctx, cdp.ID)
for _, deposit := range deposits {
err := k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, deposit.Depositor, deposit.Amount)
if err != nil {
panic(err)
}
k.DeleteDeposit(ctx, types.StatusNil, cdp.ID, deposit.Depositor)
}
}
func (k Keeper) calculatePayment(ctx sdk.Context, fees sdk.Coins, payment sdk.Coins) (sdk.Coins, sdk.Coins) {
// divides repayment into principal and fee components, with fee payment applied first.
feePayment := sdk.NewCoins()
principalPayment := sdk.NewCoins()
if fees.IsZero() {
return sdk.NewCoins(), payment
}
for _, fc := range fees {
if payment.AmountOf(fc.Denom).IsPositive() {
if payment.AmountOf(fc.Denom).GT(fc.Amount) {
feePayment = feePayment.Add(sdk.NewCoins(fc))
pc := sdk.NewCoin(fc.Denom, payment.AmountOf(fc.Denom).Sub(fc.Amount))
principalPayment = principalPayment.Add(sdk.NewCoins(pc))
} else {
fc := sdk.NewCoin(fc.Denom, payment.AmountOf(fc.Denom))
feePayment = feePayment.Add(sdk.NewCoins(fc))
}
}
}
return feePayment, principalPayment
}

198
x/cdp/keeper/draw_test.go Normal file
View File

@ -0,0 +1,198 @@
package keeper_test
import (
"testing"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/app"
"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 DrawTestSuite struct {
suite.Suite
keeper keeper.Keeper
app app.TestApp
ctx sdk.Context
addrs []sdk.AccAddress
}
func (suite *DrawTestSuite) SetupTest() {
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
_, addrs := app.GeneratePrivKeyAddressPairs(3)
authGS := app.NewAuthGenState(
addrs,
[]sdk.Coins{
cs(c("xrp", 500000000), c("btc", 500000000)),
cs(c("xrp", 200000000)),
cs(c("xrp", 10000000000000), c("usdx", 100000000000))})
tApp.InitializeFromGenesisStates(
authGS,
NewPricefeedGenStateMulti(),
NewCDPGenStateMulti(),
)
keeper := tApp.GetCDPKeeper()
suite.app = tApp
suite.keeper = keeper
suite.ctx = ctx
suite.addrs = addrs
err := suite.keeper.AddCdp(suite.ctx, addrs[0], cs(c("xrp", 400000000)), cs(c("usdx", 10000000)))
suite.NoError(err)
}
func (suite *DrawTestSuite) TestAddRepayPrincipal() {
err := suite.keeper.AddPrincipal(suite.ctx, suite.addrs[0], "xrp", cs(c("usdx", 10000000)))
suite.NoError(err)
t, _ := suite.keeper.GetCDP(suite.ctx, "xrp", uint64(1))
suite.Equal(cs(c("usdx", 20000000)), t.Principal)
ctd := suite.keeper.CalculateCollateralToDebtRatio(suite.ctx, t.Collateral, t.Principal.Add(t.AccumulatedFees))
suite.Equal(d("20.0"), ctd)
ts := suite.keeper.GetAllCdpsByDenomAndRatio(suite.ctx, "xrp", d("20.0"))
suite.Equal(0, len(ts))
ts = suite.keeper.GetAllCdpsByDenomAndRatio(suite.ctx, "xrp", d("20.0").Add(sdk.SmallestDec()))
suite.Equal(ts[0], t)
tp := suite.keeper.GetTotalPrincipal(suite.ctx, "xrp", "usdx")
suite.Equal(i(20000000), tp)
sk := suite.app.GetSupplyKeeper()
acc := sk.GetModuleAccount(suite.ctx, types.ModuleName)
suite.Equal(cs(c("xrp", 400000000), c("debt", 20000000)), acc.GetCoins())
err = suite.keeper.AddPrincipal(suite.ctx, suite.addrs[0], "xrp", cs(c("susd", 10000000)))
suite.NoError(err)
t, _ = suite.keeper.GetCDP(suite.ctx, "xrp", uint64(1))
suite.Equal(cs(c("usdx", 20000000), c("susd", 10000000)), t.Principal)
ctd = suite.keeper.CalculateCollateralToDebtRatio(suite.ctx, t.Collateral, t.Principal.Add(t.AccumulatedFees))
suite.Equal(d("400000000").Quo(d("30000000")), ctd)
ts = suite.keeper.GetAllCdpsByDenomAndRatio(suite.ctx, "xrp", d("400").Quo(d("30")))
suite.Equal(0, len(ts))
ts = suite.keeper.GetAllCdpsByDenomAndRatio(suite.ctx, "xrp", d("400").Quo(d("30")).Add(sdk.SmallestDec()))
suite.Equal(ts[0], t)
tp = suite.keeper.GetTotalPrincipal(suite.ctx, "xrp", "susd")
suite.Equal(i(10000000), tp)
sk = suite.app.GetSupplyKeeper()
acc = sk.GetModuleAccount(suite.ctx, types.ModuleName)
suite.Equal(cs(c("xrp", 400000000), c("debt", 30000000)), acc.GetCoins())
err = suite.keeper.AddPrincipal(suite.ctx, suite.addrs[1], "xrp", cs(c("usdx", 10000000)))
suite.Equal(types.CodeCdpNotFound, err.Result().Code)
err = suite.keeper.AddPrincipal(suite.ctx, suite.addrs[0], "xrp", cs(c("xusd", 10000000)))
suite.Equal(types.CodeDebtNotSupported, err.Result().Code)
err = suite.keeper.AddPrincipal(suite.ctx, suite.addrs[0], "xrp", cs(c("usdx", 311000000)))
suite.Equal(types.CodeInvalidCollateralRatio, err.Result().Code)
err = suite.keeper.RepayPrincipal(suite.ctx, suite.addrs[0], "xrp", cs(c("usdx", 10000000)))
suite.NoError(err)
t, _ = suite.keeper.GetCDP(suite.ctx, "xrp", uint64(1))
suite.Equal(cs(c("usdx", 10000000), c("susd", 10000000)), t.Principal)
ctd = suite.keeper.CalculateCollateralToDebtRatio(suite.ctx, t.Collateral, t.Principal.Add(t.AccumulatedFees))
suite.Equal(d("20.0"), ctd)
ts = suite.keeper.GetAllCdpsByDenomAndRatio(suite.ctx, "xrp", d("20.0"))
suite.Equal(0, len(ts))
ts = suite.keeper.GetAllCdpsByDenomAndRatio(suite.ctx, "xrp", d("20.0").Add(sdk.SmallestDec()))
suite.Equal(ts[0], t)
tp = suite.keeper.GetTotalPrincipal(suite.ctx, "xrp", "usdx")
suite.Equal(i(10000000), tp)
sk = suite.app.GetSupplyKeeper()
acc = sk.GetModuleAccount(suite.ctx, types.ModuleName)
suite.Equal(cs(c("xrp", 400000000), c("debt", 20000000)), acc.GetCoins())
err = suite.keeper.RepayPrincipal(suite.ctx, suite.addrs[0], "xrp", cs(c("susd", 10000000)))
suite.NoError(err)
t, _ = suite.keeper.GetCDP(suite.ctx, "xrp", uint64(1))
suite.Equal(cs(c("usdx", 10000000)), t.Principal)
ctd = suite.keeper.CalculateCollateralToDebtRatio(suite.ctx, t.Collateral, t.Principal.Add(t.AccumulatedFees))
suite.Equal(d("40.0"), ctd)
ts = suite.keeper.GetAllCdpsByDenomAndRatio(suite.ctx, "xrp", d("40.0"))
suite.Equal(0, len(ts))
ts = suite.keeper.GetAllCdpsByDenomAndRatio(suite.ctx, "xrp", d("40.0").Add(sdk.SmallestDec()))
suite.Equal(ts[0], t)
tp = suite.keeper.GetTotalPrincipal(suite.ctx, "xrp", "susd")
suite.Equal(i(0), tp)
sk = suite.app.GetSupplyKeeper()
acc = sk.GetModuleAccount(suite.ctx, types.ModuleName)
suite.Equal(cs(c("xrp", 400000000), c("debt", 10000000)), acc.GetCoins())
err = suite.keeper.RepayPrincipal(suite.ctx, suite.addrs[0], "xrp", cs(c("xusd", 10000000)))
suite.Equal(types.CodeInvalidPaymentDenom, err.Result().Code)
err = suite.keeper.RepayPrincipal(suite.ctx, suite.addrs[1], "xrp", cs(c("xusd", 10000000)))
suite.Equal(types.CodeCdpNotFound, err.Result().Code)
err = suite.keeper.RepayPrincipal(suite.ctx, suite.addrs[0], "xrp", cs(c("usdx", 100000000)))
suite.Error(err)
err = suite.keeper.RepayPrincipal(suite.ctx, suite.addrs[0], "xrp", cs(c("usdx", 10000000)))
suite.NoError(err)
_, found := suite.keeper.GetCDP(suite.ctx, "xrp", uint64(1))
suite.False(found)
ts = suite.keeper.GetAllCdpsByDenomAndRatio(suite.ctx, "xrp", types.MaxSortableDec)
suite.Equal(0, len(ts))
ts = suite.keeper.GetAllCdpsByDenom(suite.ctx, "xrp")
suite.Equal(0, len(ts))
sk = suite.app.GetSupplyKeeper()
acc = sk.GetModuleAccount(suite.ctx, types.ModuleName)
suite.Equal(sdk.Coins(nil), acc.GetCoins())
}
func (suite *DrawTestSuite) TestAddRepayPrincipalFees() {
err := suite.keeper.AddCdp(suite.ctx, suite.addrs[2], cs(c("xrp", 1000000000000)), cs(c("usdx", 100000000000)))
suite.NoError(err)
suite.ctx = suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(time.Minute * 10))
err = suite.keeper.AddPrincipal(suite.ctx, suite.addrs[2], "xrp", cs(c("usdx", 10000000)))
suite.NoError(err)
t, _ := suite.keeper.GetCDP(suite.ctx, "xrp", uint64(2))
suite.Equal(cs(c("usdx", 92827)), t.AccumulatedFees)
_ = suite.keeper.MintDebtCoins(suite.ctx, types.ModuleName, "debt", cs(c("usdx", 92827)))
err = suite.keeper.RepayPrincipal(suite.ctx, suite.addrs[2], "xrp", cs(c("usdx", 100)))
suite.NoError(err)
t, _ = suite.keeper.GetCDP(suite.ctx, "xrp", uint64(2))
suite.Equal(cs(c("usdx", 92727)), t.AccumulatedFees)
err = suite.keeper.RepayPrincipal(suite.ctx, suite.addrs[2], "xrp", cs(c("usdx", 100010092727)))
suite.NoError(err)
_, f := suite.keeper.GetCDP(suite.ctx, "xrp", uint64(2))
suite.False(f)
err = suite.keeper.AddCdp(suite.ctx, suite.addrs[2], cs(c("xrp", 1000000000000)), cs(c("usdx", 100000000)))
suite.NoError(err)
suite.ctx = suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(time.Second * 31536000))
err = suite.keeper.AddPrincipal(suite.ctx, suite.addrs[2], "xrp", cs(c("usdx", 100000000)))
suite.NoError(err)
t, _ = suite.keeper.GetCDP(suite.ctx, "xrp", uint64(3))
suite.Equal(cs(c("usdx", 5000000)), t.AccumulatedFees)
}
func (suite *DrawTestSuite) TestPricefeedFailure() {
ctx := suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(time.Hour * 2))
pfk := suite.app.GetPriceFeedKeeper()
pfk.SetCurrentPrices(ctx, "xrp:usd")
err := suite.keeper.AddPrincipal(ctx, suite.addrs[0], "xrp", cs(c("usdx", 10)))
suite.Error(err)
err = suite.keeper.RepayPrincipal(ctx, suite.addrs[0], "xrp", cs(c("usdx", 10)))
suite.NoError(err)
}
func (suite *DrawTestSuite) TestModuleAccountFailure() {
suite.Panics(func() {
ctx := suite.ctx.WithBlockHeader(suite.ctx.BlockHeader())
sk := suite.app.GetSupplyKeeper()
acc := sk.GetModuleAccount(ctx, types.ModuleName)
ak := suite.app.GetAccountKeeper()
ak.RemoveAccount(ctx, acc)
_ = suite.keeper.RepayPrincipal(ctx, suite.addrs[0], "xrp", cs(c("usdx", 10)))
})
}
func TestDrawTestSuite(t *testing.T) {
suite.Run(t, new(DrawTestSuite))
}

102
x/cdp/keeper/fees.go Normal file
View File

@ -0,0 +1,102 @@
package keeper
import (
"fmt"
"time"
"github.com/cosmos/cosmos-sdk/store/prefix"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/cdp/types"
)
// CalculateFees returns the fees accumulated since fees were last calculated based on
// the input amount of outstanding debt (principal) and the number of periods (seconds) that have passed
func (k Keeper) CalculateFees(ctx sdk.Context, principal sdk.Coins, periods sdk.Int, denom string) sdk.Coins {
newFees := sdk.NewCoins()
for _, pc := range principal {
// how fees are calculated:
// feesAccumulated = (outstandingDebt * (feeRate^periods)) - outstandingDebt
// Note that since we can't do x^y using sdk.Decimal, we are converting to int and using RelativePow
feePerSecond := k.GetFeeRate(ctx, denom)
scalar := sdk.NewInt(1000000000000000000)
feeRateInt := feePerSecond.Mul(sdk.NewDecFromInt(scalar)).TruncateInt()
accumulator := sdk.NewDecFromInt(types.RelativePow(feeRateInt, periods, scalar)).Mul(sdk.SmallestDec())
feesAccumulated := (sdk.NewDecFromInt(pc.Amount).Mul(accumulator)).Sub(sdk.NewDecFromInt(pc.Amount))
// TODO this will always round down, causing precision loss between the sum of all fees in CDPs and surplus coins in liquidator account
newFees = newFees.Add(sdk.NewCoins(sdk.NewCoin(pc.Denom, feesAccumulated.TruncateInt())))
}
return newFees
}
// IncrementTotalPrincipal increments the total amount of debt that has been drawn with that collateral type
func (k Keeper) IncrementTotalPrincipal(ctx sdk.Context, collateralDenom string, principal sdk.Coins) {
for _, pc := range principal {
total := k.GetTotalPrincipal(ctx, collateralDenom, pc.Denom)
total = total.Add(pc.Amount)
k.SetTotalPrincipal(ctx, collateralDenom, pc.Denom, total)
}
}
// DecrementTotalPrincipal decrements the total amount of debt that has been drawn for a particular collateral type
func (k Keeper) DecrementTotalPrincipal(ctx sdk.Context, collateralDenom string, principal sdk.Coins) {
for _, pc := range principal {
total := k.GetTotalPrincipal(ctx, collateralDenom, pc.Denom)
total = total.Sub(pc.Amount)
if total.IsNegative() {
// can happen in tests due to rounding errors in fee calculation
total = sdk.ZeroInt()
}
k.SetTotalPrincipal(ctx, collateralDenom, pc.Denom, total)
}
}
// GetTotalPrincipal returns the total amount of principal that has been drawn for a particular collateral
func (k Keeper) GetTotalPrincipal(ctx sdk.Context, collateralDenom string, principalDenom string) (total sdk.Int) {
store := prefix.NewStore(ctx.KVStore(k.key), types.PrincipalKeyPrefix)
bz := store.Get([]byte(collateralDenom + principalDenom))
if bz == nil {
panic(fmt.Sprintf("total principal of %s for %s collateral not set in genesis", principalDenom, collateralDenom))
}
k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &total)
return total
}
// SetTotalPrincipal sets the total amount of principal that has been drawn for the input collateral
func (k Keeper) SetTotalPrincipal(ctx sdk.Context, collateralDenom string, principalDenom string, total sdk.Int) {
store := prefix.NewStore(ctx.KVStore(k.key), types.PrincipalKeyPrefix)
store.Set([]byte(collateralDenom+principalDenom), k.cdc.MustMarshalBinaryLengthPrefixed(total))
}
// GetFeeRate returns the per second fee rate for the input denom
func (k Keeper) GetFeeRate(ctx sdk.Context, denom string) (fee sdk.Dec) {
store := prefix.NewStore(ctx.KVStore(k.key), types.AccumulatorKeyPrefix)
bz := store.Get([]byte(denom))
if bz == nil {
panic(fmt.Sprintf("fee rate for %s not set in genesis", denom))
}
k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &fee)
return fee
}
// SetFeeRate sets the per second fee rate for the input denom
func (k Keeper) SetFeeRate(ctx sdk.Context, denom string, fee sdk.Dec) {
store := prefix.NewStore(ctx.KVStore(k.key), types.AccumulatorKeyPrefix)
store.Set([]byte(denom), k.cdc.MustMarshalBinaryLengthPrefixed(fee))
}
// GetPreviousBlockTime get the blocktime for the previous block
func (k Keeper) GetPreviousBlockTime(ctx sdk.Context) (blockTime time.Time, found bool) {
store := prefix.NewStore(ctx.KVStore(k.key), types.PreviousBlockTimeKey)
b := store.Get([]byte{})
if b == nil {
return time.Time{}, false
}
k.cdc.MustUnmarshalBinaryLengthPrefixed(b, &blockTime)
return blockTime, true
}
// SetPreviousBlockTime set the time of the previous block
func (k Keeper) SetPreviousBlockTime(ctx sdk.Context, blockTime time.Time) {
store := prefix.NewStore(ctx.KVStore(k.key), types.PreviousBlockTimeKey)
store.Set([]byte{}, k.cdc.MustMarshalBinaryLengthPrefixed(blockTime))
}

95
x/cdp/keeper/fees_test.go Normal file
View File

@ -0,0 +1,95 @@
package keeper_test
import (
"math/rand"
"testing"
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/cdp/keeper"
"github.com/stretchr/testify/suite"
abci "github.com/tendermint/tendermint/abci/types"
tmtime "github.com/tendermint/tendermint/types/time"
)
type FeeTestSuite struct {
suite.Suite
keeper keeper.Keeper
app app.TestApp
ctx sdk.Context
}
func (suite *FeeTestSuite) SetupTest() {
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
}
func (suite *FeeTestSuite) TestCalculateFeesPrecisionLoss() {
// Calculates the difference between fees calculated on the total amount of debt,
// versus iterating over all the 1000 randomly generated cdps.
// Assumes 7 second block times, runs simulations for 100, 1000, 10000, 100000, and 1000000
// blocks, where the bulk debt is updated each block, and the cdps are updated once.
coins := []sdk.Coins{}
total := sdk.NewCoins()
for i := 0; i < 1000; i++ {
ri, err := simulation.RandPositiveInt(rand.New(rand.NewSource(int64(i))), sdk.NewInt(100000000000))
suite.NoError(err)
c := sdk.NewCoins(sdk.NewCoin("usdx", ri))
coins = append(coins, c)
total = total.Add(cs(sdk.NewCoin("usdx", ri)))
}
numBlocks := []int{100, 1000, 10000, 100000}
for _, nb := range numBlocks {
bulkFees := sdk.NewCoins()
individualFees := sdk.NewCoins()
for x := 0; x < nb; x++ {
fee := suite.keeper.CalculateFees(suite.ctx, total.Add(bulkFees), i(7), "xrp")
bulkFees = bulkFees.Add(fee)
}
for _, cns := range coins {
fee := suite.keeper.CalculateFees(suite.ctx, cns, i(int64(nb*7)), "xrp")
individualFees = individualFees.Add(fee)
}
absError := (sdk.OneDec().Sub(sdk.NewDecFromInt(bulkFees[0].Amount).Quo(sdk.NewDecFromInt(individualFees[0].Amount)))).Abs()
suite.T().Log(bulkFees)
suite.T().Log(individualFees)
suite.T().Log(absError)
suite.True(d("0.00001").GTE(absError))
}
}
func (suite *FeeTestSuite) TestGetSetPreviousBlockTime() {
now := tmtime.Now()
_, f := suite.keeper.GetPreviousBlockTime(suite.ctx)
suite.False(f)
suite.NotPanics(func() { suite.keeper.SetPreviousBlockTime(suite.ctx, now) })
bpt, f := suite.keeper.GetPreviousBlockTime(suite.ctx)
suite.True(f)
suite.Equal(now, bpt)
}
func TestFeeTestSuite(t *testing.T) {
suite.Run(t, new(FeeTestSuite))
}

View File

@ -7,11 +7,11 @@ import (
"github.com/kava-labs/kava/app" "github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/cdp" "github.com/kava-labs/kava/x/cdp"
"github.com/kava-labs/kava/x/cdp/types"
"github.com/kava-labs/kava/x/pricefeed" "github.com/kava-labs/kava/x/pricefeed"
tmtime "github.com/tendermint/tendermint/types/time"
) )
// Avoid cluttering test cases with long function name // Avoid cluttering test cases with long function names
func i(in int64) sdk.Int { return sdk.NewInt(in) } func i(in int64) sdk.Int { return sdk.NewInt(in) }
func d(str string) sdk.Dec { return sdk.MustNewDecFromStr(str) } func d(str string) sdk.Dec { return sdk.MustNewDecFromStr(str) }
func c(denom string, amount int64) sdk.Coin { return sdk.NewInt64Coin(denom, amount) } func c(denom string, amount int64) sdk.Coin { return sdk.NewInt64Coin(denom, amount) }
@ -21,12 +21,12 @@ func NewPricefeedGenState(asset string, price sdk.Dec) app.GenesisState {
pfGenesis := pricefeed.GenesisState{ pfGenesis := pricefeed.GenesisState{
Params: pricefeed.Params{ Params: pricefeed.Params{
Markets: []pricefeed.Market{ Markets: []pricefeed.Market{
pricefeed.Market{MarketID: asset, BaseAsset: asset, QuoteAsset: "usd", Oracles: pricefeed.Oracles{}, Active: true}, pricefeed.Market{MarketID: asset + ":usd", BaseAsset: asset, QuoteAsset: "usd", Oracles: pricefeed.Oracles{}, Active: true},
}, },
}, },
PostedPrices: []pricefeed.PostedPrice{ PostedPrices: []pricefeed.PostedPrice{
pricefeed.PostedPrice{ pricefeed.PostedPrice{
MarketID: asset, MarketID: asset + ":usd",
OracleAddress: sdk.AccAddress{}, OracleAddress: sdk.AccAddress{},
Price: price, Price: price,
Expiry: time.Now().Add(1 * time.Hour), Expiry: time.Now().Add(1 * time.Hour),
@ -38,18 +38,33 @@ func NewPricefeedGenState(asset string, price sdk.Dec) app.GenesisState {
func NewCDPGenState(asset string, liquidationRatio sdk.Dec) app.GenesisState { func NewCDPGenState(asset string, liquidationRatio sdk.Dec) app.GenesisState {
cdpGenesis := cdp.GenesisState{ cdpGenesis := cdp.GenesisState{
Params: cdp.CdpParams{ Params: cdp.Params{
GlobalDebtLimit: sdk.NewInt(1000000), GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000)),
CollateralParams: []cdp.CollateralParams{ CollateralParams: cdp.CollateralParams{
{ {
Denom: asset, Denom: asset,
LiquidationRatio: liquidationRatio, LiquidationRatio: liquidationRatio,
DebtLimit: sdk.NewInt(500000), DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000)),
StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), // %5 apr
Prefix: 0x20,
ConversionFactor: i(6),
MarketID: asset + ":usd",
},
},
DebtParams: cdp.DebtParams{
{
Denom: "usdx",
ReferenceAsset: "usd",
DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000)),
ConversionFactor: i(6),
DebtFloor: i(10000000),
}, },
}, },
}, },
GlobalDebt: sdk.ZeroInt(), StartingCdpID: cdp.DefaultCdpStartingID,
DebtDenom: cdp.DefaultDebtDenom,
CDPs: cdp.CDPs{}, CDPs: cdp.CDPs{},
PreviousBlockTime: cdp.DefaultPreviousBlockTime,
} }
return app.GenesisState{cdp.ModuleName: cdp.ModuleCdc.MustMarshalJSON(cdpGenesis)} return app.GenesisState{cdp.ModuleName: cdp.ModuleCdc.MustMarshalJSON(cdpGenesis)}
} }
@ -58,19 +73,19 @@ func NewPricefeedGenStateMulti() app.GenesisState {
pfGenesis := pricefeed.GenesisState{ pfGenesis := pricefeed.GenesisState{
Params: pricefeed.Params{ Params: pricefeed.Params{
Markets: []pricefeed.Market{ Markets: []pricefeed.Market{
pricefeed.Market{MarketID: "btc", BaseAsset: "btc", QuoteAsset: "usd", Oracles: pricefeed.Oracles{}, Active: true}, pricefeed.Market{MarketID: "btc:usd", BaseAsset: "btc", QuoteAsset: "usd", Oracles: pricefeed.Oracles{}, Active: true},
pricefeed.Market{MarketID: "xrp", BaseAsset: "xrp", QuoteAsset: "usd", Oracles: pricefeed.Oracles{}, Active: true}, pricefeed.Market{MarketID: "xrp:usd", BaseAsset: "xrp", QuoteAsset: "usd", Oracles: pricefeed.Oracles{}, Active: true},
}, },
}, },
PostedPrices: []pricefeed.PostedPrice{ PostedPrices: []pricefeed.PostedPrice{
pricefeed.PostedPrice{ pricefeed.PostedPrice{
MarketID: "btc", MarketID: "btc:usd",
OracleAddress: sdk.AccAddress{}, OracleAddress: sdk.AccAddress{},
Price: sdk.MustNewDecFromStr("8000.00"), Price: sdk.MustNewDecFromStr("8000.00"),
Expiry: time.Now().Add(1 * time.Hour), Expiry: time.Now().Add(1 * time.Hour),
}, },
pricefeed.PostedPrice{ pricefeed.PostedPrice{
MarketID: "xrp", MarketID: "xrp:usd",
OracleAddress: sdk.AccAddress{}, OracleAddress: sdk.AccAddress{},
Price: sdk.MustNewDecFromStr("0.25"), Price: sdk.MustNewDecFromStr("0.25"),
Expiry: time.Now().Add(1 * time.Hour), Expiry: time.Now().Add(1 * time.Hour),
@ -81,23 +96,59 @@ func NewPricefeedGenStateMulti() app.GenesisState {
} }
func NewCDPGenStateMulti() app.GenesisState { func NewCDPGenStateMulti() app.GenesisState {
cdpGenesis := cdp.GenesisState{ cdpGenesis := cdp.GenesisState{
Params: cdp.CdpParams{ Params: cdp.Params{
GlobalDebtLimit: sdk.NewInt(1000000), GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000), sdk.NewInt64Coin("susd", 1000000000000)),
CollateralParams: []types.CollateralParams{ CollateralParams: cdp.CollateralParams{
{
Denom: "btc",
LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
DebtLimit: sdk.NewInt(500000),
},
{ {
Denom: "xrp", Denom: "xrp",
LiquidationRatio: sdk.MustNewDecFromStr("2.0"), LiquidationRatio: sdk.MustNewDecFromStr("2.0"),
DebtLimit: sdk.NewInt(500000), 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: "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),
},
},
DebtParams: cdp.DebtParams{
{
Denom: "usdx",
ReferenceAsset: "usd",
DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000)),
ConversionFactor: i(6),
DebtFloor: i(10000000),
},
{
Denom: "susd",
ReferenceAsset: "usd",
DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("susd", 1000000000000)),
ConversionFactor: i(6),
DebtFloor: i(10000000),
}, },
}, },
}, },
GlobalDebt: sdk.ZeroInt(), StartingCdpID: cdp.DefaultCdpStartingID,
DebtDenom: cdp.DefaultDebtDenom,
CDPs: cdp.CDPs{}, CDPs: cdp.CDPs{},
PreviousBlockTime: cdp.DefaultPreviousBlockTime,
} }
return app.GenesisState{cdp.ModuleName: cdp.ModuleCdc.MustMarshalJSON(cdpGenesis)} return app.GenesisState{cdp.ModuleName: cdp.ModuleCdc.MustMarshalJSON(cdpGenesis)}
} }
func cdps() (cdps cdp.CDPs) {
_, addrs := app.GeneratePrivKeyAddressPairs(3)
c1 := cdp.NewCDP(uint64(1), addrs[0], sdk.NewCoins(sdk.NewCoin("xrp", sdk.NewInt(10000000))), sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(8000000))), tmtime.Canonical(time.Now()))
c2 := cdp.NewCDP(uint64(2), addrs[1], sdk.NewCoins(sdk.NewCoin("xrp", sdk.NewInt(100000000))), sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(10000000))), tmtime.Canonical(time.Now()))
c3 := cdp.NewCDP(uint64(3), addrs[1], sdk.NewCoins(sdk.NewCoin("btc", sdk.NewInt(1000000000))), sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(10000000))), tmtime.Canonical(time.Now()))
c4 := cdp.NewCDP(uint64(4), addrs[2], sdk.NewCoins(sdk.NewCoin("xrp", sdk.NewInt(1000000000))), sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(500000000))), tmtime.Canonical(time.Now()))
cdps = append(cdps, c1, c2, c3, c4)
return
}

View File

@ -1,499 +1,103 @@
package keeper package keeper
import ( import (
"bytes"
"fmt" "fmt"
"sort"
"github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/store/prefix"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/params/subspace" "github.com/cosmos/cosmos-sdk/x/params/subspace"
"github.com/kava-labs/kava/x/cdp/types" "github.com/kava-labs/kava/x/cdp/types"
) )
// Keeper cdp Keeper // Keeper keeper for the cdp module
type Keeper struct { type Keeper struct {
key sdk.StoreKey key sdk.StoreKey
cdc *codec.Codec cdc *codec.Codec
paramSubspace subspace.Subspace paramSubspace subspace.Subspace
pricefeedKeeper types.PricefeedKeeper pricefeedKeeper types.PricefeedKeeper
bankKeeper types.BankKeeper supplyKeeper types.SupplyKeeper
codespace sdk.CodespaceType
} }
// NewKeeper creates a new keeper // NewKeeper creates a new keeper
func NewKeeper(cdc *codec.Codec, key sdk.StoreKey, paramstore subspace.Subspace, pfk types.PricefeedKeeper, bk types.BankKeeper) Keeper { func NewKeeper(cdc *codec.Codec, key sdk.StoreKey, paramstore subspace.Subspace, pfk types.PricefeedKeeper, sk types.SupplyKeeper, codespace sdk.CodespaceType) Keeper {
// ensure module account is set
if addr := sk.GetModuleAddress(types.ModuleName); addr == nil {
panic(fmt.Sprintf("%s module account has not been set", types.ModuleName))
}
return Keeper{ return Keeper{
key: key, key: key,
cdc: cdc, cdc: cdc,
paramSubspace: paramstore.WithKeyTable(types.ParamKeyTable()), paramSubspace: paramstore.WithKeyTable(types.ParamKeyTable()),
pricefeedKeeper: pfk, pricefeedKeeper: pfk,
bankKeeper: bk, supplyKeeper: sk,
codespace: codespace,
} }
} }
// ModifyCDP creates, changes, or deletes a CDP // CdpDenomIndexIterator returns an sdk.Iterator for all cdps with matching collateral denom
// TODO can/should this function be split up? func (k Keeper) CdpDenomIndexIterator(ctx sdk.Context, denom string) sdk.Iterator {
func (k Keeper) ModifyCDP(ctx sdk.Context, owner sdk.AccAddress, collateralDenom string, changeInCollateral sdk.Int, changeInDebt sdk.Int) sdk.Error { store := prefix.NewStore(ctx.KVStore(k.key), types.CdpKeyPrefix)
db, _ := k.GetDenomPrefix(ctx, denom)
// Phase 1: Get state, make changes in memory and check if they're ok. return sdk.KVStorePrefixIterator(store, types.DenomIterKey(db))
// Check collateral type ok
p := k.GetParams(ctx)
if !p.IsCollateralPresent(collateralDenom) { // maybe abstract this logic into GetCDP
return sdk.ErrInternal("collateral type not enabled to create CDPs")
} }
// Check the owner has enough collateral and stable coins // CdpCollateralRatioIndexIterator returns an sdk.Iterator for all cdps that have collateral denom
if changeInCollateral.IsPositive() { // adding collateral to CDP // matching denom and collateral:debt ratio LESS THAN targetRatio
ok := k.bankKeeper.HasCoins(ctx, owner, sdk.NewCoins(sdk.NewCoin(collateralDenom, changeInCollateral))) func (k Keeper) CdpCollateralRatioIndexIterator(ctx sdk.Context, denom string, targetRatio sdk.Dec) sdk.Iterator {
if !ok { store := prefix.NewStore(ctx.KVStore(k.key), types.CollateralRatioIndexPrefix)
return sdk.ErrInsufficientCoins("not enough collateral in sender's account") db, _ := k.GetDenomPrefix(ctx, denom)
} return store.Iterator(types.CollateralRatioIterKey(db, sdk.ZeroDec()), types.CollateralRatioIterKey(db, targetRatio))
}
if changeInDebt.IsNegative() { // reducing debt, by adding stable coin to CDP
ok := k.bankKeeper.HasCoins(ctx, owner, sdk.NewCoins(sdk.NewCoin("usdx", changeInDebt.Neg())))
if !ok {
return sdk.ErrInsufficientCoins("not enough stable coin in sender's account")
}
} }
// Change collateral and debt recorded in CDP // IterateAllCdps iterates over all cdps and performs a callback function
// Get CDP (or create if not exists) func (k Keeper) IterateAllCdps(ctx sdk.Context, cb func(cdp types.CDP) (stop bool)) {
cdp, found := k.GetCDP(ctx, owner, collateralDenom) store := prefix.NewStore(ctx.KVStore(k.key), types.CdpKeyPrefix)
if !found { iterator := sdk.KVStorePrefixIterator(store, []byte{})
cdp = types.CDP{Owner: owner, CollateralDenom: collateralDenom, CollateralAmount: sdk.ZeroInt(), Debt: sdk.ZeroInt()} defer iterator.Close()
} for ; iterator.Valid(); iterator.Next() {
// Add/Subtract collateral and debt
cdp.CollateralAmount = cdp.CollateralAmount.Add(changeInCollateral)
if cdp.CollateralAmount.IsNegative() {
return sdk.ErrInternal(" can't withdraw more collateral than exists in CDP")
}
cdp.Debt = cdp.Debt.Add(changeInDebt)
if cdp.Debt.IsNegative() {
return sdk.ErrInternal("can't pay back more debt than exists in CDP")
}
isUnderCollateralized := cdp.IsUnderCollateralized(
k.pricefeedKeeper.GetCurrentPrice(ctx, cdp.CollateralDenom).Price,
p.GetCollateralParams(cdp.CollateralDenom).LiquidationRatio,
)
if isUnderCollateralized {
return sdk.ErrInternal("Change to CDP would put it below liquidation ratio")
}
// TODO check for dust
// Add/Subtract from global debt limit
gDebt := k.GetGlobalDebt(ctx)
gDebt = gDebt.Add(changeInDebt)
if gDebt.IsNegative() {
return sdk.ErrInternal("global debt can't be negative") // This should never happen if debt per CDP can't be negative
}
if gDebt.GT(p.GlobalDebtLimit) {
return sdk.ErrInternal("change to CDP would put the system over the global debt limit")
}
// Add/Subtract from collateral debt limit
collateralState, found := k.GetCollateralState(ctx, cdp.CollateralDenom)
if !found {
collateralState = types.CollateralState{Denom: cdp.CollateralDenom, TotalDebt: sdk.ZeroInt()} // Already checked that this denom is authorized, so ok to create new CollateralState
}
collateralState.TotalDebt = collateralState.TotalDebt.Add(changeInDebt)
if collateralState.TotalDebt.IsNegative() {
return sdk.ErrInternal("total debt for this collateral type can't be negative") // This should never happen if debt per CDP can't be negative
}
if collateralState.TotalDebt.GT(p.GetCollateralParams(cdp.CollateralDenom).DebtLimit) {
return sdk.ErrInternal("change to CDP would put the system over the debt limit for this collateral type")
}
// Phase 2: Update all the state
// change owner's coins (increase or decrease)
var err sdk.Error
if changeInCollateral.IsNegative() {
_, err = k.bankKeeper.AddCoins(ctx, owner, sdk.NewCoins(sdk.NewCoin(collateralDenom, changeInCollateral.Neg())))
} else {
_, err = k.bankKeeper.SubtractCoins(ctx, owner, sdk.NewCoins(sdk.NewCoin(collateralDenom, changeInCollateral)))
}
if err != nil {
panic(err) // this shouldn't happen because coin balance was checked earlier
}
if changeInDebt.IsNegative() {
_, err = k.bankKeeper.SubtractCoins(ctx, owner, sdk.NewCoins(sdk.NewCoin("usdx", changeInDebt.Neg())))
} else {
_, err = k.bankKeeper.AddCoins(ctx, owner, sdk.NewCoins(sdk.NewCoin("usdx", changeInDebt)))
}
if err != nil {
panic(err) // this shouldn't happen because coin balance was checked earlier
}
// Set CDP
if cdp.CollateralAmount.IsZero() && cdp.Debt.IsZero() { // TODO maybe abstract this logic into SetCDP
k.DeleteCDP(ctx, cdp)
} else {
k.SetCDP(ctx, cdp)
}
// set total debts
k.SetGlobalDebt(ctx, gDebt)
k.SetCollateralState(ctx, collateralState)
return nil
}
// TODO
// // TransferCDP allows people to transfer ownership of their CDPs to others
// func (k Keeper) TransferCDP(ctx sdk.Context, from sdk.AccAddress, to sdk.AccAddress, collateralDenom string) sdk.Error {
// return nil
// }
// PartialSeizeCDP removes collateral and debt from a CDP and decrements global debt counters. It does not move collateral to another account so is unsafe.
// TODO should this be made safer by moving collateral to liquidatorModuleAccount ? If so how should debt be moved?
func (k Keeper) PartialSeizeCDP(ctx sdk.Context, owner sdk.AccAddress, collateralDenom string, collateralToSeize sdk.Int, debtToSeize sdk.Int) sdk.Error {
// get CDP
cdp, found := k.GetCDP(ctx, owner, collateralDenom)
if !found {
return sdk.ErrInternal("could not find CDP")
}
// Check if CDP is undercollateralized
p := k.GetParams(ctx)
isUnderCollateralized := cdp.IsUnderCollateralized(
k.pricefeedKeeper.GetCurrentPrice(ctx, cdp.CollateralDenom).Price,
p.GetCollateralParams(cdp.CollateralDenom).LiquidationRatio,
)
if !isUnderCollateralized {
return sdk.ErrInternal("CDP is not currently under the liquidation ratio")
}
// Remove Collateral
if collateralToSeize.IsNegative() {
return sdk.ErrInternal("cannot seize negative collateral")
}
cdp.CollateralAmount = cdp.CollateralAmount.Sub(collateralToSeize)
if cdp.CollateralAmount.IsNegative() {
return sdk.ErrInternal("can't seize more collateral than exists in CDP")
}
// Remove Debt
if debtToSeize.IsNegative() {
return sdk.ErrInternal("cannot seize negative debt")
}
cdp.Debt = cdp.Debt.Sub(debtToSeize)
if cdp.Debt.IsNegative() {
return sdk.ErrInternal("can't seize more debt than exists in CDP")
}
// Update debt per collateral type
collateralState, found := k.GetCollateralState(ctx, cdp.CollateralDenom)
if !found {
return sdk.ErrInternal("could not find collateral state")
}
collateralState.TotalDebt = collateralState.TotalDebt.Sub(debtToSeize)
if collateralState.TotalDebt.IsNegative() {
return sdk.ErrInternal("Total debt per collateral type is negative.") // This should not happen given the checks on the CDP.
}
// Note: Global debt is not decremented here. It's only decremented when debt and stable coin are annihilated (aka heal)
// TODO update global seized debt? this is what maker does (named vice in Vat.grab) but it's not used anywhere
// Store updated state
if cdp.CollateralAmount.IsZero() && cdp.Debt.IsZero() { // TODO maybe abstract this logic into SetCDP
k.DeleteCDP(ctx, cdp)
} else {
k.SetCDP(ctx, cdp)
}
k.SetCollateralState(ctx, collateralState)
return nil
}
// ReduceGlobalDebt decreases the stored global debt counter. It is used by the liquidator when it annihilates debt and stable coin.
// TODO Can the interface between cdp and liquidator modules be improved so that this function doesn't exist?
func (k Keeper) ReduceGlobalDebt(ctx sdk.Context, amount sdk.Int) sdk.Error {
if amount.IsNegative() {
return sdk.ErrInternal("reduction in global debt must be a positive amount")
}
newGDebt := k.GetGlobalDebt(ctx).Sub(amount)
if newGDebt.IsNegative() {
return sdk.ErrInternal("cannot reduce global debt by amount specified")
}
k.SetGlobalDebt(ctx, newGDebt)
return nil
}
func (k Keeper) GetStableDenom() string {
return "usdx"
}
func (k Keeper) GetGovDenom() string {
return "kava"
}
// ---------- Store Wrappers ----------
func (k Keeper) getCDPKeyPrefix(collateralDenom string) []byte {
return bytes.Join(
[][]byte{
[]byte("cdp"),
[]byte(collateralDenom),
},
nil, // no separator
)
}
func (k Keeper) getCDPKey(owner sdk.AccAddress, collateralDenom string) []byte {
return bytes.Join(
[][]byte{
k.getCDPKeyPrefix(collateralDenom),
[]byte(owner.String()),
},
nil, // no separator
)
}
func (k Keeper) GetCDP(ctx sdk.Context, owner sdk.AccAddress, collateralDenom string) (types.CDP, bool) {
// get store
store := ctx.KVStore(k.key)
// get CDP
bz := store.Get(k.getCDPKey(owner, collateralDenom))
// unmarshal
if bz == nil {
return types.CDP{}, false
}
var cdp types.CDP var cdp types.CDP
k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &cdp) k.cdc.MustUnmarshalBinaryLengthPrefixed(iterator.Value(), &cdp)
return cdp, true
if cb(cdp) {
break
} }
func (k Keeper) SetCDP(ctx sdk.Context, cdp types.CDP) {
// get store
store := ctx.KVStore(k.key)
// marshal and set
bz := k.cdc.MustMarshalBinaryLengthPrefixed(cdp)
store.Set(k.getCDPKey(cdp.Owner, cdp.CollateralDenom), bz)
} }
func (k Keeper) DeleteCDP(ctx sdk.Context, cdp types.CDP) { // TODO should this id the cdp by passing in owner,collateralDenom pair?
// get store
store := ctx.KVStore(k.key)
// delete key
store.Delete(k.getCDPKey(cdp.Owner, cdp.CollateralDenom))
} }
// GetCDPs returns all CDPs, optionally filtered by collateral type and liquidation price. // IterateCdpsByDenom iterates over cdps with matching denom and performs a callback function
// `price` filters for CDPs that will be below the liquidation ratio when the collateral is at that specified price. func (k Keeper) IterateCdpsByDenom(ctx sdk.Context, denom string, cb func(cdp types.CDP) (stop bool)) {
func (k Keeper) GetCDPs(ctx sdk.Context, collateralDenom string, price sdk.Dec) (types.CDPs, sdk.Error) { iterator := k.CdpDenomIndexIterator(ctx, denom)
// Validate inputs
params := k.GetParams(ctx)
if len(collateralDenom) != 0 && !params.IsCollateralPresent(collateralDenom) {
return nil, sdk.ErrInternal("collateral denom not authorized")
}
if len(collateralDenom) == 0 && !(price.IsNil() || price.IsNegative()) {
return nil, sdk.ErrInternal("cannot specify price without collateral denom")
}
// Get an iterator over CDPs defer iterator.Close()
store := ctx.KVStore(k.key) for ; iterator.Valid(); iterator.Next() {
iter := sdk.KVStorePrefixIterator(store, k.getCDPKeyPrefix(collateralDenom)) // could be all CDPs is collateralDenom is ""
// Decode CDPs into slice
var cdps types.CDPs
for ; iter.Valid(); iter.Next() {
var cdp types.CDP var cdp types.CDP
k.cdc.MustUnmarshalBinaryLengthPrefixed(iter.Value(), &cdp) k.cdc.MustUnmarshalBinaryLengthPrefixed(iterator.Value(), &cdp)
cdps = append(cdps, cdp) if cb(cdp) {
break
} }
// Sort by collateral ratio (collateral/debt)
sort.Sort(types.ByCollateralRatio(cdps)) // TODO this doesn't make much sense across different collateral types
// Filter for CDPs that would be under-collateralized at the specified price
// If price is nil or -ve, skip the filtering as it would return all CDPs anyway
if !price.IsNil() && !price.IsNegative() {
var filteredCDPs types.CDPs
for _, cdp := range cdps {
if cdp.IsUnderCollateralized(price, params.GetCollateralParams(collateralDenom).LiquidationRatio) {
filteredCDPs = append(filteredCDPs, cdp)
} else {
break // break early because list is sorted
}
}
cdps = filteredCDPs
}
return cdps, nil
}
var globalDebtKey = []byte("globalDebt")
func (k Keeper) GetGlobalDebt(ctx sdk.Context) sdk.Int {
// get store
store := ctx.KVStore(k.key)
// get bytes
bz := store.Get(globalDebtKey)
// unmarshal
if bz == nil {
panic("global debt not found")
}
var globalDebt sdk.Int
k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &globalDebt)
return globalDebt
}
func (k Keeper) SetGlobalDebt(ctx sdk.Context, globalDebt sdk.Int) {
// get store
store := ctx.KVStore(k.key)
// marshal and set
bz := k.cdc.MustMarshalBinaryLengthPrefixed(globalDebt)
store.Set(globalDebtKey, bz)
}
func (k Keeper) getCollateralStateKey(collateralDenom string) []byte {
return []byte(collateralDenom)
}
func (k Keeper) GetCollateralState(ctx sdk.Context, collateralDenom string) (types.CollateralState, bool) {
// get store
store := ctx.KVStore(k.key)
// get bytes
bz := store.Get(k.getCollateralStateKey(collateralDenom))
// unmarshal
if bz == nil {
return types.CollateralState{}, false
}
var collateralState types.CollateralState
k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &collateralState)
return collateralState, true
}
func (k Keeper) SetCollateralState(ctx sdk.Context, collateralstate types.CollateralState) {
// get store
store := ctx.KVStore(k.key)
// marshal and set
bz := k.cdc.MustMarshalBinaryLengthPrefixed(collateralstate)
store.Set(k.getCollateralStateKey(collateralstate.Denom), bz)
}
// ---------- Weird Bank Stuff ----------
// This only exists because module accounts aren't really a thing yet.
// Also because we need module accounts that allow for burning/minting.
// These functions make the CDP module act as a bank keeper, ie it fulfills the bank.Keeper interface.
// It intercepts calls to send coins to/from the liquidator module account, otherwise passing the calls onto the normal bank keeper.
// Not sure if module accounts are good, but they make the auction module more general:
// - startAuction would just "mints" coins, relying on calling function to decrement them somewhere
// - closeAuction would have to call something specific for the receiver module to accept coins (like liquidationKeeper.AddStableCoins)
// The auction and liquidator modules can probably just use SendCoins to keep things safe (instead of AddCoins and SubtractCoins).
// So they should define their own interfaces which this module should fulfill, rather than this fulfilling the entire bank.Keeper interface.
// bank.Keeper interfaces:
// type SendKeeper interface {
// type ViewKeeper interface {
// GetCoins(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins
// HasCoins(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins) bool
// Codespace() sdk.CodespaceType
// }
// SendCoins(ctx sdk.Context, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amt sdk.Coins) (sdk.Tags, sdk.Error)
// GetSendEnabled(ctx sdk.Context) bool
// SetSendEnabled(ctx sdk.Context, enabled bool)
// }
// type Keeper interface {
// SendKeeper
// SetCoins(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins) sdk.Error
// SubtractCoins(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins) (sdk.Coins, sdk.Tags, sdk.Error)
// AddCoins(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins) (sdk.Coins, sdk.Tags, sdk.Error)
// InputOutputCoins(ctx sdk.Context, inputs []Input, outputs []Output) (sdk.Tags, sdk.Error)
// DelegateCoins(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins) (sdk.Tags, sdk.Error)
// UndelegateCoins(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins) (sdk.Tags, sdk.Error)
var LiquidatorAccountAddress = sdk.AccAddress([]byte("whatever"))
var liquidatorAccountKey = []byte("liquidatorAccount")
func (k Keeper) GetLiquidatorAccountAddress() sdk.AccAddress {
return LiquidatorAccountAddress
}
type LiquidatorModuleAccount struct {
Coins sdk.Coins // keeps track of seized collateral, surplus usdx, and mints/burns gov coins
}
func (k Keeper) AddCoins(ctx sdk.Context, address sdk.AccAddress, amount sdk.Coins) (sdk.Coins, sdk.Error) {
// intercept module account
if address.Equals(LiquidatorAccountAddress) {
if !amount.IsValid() {
return nil, sdk.ErrInvalidCoins(amount.String())
}
// remove gov token from list
filteredCoins := stripGovCoin(amount)
// add coins to module account
lma := k.getLiquidatorModuleAccount(ctx)
updatedCoins := lma.Coins.Add(filteredCoins)
if updatedCoins.IsAnyNegative() {
return amount, sdk.ErrInsufficientCoins(fmt.Sprintf("insufficient account funds; %s < %s", lma.Coins, amount))
}
lma.Coins = updatedCoins
k.SetLiquidatorModuleAccount(ctx, lma)
return updatedCoins, nil
} else {
return k.bankKeeper.AddCoins(ctx, address, amount)
} }
} }
// TODO abstract stuff better // IterateCdpsByCollateralRatio iterate over cdps with collateral denom equal to denom and
func (k Keeper) SubtractCoins(ctx sdk.Context, address sdk.AccAddress, amount sdk.Coins) (sdk.Coins, sdk.Error) { // collateral:debt ratio LESS THAN targetRatio and performs a callback function.
// intercept module account func (k Keeper) IterateCdpsByCollateralRatio(ctx sdk.Context, denom string, targetRatio sdk.Dec, cb func(cdp types.CDP) (stop bool)) {
if address.Equals(LiquidatorAccountAddress) { iterator := k.CdpCollateralRatioIndexIterator(ctx, denom, targetRatio)
if !amount.IsValid() {
return nil, sdk.ErrInvalidCoins(amount.String()) defer iterator.Close()
} for ; iterator.Valid(); iterator.Next() {
// remove gov token from list db, id, _ := types.SplitCollateralRatioKey(iterator.Key())
filteredCoins := stripGovCoin(amount) d := k.getDenomFromByte(ctx, db)
// subtract coins from module account cdp, found := k.GetCDP(ctx, d, id)
lma := k.getLiquidatorModuleAccount(ctx) if !found {
updatedCoins, isNegative := lma.Coins.SafeSub(filteredCoins) panic(fmt.Sprintf("cdp %d does not exist", id))
if isNegative {
return amount, sdk.ErrInsufficientCoins(fmt.Sprintf("insufficient account funds; %s < %s", lma.Coins, amount))
}
lma.Coins = updatedCoins
k.SetLiquidatorModuleAccount(ctx, lma)
return updatedCoins, nil
} else {
return k.bankKeeper.SubtractCoins(ctx, address, amount)
} }
if cb(cdp) {
break
} }
// TODO Should this return anything for the gov coin balance? Currently returns nothing.
func (k Keeper) GetCoins(ctx sdk.Context, address sdk.AccAddress) sdk.Coins {
if address.Equals(LiquidatorAccountAddress) {
return k.getLiquidatorModuleAccount(ctx).Coins
} else {
return k.bankKeeper.GetCoins(ctx, address)
} }
} }
// TODO test this with unsorted coins
func (k Keeper) HasCoins(ctx sdk.Context, address sdk.AccAddress, amount sdk.Coins) bool {
if address.Equals(LiquidatorAccountAddress) {
return true
} else {
return k.getLiquidatorModuleAccount(ctx).Coins.IsAllGTE(stripGovCoin(amount))
}
}
func (k Keeper) getLiquidatorModuleAccount(ctx sdk.Context) LiquidatorModuleAccount {
// get store
store := ctx.KVStore(k.key)
// get bytes
bz := store.Get(liquidatorAccountKey)
if bz == nil {
return LiquidatorModuleAccount{} // TODO is it safe to do this, or better to initialize the account explicitly
}
// unmarshal
var lma LiquidatorModuleAccount
k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &lma)
return lma
}
func (k Keeper) SetLiquidatorModuleAccount(ctx sdk.Context, lma LiquidatorModuleAccount) {
store := ctx.KVStore(k.key)
bz := k.cdc.MustMarshalBinaryLengthPrefixed(lma)
store.Set(liquidatorAccountKey, bz)
}
func stripGovCoin(coins sdk.Coins) sdk.Coins {
filteredCoins := sdk.NewCoins()
for _, c := range coins {
if c.Denom != "kava" {
filteredCoins = append(filteredCoins, c)
}
}
return filteredCoins
}

View File

@ -1,316 +0,0 @@
package keeper_test
import (
"fmt"
"testing"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/require"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/cdp/types"
)
// How could one reduce the number of params in the test cases. Create a table driven test for each of the 4 add/withdraw collateral/debt?
// These are more like app level tests - I think this is a symptom of having 'ModifyCDP' do a lot. Could be easier for testing purposes to break it down.
func TestKeeper_ModifyCDP(t *testing.T) {
_, addrs := app.GeneratePrivKeyAddressPairs(1)
ownerAddr := addrs[0]
type state struct {
CDP types.CDP
OwnerCoins sdk.Coins
GlobalDebt sdk.Int
CollateralState types.CollateralState
}
type args struct {
owner sdk.AccAddress
collateralDenom string
changeInCollateral sdk.Int
changeInDebt sdk.Int
}
tests := []struct {
name string
priorState state
price string
// also missing CDPModuleParams
args args
expectPass bool
expectedState state
}{
{
"addCollateralAndDecreaseDebt",
state{types.CDP{ownerAddr, "xrp", i(100), i(2)}, cs(c("xrp", 10), c("usdx", 2)), i(2), types.CollateralState{"xrp", i(2)}},
"10.345",
args{ownerAddr, "xrp", i(10), i(-1)},
true,
state{types.CDP{ownerAddr, "xrp", i(110), i(1)}, cs( /* 0xrp */ c("usdx", 1)), i(1), types.CollateralState{"xrp", i(1)}},
},
{
"removeTooMuchCollateral",
state{types.CDP{ownerAddr, "xrp", i(1000), i(200)}, cs(c("xrp", 10), c("usdx", 10)), i(200), types.CollateralState{"xrp", i(200)}},
"1.00",
args{ownerAddr, "xrp", i(-601), i(0)},
false,
state{types.CDP{ownerAddr, "xrp", i(1000), i(200)}, cs(c("xrp", 10), c("usdx", 10)), i(200), types.CollateralState{"xrp", i(200)}},
},
{
"withdrawTooMuchStableCoin",
state{types.CDP{ownerAddr, "xrp", i(1000), i(200)}, cs(c("xrp", 10), c("usdx", 10)), i(200), types.CollateralState{"xrp", i(200)}},
"1.00",
args{ownerAddr, "xrp", i(0), i(301)},
false,
state{types.CDP{ownerAddr, "xrp", i(1000), i(200)}, cs(c("xrp", 10), c("usdx", 10)), i(200), types.CollateralState{"xrp", i(200)}},
},
{
"createCDPAndWithdrawStable",
state{types.CDP{}, cs(c("xrp", 10), c("usdx", 10)), i(0), types.CollateralState{"xrp", i(0)}},
"1.00",
args{ownerAddr, "xrp", i(5), i(2)},
true,
state{types.CDP{ownerAddr, "xrp", i(5), i(2)}, cs(c("xrp", 5), c("usdx", 12)), i(2), types.CollateralState{"xrp", i(2)}},
},
{
"emptyCDP",
state{types.CDP{ownerAddr, "xrp", i(1000), i(200)}, cs(c("xrp", 10), c("usdx", 201)), i(200), types.CollateralState{"xrp", i(200)}},
"1.00",
args{ownerAddr, "xrp", i(-1000), i(-200)},
true,
state{types.CDP{}, cs(c("xrp", 1010), c("usdx", 1)), i(0), types.CollateralState{"xrp", i(0)}},
},
{
"invalidCollateralType",
state{types.CDP{}, cs(c("shitcoin", 5000000)), i(0), types.CollateralState{}},
"0.000001",
args{ownerAddr, "shitcoin", i(5000000), i(1)}, // ratio of 5:1
false,
state{types.CDP{}, cs(c("shitcoin", 5000000)), i(0), types.CollateralState{}},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// setup test app
tApp := app.NewTestApp()
// initialize cdp owner account with coins, and collateral with price and params
tApp.InitializeFromGenesisStates(
app.NewAuthGenState([]sdk.AccAddress{ownerAddr}, []sdk.Coins{tc.priorState.OwnerCoins}),
NewPricefeedGenState("xrp", d(tc.price)),
NewCDPGenState("xrp", d("2.0")),
)
// create a context for db access
ctx := tApp.NewContext(false, abci.Header{})
// setup store state
keeper := tApp.GetCDPKeeper()
if tc.priorState.CDP.CollateralDenom != "" { // check if the prior CDP should be created or not (see if an empty one was specified)
keeper.SetCDP(ctx, tc.priorState.CDP)
}
keeper.SetGlobalDebt(ctx, tc.priorState.GlobalDebt)
if tc.priorState.CollateralState.Denom != "" {
keeper.SetCollateralState(ctx, tc.priorState.CollateralState)
}
// call func under test
err := keeper.ModifyCDP(ctx, tc.args.owner, tc.args.collateralDenom, tc.args.changeInCollateral, tc.args.changeInDebt)
// get new state for verification
actualCDP, found := keeper.GetCDP(ctx, tc.args.owner, tc.args.collateralDenom)
// check for err
if tc.expectPass {
require.NoError(t, err, fmt.Sprint(err))
} else {
require.Error(t, err)
}
actualGDebt := keeper.GetGlobalDebt(ctx)
actualCstate, _ := keeper.GetCollateralState(ctx, tc.args.collateralDenom)
// check state
require.Equal(t, tc.expectedState.CDP, actualCDP)
if tc.expectedState.CDP.CollateralDenom == "" { // if the expected CDP is blank, then expect the CDP to have been deleted (hence not found)
require.False(t, found)
} else {
require.True(t, found)
}
require.Equal(t, tc.expectedState.GlobalDebt, actualGDebt)
require.Equal(t, tc.expectedState.CollateralState, actualCstate)
// check owner balance
tApp.CheckBalance(t, ctx, ownerAddr, tc.expectedState.OwnerCoins)
})
}
}
func TestKeeper_PartialSeizeCDP(t *testing.T) {
// Setup
const collateral = "xrp"
_, addrs := app.GeneratePrivKeyAddressPairs(1)
testAddr := addrs[0]
tApp := app.NewTestApp()
tApp.InitializeFromGenesisStates(
app.NewAuthGenState(addrs, []sdk.Coins{cs(c(collateral, 100))}),
NewPricefeedGenState(collateral, d("1.00")),
NewCDPGenState(collateral, d("2.00")),
)
ctx := tApp.NewContext(false, abci.Header{})
keeper := tApp.GetCDPKeeper()
// Create CDP
err := keeper.ModifyCDP(ctx, testAddr, collateral, i(10), i(5))
require.NoError(t, err)
// Reduce price
tApp.GetPriceFeedKeeper().SetPrice(
ctx, sdk.AccAddress{}, collateral,
d("0.90"), time.Now().Add(1*time.Hour))
tApp.GetPriceFeedKeeper().SetCurrentPrices(ctx, collateral)
// Seize entire CDP
err = keeper.PartialSeizeCDP(ctx, testAddr, collateral, i(10), i(5))
// Check
require.NoError(t, err)
_, found := keeper.GetCDP(ctx, testAddr, collateral)
require.False(t, found)
collateralState, found := keeper.GetCollateralState(ctx, collateral)
require.True(t, found)
require.Equal(t, sdk.ZeroInt(), collateralState.TotalDebt)
}
func TestKeeper_GetCDPs(t *testing.T) {
// setup test app
tApp := app.NewTestApp().InitializeFromGenesisStates(
NewPricefeedGenStateMulti(), // collateral needs to be in pricefeed for cdp InitGenesis to validate
NewCDPGenStateMulti(),
)
ctx := tApp.NewContext(true, abci.Header{})
keeper := tApp.GetCDPKeeper()
// setup CDPs
_, addrs := app.GeneratePrivKeyAddressPairs(2)
cdps := types.CDPs{
{addrs[0], "xrp", i(4000), i(5)},
{addrs[1], "xrp", i(4000), i(2000)},
{addrs[0], "btc", i(10), i(20)},
}
for _, cdp := range cdps {
keeper.SetCDP(ctx, cdp)
}
// Check nil params returns all CDPs
returnedCdps, err := keeper.GetCDPs(ctx, "", sdk.Dec{})
require.NoError(t, err)
require.Equal(t,
types.CDPs{
{addrs[0], "btc", i(10), i(20)},
{addrs[1], "xrp", i(4000), i(2000)},
{addrs[0], "xrp", i(4000), i(5)}},
returnedCdps,
)
// Check correct CDPs filtered by collateral and sorted
returnedCdps, err = keeper.GetCDPs(ctx, "xrp", d("0.00000001"))
require.NoError(t, err)
require.Equal(t,
types.CDPs{
{addrs[1], "xrp", i(4000), i(2000)},
{addrs[0], "xrp", i(4000), i(5)}},
returnedCdps,
)
returnedCdps, err = keeper.GetCDPs(ctx, "xrp", sdk.Dec{})
require.NoError(t, err)
require.Equal(t,
types.CDPs{
{addrs[1], "xrp", i(4000), i(2000)},
{addrs[0], "xrp", i(4000), i(5)}},
returnedCdps,
)
returnedCdps, err = keeper.GetCDPs(ctx, "xrp", d("0.9"))
require.NoError(t, err)
require.Equal(t,
types.CDPs{
{addrs[1], "xrp", i(4000), i(2000)}},
returnedCdps,
)
// Check high price returns no CDPs
returnedCdps, err = keeper.GetCDPs(ctx, "xrp", d("999999999.99"))
require.NoError(t, err)
require.Equal(t,
types.CDPs(nil),
returnedCdps,
)
// Check unauthorized collateral denom returns error
_, err = keeper.GetCDPs(ctx, "a non existent coin", d("0.34023"))
require.Error(t, err)
// Check price without collateral returns error
_, err = keeper.GetCDPs(ctx, "", d("0.34023"))
require.Error(t, err)
// Check deleting a CDP removes it
keeper.DeleteCDP(ctx, cdps[0])
returnedCdps, err = keeper.GetCDPs(ctx, "", sdk.Dec{})
require.NoError(t, err)
require.Equal(t,
types.CDPs{
{addrs[0], "btc", i(10), i(20)},
{addrs[1], "xrp", i(4000), i(2000)}},
returnedCdps,
)
}
func TestKeeper_GetSetDeleteCDP(t *testing.T) {
// setup keeper, create CDP
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{})
keeper := tApp.GetCDPKeeper()
_, addrs := app.GeneratePrivKeyAddressPairs(1)
cdp := types.CDP{addrs[0], "xrp", i(412), i(56)}
// write and read from store
keeper.SetCDP(ctx, cdp)
readCDP, found := keeper.GetCDP(ctx, cdp.Owner, cdp.CollateralDenom)
// check before and after match
require.True(t, found)
require.Equal(t, cdp, readCDP)
// delete auction
keeper.DeleteCDP(ctx, cdp)
// check auction does not exist
_, found = keeper.GetCDP(ctx, cdp.Owner, cdp.CollateralDenom)
require.False(t, found)
}
func TestKeeper_GetSetGDebt(t *testing.T) {
// setup keeper, create GDebt
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{})
keeper := tApp.GetCDPKeeper()
gDebt := i(4120000)
// write and read from store
keeper.SetGlobalDebt(ctx, gDebt)
readGDebt := keeper.GetGlobalDebt(ctx)
// check before and after match
require.Equal(t, gDebt, readGDebt)
}
func TestKeeper_GetSetCollateralState(t *testing.T) {
// setup keeper, create CState
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{})
keeper := tApp.GetCDPKeeper()
collateralState := types.CollateralState{"xrp", i(15400)}
// write and read from store
keeper.SetCollateralState(ctx, collateralState)
readCState, found := keeper.GetCollateralState(ctx, collateralState.Denom)
// check before and after match
require.Equal(t, collateralState, readCState)
require.True(t, found)
}

View File

@ -1,19 +1,81 @@
package keeper package keeper
import ( import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/cdp/types" "github.com/kava-labs/kava/x/cdp/types"
) )
// ---------- Module Parameters ----------
// GetParams returns the params from the store // GetParams returns the params from the store
func (k Keeper) GetParams(ctx sdk.Context) types.CdpParams { func (k Keeper) GetParams(ctx sdk.Context) types.Params {
var p types.CdpParams var p types.Params
k.paramSubspace.GetParamSet(ctx, &p) k.paramSubspace.GetParamSet(ctx, &p)
return p return p
} }
// SetParams sets params on the store // SetParams sets params on the store
func (k Keeper) SetParams(ctx sdk.Context, cdpParams types.CdpParams) { func (k Keeper) SetParams(ctx sdk.Context, params types.Params) {
k.paramSubspace.SetParamSet(ctx, &cdpParams) k.paramSubspace.SetParamSet(ctx, &params)
}
// GetCollateral returns the collateral param with corresponding denom
func (k Keeper) GetCollateral(ctx sdk.Context, denom string) (types.CollateralParam, bool) {
params := k.GetParams(ctx)
for _, cp := range params.CollateralParams {
if cp.Denom == denom {
return cp, true
}
}
return types.CollateralParam{}, false
}
// GetDebt returns the debt param with matching denom
func (k Keeper) GetDebt(ctx sdk.Context, denom string) (types.DebtParam, bool) {
params := k.GetParams(ctx)
for _, dp := range params.DebtParams {
if dp.Denom == denom {
return dp, true
}
}
return types.DebtParam{}, false
}
// GetDenomPrefix returns the prefix of the matching denom
func (k Keeper) GetDenomPrefix(ctx sdk.Context, denom string) (byte, bool) {
params := k.GetParams(ctx)
for _, cp := range params.CollateralParams {
if cp.Denom == denom {
return cp.Prefix, true
}
}
return 0x00, false
}
// private methods 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 {
if cp.Prefix == db {
return cp.Denom
}
}
panic(fmt.Sprintf("no collateral denom with prefix %b", db))
}
func (k Keeper) getMarketID(ctx sdk.Context, denom string) string {
cp, found := k.GetCollateral(ctx, denom)
if !found {
panic(fmt.Sprintf("collateral not found: %s", denom))
}
return cp.MarketID
}
func (k Keeper) getLiquidationRatio(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.LiquidationRatio
} }

View File

@ -10,13 +10,16 @@ import (
"github.com/kava-labs/kava/x/cdp/types" "github.com/kava-labs/kava/x/cdp/types"
) )
// NewQuerier returns a new querier function
func NewQuerier(keeper Keeper) sdk.Querier { func NewQuerier(keeper Keeper) sdk.Querier {
return func(ctx sdk.Context, path []string, req abci.RequestQuery) (res []byte, err sdk.Error) { return func(ctx sdk.Context, path []string, req abci.RequestQuery) (res []byte, err sdk.Error) {
switch path[0] { switch path[0] {
case types.QueryGetCdp:
return queryGetCdp(ctx, req, keeper)
case types.QueryGetCdps: case types.QueryGetCdps:
return queryGetCdps(ctx, req, keeper) return queryGetCdpsByDenom(ctx, req, keeper)
case types.QueryGetCdpsByCollateralization:
return queryGetCdpsByRatio(ctx, req, keeper)
case types.QueryGetParams: case types.QueryGetParams:
return queryGetParams(ctx, req, keeper) return queryGetParams(ctx, req, keeper)
default: default:
@ -25,42 +28,45 @@ func NewQuerier(keeper Keeper) sdk.Querier {
} }
} }
// query a specific cdp
// queryGetCdps fetches CDPs, optionally filtering by any of the query params (in QueryCdpsParams). func queryGetCdp(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) {
// While CDPs do not have an ID, this method can be used to get one CDP by specifying the collateral and owner. var requestParams types.QueryCdpParams
func queryGetCdps(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) {
// Decode request
var requestParams types.QueryCdpsParams
err := keeper.cdc.UnmarshalJSON(req.Data, &requestParams) err := keeper.cdc.UnmarshalJSON(req.Data, &requestParams)
if err != nil { if err != nil {
return nil, sdk.ErrInternal(fmt.Sprintf("failed to parse params: %s", err)) return nil, sdk.ErrInternal(fmt.Sprintf("failed to parse params: %s", err))
} }
// Get CDPs _, valid := keeper.GetDenomPrefix(ctx, requestParams.CollateralDenom)
var cdps types.CDPs if !valid {
if len(requestParams.Owner) != 0 { return nil, types.ErrInvalidCollateralDenom(keeper.codespace, requestParams.CollateralDenom)
if len(requestParams.CollateralDenom) != 0 { }
// owner and collateral specified - get a single CDP
cdp, found := keeper.GetCDP(ctx, requestParams.Owner, requestParams.CollateralDenom) cdp, found := keeper.GetCdpByOwnerAndDenom(ctx, requestParams.Owner, requestParams.CollateralDenom)
if !found { if !found {
cdp = types.CDP{Owner: requestParams.Owner, CollateralDenom: requestParams.CollateralDenom, CollateralAmount: sdk.ZeroInt(), Debt: sdk.ZeroInt()} return nil, types.ErrCdpNotFound(keeper.codespace, requestParams.Owner, requestParams.CollateralDenom)
} }
cdps = types.CDPs{cdp}
} else { bz, err := codec.MarshalJSONIndent(keeper.cdc, cdp)
// owner, but no collateral specified - get all CDPs for one address if err != nil {
return nil, sdk.ErrInternal("getting all CDPs belonging to one owner not implemented") return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error()))
}
} else {
// owner not specified -- get all CDPs or all CDPs of one collateral type, optionally filtered by price
var errSdk sdk.Error // := doesn't work here
cdps, errSdk = keeper.GetCDPs(ctx, requestParams.CollateralDenom, requestParams.UnderCollateralizedAt)
if errSdk != nil {
return nil, errSdk
} }
return bz, nil
} }
// Encode results // query cdps with matching denom and ratio LESS THAN the input ratio
func queryGetCdpsByRatio(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) {
var requestParams types.QueryCdpsByRatioParams
err := keeper.cdc.UnmarshalJSON(req.Data, &requestParams)
if err != nil {
return nil, sdk.ErrInternal(fmt.Sprintf("failed to parse params: %s", err))
}
_, valid := keeper.GetDenomPrefix(ctx, requestParams.CollateralDenom)
if !valid {
return nil, types.ErrInvalidCollateralDenom(keeper.codespace, requestParams.CollateralDenom)
}
cdps := keeper.GetAllCdpsByDenomAndRatio(ctx, requestParams.CollateralDenom, requestParams.Ratio)
bz, err := codec.MarshalJSONIndent(keeper.cdc, cdps) bz, err := codec.MarshalJSONIndent(keeper.cdc, cdps)
if err != nil { if err != nil {
return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error())) return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error()))
@ -68,8 +74,27 @@ func queryGetCdps(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte
return bz, nil return bz, nil
} }
// queryGetParams fetches the cdp module parameters // query all cdps with matching collateral denom
// TODO does this need to exist? Can you use cliCtx.QueryStore instead? func queryGetCdpsByDenom(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) {
var requestParams types.QueryCdpsParams
err := keeper.cdc.UnmarshalJSON(req.Data, &requestParams)
if err != nil {
return nil, sdk.ErrInternal(fmt.Sprintf("failed to parse params: %s", err))
}
_, valid := keeper.GetDenomPrefix(ctx, requestParams.CollateralDenom)
if !valid {
return nil, types.ErrInvalidCollateralDenom(keeper.codespace, requestParams.CollateralDenom)
}
cdps := keeper.GetAllCdpsByDenom(ctx, requestParams.CollateralDenom)
bz, err := codec.MarshalJSONIndent(keeper.cdc, cdps)
if err != nil {
return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error()))
}
return bz, nil
}
// query params in the cdp store
func queryGetParams(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) { func queryGetParams(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) {
// Get params // Get params
params := keeper.GetParams(ctx) params := keeper.GetParams(ctx)

View File

@ -0,0 +1,226 @@
package keeper_test
import (
"math/rand"
"sort"
"strings"
"testing"
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/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"
)
const (
custom = "custom"
)
type QuerierTestSuite struct {
suite.Suite
keeper keeper.Keeper
addrs []sdk.AccAddress
app app.TestApp
cdps types.CDPs
ctx sdk.Context
querier sdk.Querier
}
func (suite *QuerierTestSuite) SetupTest() {
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
cdps := make(types.CDPs, 100)
_, addrs := app.GeneratePrivKeyAddressPairs(100)
coins := []sdk.Coins{}
for j := 0; j < 100; j++ {
coins = append(coins, cs(c("btc", 10000000000), c("xrp", 10000000000)))
}
authGS := app.NewAuthGenState(
addrs, coins)
tApp.InitializeFromGenesisStates(
authGS,
NewPricefeedGenStateMulti(),
NewCDPGenStateMulti(),
)
suite.ctx = ctx
suite.app = tApp
suite.keeper = tApp.GetCDPKeeper()
for j := 0; j < 100; j++ {
collateral := "xrp"
amount := simulation.RandIntBetween(rand.New(rand.NewSource(int64(j))), 2500000000, 9000000000)
debt := simulation.RandIntBetween(rand.New(rand.NewSource(int64(j))), 50000000, 250000000)
if j%2 == 0 {
collateral = "btc"
amount = simulation.RandIntBetween(rand.New(rand.NewSource(int64(j))), 500000000, 5000000000)
debt = simulation.RandIntBetween(rand.New(rand.NewSource(int64(j))), 1000000000, 25000000000)
}
suite.Nil(suite.keeper.AddCdp(suite.ctx, addrs[j], cs(c(collateral, int64(amount))), cs(c("usdx", int64(debt)))))
c, f := suite.keeper.GetCDP(suite.ctx, collateral, uint64(j+1))
suite.True(f)
cdps[j] = c
}
suite.cdps = cdps
suite.querier = keeper.NewQuerier(suite.keeper)
suite.addrs = addrs
}
func (suite *QuerierTestSuite) TestQueryCdp() {
ctx := suite.ctx.WithIsCheckTx(false)
query := abci.RequestQuery{
Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryGetCdp}, "/"),
Data: types.ModuleCdc.MustMarshalJSON(types.NewQueryCdpParams(suite.cdps[0].Owner, suite.cdps[0].Collateral[0].Denom)),
}
bz, err := suite.querier(ctx, []string{types.QueryGetCdp}, query)
suite.Nil(err)
suite.NotNil(bz)
var c types.CDP
suite.Nil(types.ModuleCdc.UnmarshalJSON(bz, &c))
suite.Equal(suite.cdps[0], c)
query = abci.RequestQuery{
Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryGetCdp}, "/"),
Data: types.ModuleCdc.MustMarshalJSON(types.NewQueryCdpParams(suite.cdps[0].Owner, "lol")),
}
_, err = suite.querier(ctx, []string{types.QueryGetCdp}, query)
suite.Error(err)
query = abci.RequestQuery{
Path: strings.Join([]string{custom, "nonsense"}, "/"),
Data: []byte("nonsense"),
}
_, err = suite.querier(ctx, []string{query.Path}, query)
suite.Error(err)
_, err = suite.querier(ctx, []string{types.QueryGetCdp}, query)
suite.Error(err)
query = abci.RequestQuery{
Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryGetCdp}, "/"),
Data: types.ModuleCdc.MustMarshalJSON(types.NewQueryCdpParams(suite.cdps[0].Owner, "xrp")),
}
_, err = suite.querier(ctx, []string{types.QueryGetCdp}, query)
suite.Error(err)
}
func (suite *QuerierTestSuite) TestQueryCdpsByDenom() {
ctx := suite.ctx.WithIsCheckTx(false)
query := abci.RequestQuery{
Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryGetCdps}, "/"),
Data: types.ModuleCdc.MustMarshalJSON(types.NewQueryCdpsParams(suite.cdps[0].Collateral[0].Denom)),
}
bz, err := suite.querier(ctx, []string{types.QueryGetCdps}, query)
suite.Nil(err)
suite.NotNil(bz)
var c types.CDPs
suite.Nil(types.ModuleCdc.UnmarshalJSON(bz, &c))
suite.Equal(50, len(c))
query = abci.RequestQuery{
Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryGetCdps}, "/"),
Data: types.ModuleCdc.MustMarshalJSON(types.NewQueryCdpsParams("lol")),
}
_, err = suite.querier(ctx, []string{types.QueryGetCdps}, query)
suite.Error(err)
}
func (suite *QuerierTestSuite) TestQueryCdpsByRatio() {
ratioCountBtc := 0
ratioCountXrp := 0
xrpRatio := d("50.0")
btcRatio := d("0.003")
expectedXrpIds := []int{}
expectedBtcIds := []int{}
for _, cdp := range suite.cdps {
r := suite.keeper.CalculateCollateralToDebtRatio(suite.ctx, cdp.Collateral, cdp.Principal)
if cdp.Collateral[0].Denom == "xrp" {
if r.LT(xrpRatio) {
ratioCountXrp += 1
expectedXrpIds = append(expectedXrpIds, int(cdp.ID))
}
} else {
if r.LT(btcRatio) {
ratioCountBtc += 1
expectedBtcIds = append(expectedBtcIds, int(cdp.ID))
}
}
}
ctx := suite.ctx.WithIsCheckTx(false)
query := abci.RequestQuery{
Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryGetCdpsByCollateralization}, "/"),
Data: types.ModuleCdc.MustMarshalJSON(types.NewQueryCdpsByRatioParams("xrp", xrpRatio)),
}
bz, err := suite.querier(ctx, []string{types.QueryGetCdpsByCollateralization}, query)
suite.Nil(err)
suite.NotNil(bz)
var c types.CDPs
actualXrpIds := []int{}
suite.Nil(types.ModuleCdc.UnmarshalJSON(bz, &c))
for _, k := range c {
actualXrpIds = append(actualXrpIds, int(k.ID))
}
sort.Ints(actualXrpIds)
suite.Equal(expectedXrpIds, actualXrpIds)
query = abci.RequestQuery{
Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryGetCdpsByCollateralization}, "/"),
Data: types.ModuleCdc.MustMarshalJSON(types.NewQueryCdpsByRatioParams("btc", btcRatio)),
}
bz, err = suite.querier(ctx, []string{types.QueryGetCdpsByCollateralization}, query)
suite.Nil(err)
suite.NotNil(bz)
c = types.CDPs{}
actualBtcIds := []int{}
suite.Nil(types.ModuleCdc.UnmarshalJSON(bz, &c))
for _, k := range c {
actualBtcIds = append(actualBtcIds, int(k.ID))
}
sort.Ints(actualBtcIds)
suite.Equal(expectedBtcIds, actualBtcIds)
query = abci.RequestQuery{
Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryGetCdpsByCollateralization}, "/"),
Data: types.ModuleCdc.MustMarshalJSON(types.NewQueryCdpsByRatioParams("xrp", d("0.003"))),
}
bz, err = suite.querier(ctx, []string{types.QueryGetCdpsByCollateralization}, query)
suite.Nil(err)
suite.NotNil(bz)
c = types.CDPs{}
suite.Nil(types.ModuleCdc.UnmarshalJSON(bz, &c))
suite.Equal(0, len(c))
}
func (suite *QuerierTestSuite) TestQueryParams() {
ctx := suite.ctx.WithIsCheckTx(false)
bz, err := suite.querier(ctx, []string{types.QueryGetParams}, abci.RequestQuery{})
suite.Nil(err)
suite.NotNil(bz)
var p types.Params
suite.Nil(types.ModuleCdc.UnmarshalJSON(bz, &p))
cdpGS := NewCDPGenStateMulti()
gs := types.GenesisState{}
types.ModuleCdc.UnmarshalJSON(cdpGS["cdp"], &gs)
suite.Equal(gs.Params, p)
}
func TestQuerierTestSuite(t *testing.T) {
suite.Run(t, new(QuerierTestSuite))
}

102
x/cdp/keeper/seize.go Normal file
View File

@ -0,0 +1,102 @@
package keeper
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/cdp/types"
)
// SeizeCollateral liquidates the collateral in the input cdp.
// the following operations are performed:
// 1. updates the fees for the input cdp,
// 2. sends collateral for all deposits from the cdp module to the liquidator module,
// 3. moves debt coins from the cdp module to the liquidator module,
// 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) {
// 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
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)
}
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)
if err != nil {
panic(err)
}
// Decrement total principal for this collateral type
for _, dc := range cdp.Principal {
feeAmount := cdp.AccumulatedFees.AmountOf(dc.Denom)
coinsToDecrement := sdk.NewCoins(dc)
if feeAmount.IsPositive() {
feeCoins := sdk.NewCoins(sdk.NewCoin(dc.Denom, feeAmount))
coinsToDecrement = coinsToDecrement.Add(feeCoins)
}
k.DecrementTotalPrincipal(ctx, cdp.Collateral[0].Denom, coinsToDecrement)
}
}
// HandleNewDebt compounds the accumulated fees for the input collateral and principal coins.
// the following operations are performed:
// 1. mints the fee coins in the liquidator module account,
// 2. mints the same amount of debt coins in the cdp module account
// 3. updates the total amount of principal for the input collateral type in the store,
func (k Keeper) HandleNewDebt(ctx sdk.Context, collateralDenom string, principalDenom string, periods sdk.Int) {
previousDebt := k.GetTotalPrincipal(ctx, collateralDenom, principalDenom)
feeCoins := sdk.NewCoins(sdk.NewCoin(principalDenom, previousDebt))
newFees := k.CalculateFees(ctx, feeCoins, periods, collateralDenom)
k.MintDebtCoins(ctx, types.ModuleName, k.GetDebtDenom(ctx), newFees)
k.supplyKeeper.MintCoins(ctx, types.LiquidatorMacc, newFees)
k.SetTotalPrincipal(ctx, collateralDenom, principalDenom, feeCoins.Add(newFees).AmountOf(principalDenom))
}
// 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) {
price, err := k.pricefeedKeeper.GetCurrentPrice(ctx, marketID)
if err != nil {
return
}
normalizedRatio := sdk.OneDec().Quo(price.Price.Quo(liquidationRatio))
cdpsToLiquidate := k.GetAllCdpsByDenomAndRatio(ctx, denom, normalizedRatio)
for _, c := range cdpsToLiquidate {
k.SeizeCollateral(ctx, c)
}
return
}

141
x/cdp/keeper/seize_test.go Normal file
View File

@ -0,0 +1,141 @@
package keeper_test
import (
"math/rand"
"testing"
"time"
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/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 SeizeTestSuite struct {
suite.Suite
keeper keeper.Keeper
addrs []sdk.AccAddress
app app.TestApp
cdps types.CDPs
ctx sdk.Context
liquidations liquidationTracker
}
type liquidationTracker struct {
xrp []uint64
btc []uint64
debt int64
}
func (suite *SeizeTestSuite) SetupTest() {
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
cdps := make(types.CDPs, 100)
_, addrs := app.GeneratePrivKeyAddressPairs(100)
coins := []sdk.Coins{}
tracker := liquidationTracker{}
for j := 0; j < 100; j++ {
coins = append(coins, cs(c("btc", 100000000), c("xrp", 10000000000)))
}
authGS := app.NewAuthGenState(
addrs, coins)
tApp.InitializeFromGenesisStates(
authGS,
NewPricefeedGenStateMulti(),
NewCDPGenStateMulti(),
)
suite.ctx = ctx
suite.app = tApp
suite.keeper = tApp.GetCDPKeeper()
for j := 0; j < 100; j++ {
collateral := "xrp"
amount := 10000000000
debt := simulation.RandIntBetween(rand.New(rand.NewSource(int64(j))), 750000000, 1249000000)
if j%2 == 0 {
collateral = "btc"
amount = 100000000
debt = simulation.RandIntBetween(rand.New(rand.NewSource(int64(j))), 2700000000, 5332000000)
if debt >= 4000000000 {
tracker.btc = append(tracker.btc, uint64(j+1))
tracker.debt += int64(debt)
}
} else {
if debt >= 1000000000 {
tracker.xrp = append(tracker.xrp, uint64(j+1))
tracker.debt += int64(debt)
}
}
err := suite.keeper.AddCdp(suite.ctx, addrs[j], cs(c(collateral, int64(amount))), cs(c("usdx", int64(debt))))
suite.NoError(err)
c, f := suite.keeper.GetCDP(suite.ctx, collateral, uint64(j+1))
suite.True(f)
cdps[j] = c
}
suite.cdps = cdps
suite.addrs = addrs
suite.liquidations = tracker
}
func (suite *SeizeTestSuite) setPrice(price sdk.Dec, market string) {
pfKeeper := suite.app.GetPriceFeedKeeper()
pfKeeper.SetPrice(suite.ctx, sdk.AccAddress{}, market, price, suite.ctx.BlockTime().Add(time.Hour*3))
err := pfKeeper.SetCurrentPrices(suite.ctx, market)
suite.NoError(err)
pp, err := pfKeeper.GetCurrentPrice(suite.ctx, market)
suite.NoError(err)
suite.Equal(price, pp.Price)
}
func (suite *SeizeTestSuite) TestSeizeCollateral() {
sk := suite.app.GetSupplyKeeper()
cdp, _ := suite.keeper.GetCDP(suite.ctx, "xrp", uint64(2))
p := cdp.Principal[0].Amount
cl := cdp.Collateral[0].Amount
tpb := suite.keeper.GetTotalPrincipal(suite.ctx, "xrp", "usdx")
suite.keeper.SeizeCollateral(suite.ctx, cdp)
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())
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)
}
func (suite *SeizeTestSuite) TestLiquidateCdps() {
sk := suite.app.GetSupplyKeeper()
acc := sk.GetModuleAccount(suite.ctx, types.ModuleName)
originalXrpCollateral := acc.GetCoins().AmountOf("xrp")
suite.setPrice(d("0.2"), "xrp:usd")
p, _ := suite.keeper.GetCollateral(suite.ctx, "xrp")
suite.keeper.LiquidateCdps(suite.ctx, "xrp:usd", "xrp", p.LiquidationRatio)
acc = sk.GetModuleAccount(suite.ctx, types.ModuleName)
finalXrpCollateral := acc.GetCoins().AmountOf("xrp")
seizedXrpCollateral := originalXrpCollateral.Sub(finalXrpCollateral)
xrpLiquidations := int(seizedXrpCollateral.Quo(i(10000000000)).Int64())
suite.Equal(len(suite.liquidations.xrp), xrpLiquidations)
}
func (suite *SeizeTestSuite) TestHandleNewDebt() {
tpb := suite.keeper.GetTotalPrincipal(suite.ctx, "xrp", "usdx")
suite.keeper.HandleNewDebt(suite.ctx, "xrp", "usdx", i(31536000))
tpa := suite.keeper.GetTotalPrincipal(suite.ctx, "xrp", "usdx")
suite.Equal(sdk.NewDec(tpb.Int64()).Mul(d("1.05")).TruncateInt().Int64(), tpa.Int64())
}
func TestSeizeTestSuite(t *testing.T) {
suite.Run(t, new(SeizeTestSuite))
}

View File

@ -41,12 +41,12 @@ func (AppModuleBasic) DefaultGenesis() json.RawMessage {
// ValidateGenesis module validate genesis // ValidateGenesis module validate genesis
func (AppModuleBasic) ValidateGenesis(bz json.RawMessage) error { func (AppModuleBasic) ValidateGenesis(bz json.RawMessage) error {
var data GenesisState var gs GenesisState
err := ModuleCdc.UnmarshalJSON(bz, &data) err := ModuleCdc.UnmarshalJSON(bz, &gs)
if err != nil { if err != nil {
return err return err
} }
return ValidateGenesis(data) return gs.Validate()
} }
// RegisterRESTRoutes registers the REST routes for the cdp module. // RegisterRESTRoutes registers the REST routes for the cdp module.
@ -124,9 +124,11 @@ func (am AppModule) ExportGenesis(ctx sdk.Context) json.RawMessage {
} }
// BeginBlock module begin-block // BeginBlock module begin-block
func (AppModule) BeginBlock(_ sdk.Context, _ abci.RequestBeginBlock) {} func (am AppModule) BeginBlock(ctx sdk.Context, req abci.RequestBeginBlock) {
BeginBlocker(ctx, req, am.keeper)
}
// EndBlock module end-block // EndBlock module end-block
func (am AppModule) EndBlock(ctx sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate { func (am AppModule) EndBlock(_ sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate {
return []abci.ValidatorUpdate{} return []abci.ValidatorUpdate{}
} }

64
x/cdp/types/cdp.go Normal file
View File

@ -0,0 +1,64 @@
package types
import (
"fmt"
"strings"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// CDP is the state of a single collateralized debt position.
type CDP struct {
ID uint64 `json:"id" yaml:"id"` // unique id for cdp
Owner sdk.AccAddress `json:"owner" yaml:"owner"` // Account that authorizes changes to the CDP
Collateral sdk.Coins `json:"collateral" yaml:"collateral"` // Amount of collateral stored in this CDP
Principal sdk.Coins `json:"principal" yaml:"principal"`
AccumulatedFees sdk.Coins `json:"accumulated_fees" yaml:"accumulated_fees"`
FeesUpdated time.Time `json:"fees_updated" yaml:"fees_updated"` // Amount of stable coin drawn from this CDP
}
// NewCDP creates a new CDP object
func NewCDP(id uint64, owner sdk.AccAddress, collateral sdk.Coins, principal sdk.Coins, time time.Time) CDP {
var fees sdk.Coins
return CDP{
ID: id,
Owner: owner,
Collateral: collateral,
Principal: principal,
AccumulatedFees: fees,
FeesUpdated: time,
}
}
// String implements fmt.stringer
func (cdp CDP) String() string {
return strings.TrimSpace(fmt.Sprintf(`CDP:
Owner: %s
ID: %d
Collateral Type: %s
Collateral: %s
Principal: %s
Fees: %s
Fees Last Updated: %s`,
cdp.Owner,
cdp.ID,
cdp.Collateral[0].Denom,
cdp.Collateral,
cdp.Principal,
cdp.AccumulatedFees,
cdp.FeesUpdated,
))
}
// CDPs a collection of CDP objects
type CDPs []CDP
// String implements stringer
func (cdps CDPs) String() string {
out := ""
for _, cdp := range cdps {
out += cdp.String() + "\n"
}
return out
}

View File

@ -12,8 +12,12 @@ func init() {
ModuleCdc = cdc.Seal() ModuleCdc = cdc.Seal()
} }
// RegisterCodec registers concrete types on the codec. // RegisterCodec registers the necessary types for cdp module
func RegisterCodec(cdc *codec.Codec) { func RegisterCodec(cdc *codec.Codec) {
cdc.RegisterConcrete(MsgCreateOrModifyCDP{}, "cdp/MsgCreateOrModifyCDP", nil) cdc.RegisterConcrete(MsgCreateCDP{}, "cdp/MsgCreateCDP", nil)
cdc.RegisterConcrete(MsgDeposit{}, "cdp/MsgDeposit", nil)
cdc.RegisterConcrete(MsgWithdraw{}, "cdp/MsgWithdraw", nil)
cdc.RegisterConcrete(MsgDrawDebt{}, "cdp/MsgDrawDebt", nil)
cdc.RegisterConcrete(MsgRepayDebt{}, "cdp/MsgRepayDebt", nil)
cdc.RegisterConcrete(MsgTransferCDP{}, "cdp/MsgTransferCDP", nil) cdc.RegisterConcrete(MsgTransferCDP{}, "cdp/MsgTransferCDP", nil)
} }

83
x/cdp/types/deposit.go Normal file
View File

@ -0,0 +1,83 @@
package types
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// 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))
}
}
// NewDeposit creates a new Deposit object
func NewDeposit(cdpID uint64, depositor sdk.AccAddress, amount sdk.Coins) Deposit {
return Deposit{cdpID, depositor, amount, false}
}
// 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)
}
// Deposits a collection of Deposit objects
type Deposits []Deposit
// String implements fmt.Stringer
func (ds Deposits) String() string {
if len(ds) == 0 {
return "[]"
}
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
}
// Equals returns whether two deposits are equal.
func (d Deposit) Equals(comp Deposit) bool {
return d.Depositor.Equals(comp.Depositor) && d.CdpID == comp.CdpID && d.Amount.IsEqual(comp.Amount)
}
// Empty returns whether a deposit is empty.
func (d Deposit) Empty() bool {
return d.Equals(Deposit{})
}

103
x/cdp/types/errors.go Normal file
View File

@ -0,0 +1,103 @@
// DONTCOVER
package types
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// Error codes specific to cdp module
const (
DefaultCodespace sdk.CodespaceType = ModuleName
CodeCdpAlreadyExists sdk.CodeType = 1
CodeCollateralLengthInvalid sdk.CodeType = 2
CodeCollateralNotSupported sdk.CodeType = 3
CodeDebtNotSupported sdk.CodeType = 4
CodeExceedsDebtLimit sdk.CodeType = 5
CodeInvalidCollateralRatio sdk.CodeType = 6
CodeCdpNotFound sdk.CodeType = 7
CodeDepositNotFound sdk.CodeType = 8
CodeInvalidDepositDenom sdk.CodeType = 9
CodeInvalidPaymentDenom sdk.CodeType = 10
CodeDepositNotAvailable sdk.CodeType = 11
CodeInvalidCollateralDenom sdk.CodeType = 12
CodeInvalidWithdrawAmount sdk.CodeType = 13
CodeCdpNotAvailable sdk.CodeType = 14
CodeBelowDebtFloor sdk.CodeType = 15
)
// ErrCdpAlreadyExists error for duplicate cdps
func ErrCdpAlreadyExists(codespace sdk.CodespaceType, owner sdk.AccAddress, denom string) sdk.Error {
return sdk.NewError(codespace, CodeCdpAlreadyExists, fmt.Sprintf("cdp for owner %s and collateral %s already exists", owner, denom))
}
// ErrInvalidCollateralLength error for invalid collateral input length
func ErrInvalidCollateralLength(codespace sdk.CodespaceType, length int) sdk.Error {
return sdk.NewError(codespace, CodeCollateralLengthInvalid, fmt.Sprintf("only one collateral type per cdp, has %d", length))
}
// ErrCollateralNotSupported error for unsupported collateral
func ErrCollateralNotSupported(codespace sdk.CodespaceType, denom string) sdk.Error {
return sdk.NewError(codespace, CodeCollateralNotSupported, fmt.Sprintf("collateral %s not supported", denom))
}
// ErrDebtNotSupported error for unsupported debt
func ErrDebtNotSupported(codespace sdk.CodespaceType, denom string) sdk.Error {
return sdk.NewError(codespace, CodeDebtNotSupported, fmt.Sprintf("collateral %s not supported", denom))
}
// ErrExceedsDebtLimit error for attempted draws that exceed debt limit
func ErrExceedsDebtLimit(codespace sdk.CodespaceType, proposed sdk.Coins, limit sdk.Coins) sdk.Error {
return sdk.NewError(codespace, CodeExceedsDebtLimit, fmt.Sprintf("proposed debt increase %s would exceed debt limit of %s", proposed, limit))
}
// ErrInvalidCollateralRatio error for attempted draws that are below liquidation ratio
func ErrInvalidCollateralRatio(codespace sdk.CodespaceType, denom string, collateralRatio sdk.Dec, liquidationRatio sdk.Dec) sdk.Error {
return sdk.NewError(codespace, CodeInvalidCollateralRatio, fmt.Sprintf("proposed collateral ratio of %s is below liqudation ratio of %s for collateral %s", collateralRatio, liquidationRatio, denom))
}
// ErrCdpNotFound error cdp not found
func ErrCdpNotFound(codespace sdk.CodespaceType, owner sdk.AccAddress, denom string) sdk.Error {
return sdk.NewError(codespace, CodeCdpNotFound, fmt.Sprintf("cdp for owner %s and collateral %s not found", owner, denom))
}
// ErrDepositNotFound error for deposit not found
func ErrDepositNotFound(codespace sdk.CodespaceType, depositor sdk.AccAddress, cdpID uint64) sdk.Error {
return sdk.NewError(codespace, CodeDepositNotFound, fmt.Sprintf("deposit for cdp %d not found for %s", cdpID, depositor))
}
// ErrInvalidDepositDenom error for invalid deposit denoms
func ErrInvalidDepositDenom(codespace sdk.CodespaceType, cdpID uint64, expected string, actual string) sdk.Error {
return sdk.NewError(codespace, CodeInvalidDepositDenom, fmt.Sprintf("invalid deposit for cdp %d, expects %s, got %s", cdpID, expected, actual))
}
// ErrInvalidPaymentDenom error for invalid payment denoms
func ErrInvalidPaymentDenom(codespace sdk.CodespaceType, cdpID uint64, expected []string, actual []string) sdk.Error {
return sdk.NewError(codespace, CodeInvalidPaymentDenom, fmt.Sprintf("invalid payment for cdp %d, expects %s, got %s", cdpID, expected, actual))
}
//ErrDepositNotAvailable error for withdrawing deposits in liquidation
func ErrDepositNotAvailable(codespace sdk.CodespaceType, cdpID uint64, depositor sdk.AccAddress) sdk.Error {
return sdk.NewError(codespace, CodeDepositNotAvailable, fmt.Sprintf("deposit from %s for cdp %d in liquidation", depositor, cdpID))
}
// ErrInvalidCollateralDenom error for invalid collateral denoms
func ErrInvalidCollateralDenom(codespace sdk.CodespaceType, denom string) sdk.Error {
return sdk.NewError(codespace, CodeInvalidDepositDenom, fmt.Sprintf("invalid denom: %s", denom))
}
// ErrInvalidWithdrawAmount error for invalid withdrawal amount
func ErrInvalidWithdrawAmount(codespace sdk.CodespaceType, withdraw sdk.Coins, deposit sdk.Coins) sdk.Error {
return sdk.NewError(codespace, CodeInvalidWithdrawAmount, fmt.Sprintf("withdrawal amount of %s exceeds deposit of %s", withdraw, deposit))
}
//ErrCdpNotAvailable error for depositing to a CDP in liquidation
func ErrCdpNotAvailable(codespace sdk.CodespaceType, cdpID uint64) sdk.Error {
return sdk.NewError(codespace, CodeCdpNotAvailable, fmt.Sprintf("cannot modify cdp %d, in liquidation", cdpID))
}
// ErrBelowDebtFloor error for creating a cdp with debt below the minimum
func ErrBelowDebtFloor(codespace sdk.CodespaceType, debt sdk.Coins, floor sdk.Int) sdk.Error {
return sdk.NewError(codespace, CodeBelowDebtFloor, fmt.Sprintf("proposed cdp debt of %s is below the minimum of %s", debt, floor))
}

16
x/cdp/types/events.go Normal file
View File

@ -0,0 +1,16 @@
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"
AttributeKeyCdpID = "cdp_id"
AttributeKeyDepositor = "depositor"
AttributeValueCategory = "cdp"
)

View File

@ -4,18 +4,28 @@ import (
"time" "time"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
supplyexported "github.com/cosmos/cosmos-sdk/x/supply/exported"
pftypes "github.com/kava-labs/kava/x/pricefeed/types" pftypes "github.com/kava-labs/kava/x/pricefeed/types"
) )
type BankKeeper interface { // SupplyKeeper defines the expected supply keeper for module accounts
GetCoins(sdk.Context, sdk.AccAddress) sdk.Coins type SupplyKeeper interface {
HasCoins(sdk.Context, sdk.AccAddress, sdk.Coins) bool GetModuleAddress(name string) sdk.AccAddress
AddCoins(sdk.Context, sdk.AccAddress, sdk.Coins) (sdk.Coins, sdk.Error) GetModuleAccount(ctx sdk.Context, name string) supplyexported.ModuleAccountI
SubtractCoins(sdk.Context, sdk.AccAddress, sdk.Coins) (sdk.Coins, sdk.Error)
// TODO remove with genesis 2-phases refactor https://github.com/cosmos/cosmos-sdk/issues/2862
SetModuleAccount(sdk.Context, supplyexported.ModuleAccountI)
SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) sdk.Error
SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) sdk.Error
SendCoinsFromModuleToModule(ctx sdk.Context, senderModule, recipientModule string, amt sdk.Coins) sdk.Error
BurnCoins(ctx sdk.Context, name string, amt sdk.Coins) sdk.Error
MintCoins(ctx sdk.Context, name string, amt sdk.Coins) sdk.Error
} }
// PricefeedKeeper defines the expected interface for the pricefeed
type PricefeedKeeper interface { type PricefeedKeeper interface {
GetCurrentPrice(sdk.Context, string) pftypes.CurrentPrice GetCurrentPrice(sdk.Context, string) (pftypes.CurrentPrice, sdk.Error)
GetParams(sdk.Context) pftypes.Params GetParams(sdk.Context) pftypes.Params
// These are used for testing TODO replace mockApp with keeper in tests to remove these // These are used for testing TODO replace mockApp with keeper in tests to remove these
SetParams(sdk.Context, pftypes.Params) SetParams(sdk.Context, pftypes.Params)

View File

@ -1,34 +1,61 @@
package types package types
import sdk "github.com/cosmos/cosmos-sdk/types" import (
"bytes"
"fmt"
"time"
)
// GenesisState is the state that must be provided at genesis. // GenesisState is the state that must be provided at genesis.
// TODO What is globaldebt and is is separate from the global debt limit in CdpParams
type GenesisState struct { type GenesisState struct {
Params CdpParams `json:"params" yaml:"params"` Params Params `json:"params" yaml:"params"`
GlobalDebt sdk.Int `json:"global_debt" yaml:"global_debt"`
CDPs CDPs `json:"cdps" yaml:"cdps"` CDPs CDPs `json:"cdps" yaml:"cdps"`
// don't need to setup CollateralStates as they are created as needed Deposits Deposits `json:"deposits" yaml:"deposits"`
StartingCdpID uint64 `json:"starting_cdp_id" yaml:"starting_cdp_id"`
DebtDenom string `json:"debt_denom" yaml:"debt_denom"`
PreviousBlockTime time.Time `json:"previous_block_time" yaml:"previous_block_time"`
} }
// DefaultGenesisState returns a default genesis state // DefaultGenesisState returns a default genesis state
// TODO make this empty, load test values independent
func DefaultGenesisState() GenesisState { func DefaultGenesisState() GenesisState {
return GenesisState{ return GenesisState{
Params: DefaultParams(), Params: DefaultParams(),
CDPs: CDPs{}, CDPs: CDPs{},
Deposits: Deposits{},
StartingCdpID: DefaultCdpStartingID,
DebtDenom: DefaultDebtDenom,
PreviousBlockTime: DefaultPreviousBlockTime,
} }
} }
// ValidateGenesis performs basic validation of genesis data returning an // Validate performs basic validation of genesis data returning an
// error for any failed validation criteria. // error for any failed validation criteria.
func ValidateGenesis(data GenesisState) error { func (gs GenesisState) Validate() error {
if err := data.Params.Validate(); err != nil { if err := gs.Params.Validate(); err != nil {
return err return err
} }
// check global debt is zero - force the chain to always start with zero stable coin, otherwise collateralStatus's will need to be set up as well. - what? This seems indefensible. if gs.PreviousBlockTime.Equal(time.Time{}) {
return fmt.Errorf("previous block time not set")
}
if gs.DebtDenom == "" {
return fmt.Errorf("debt denom not set")
}
return nil return nil
} }
// Equal checks whether two gov GenesisState structs are equivalent
func (gs GenesisState) Equal(gs2 GenesisState) bool {
b1 := ModuleCdc.MustMarshalBinaryBare(gs)
b2 := ModuleCdc.MustMarshalBinaryBare(gs2)
return bytes.Equal(b1, b2)
}
// IsEmpty returns true if a GenesisState is empty
func (gs GenesisState) IsEmpty() bool {
return gs.Equal(GenesisState{})
}

View File

@ -0,0 +1,22 @@
package types
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestEqualProposalID(t *testing.T) {
state1 := GenesisState{}
state2 := GenesisState{}
require.Equal(t, state1, state2)
// Proposals
state1.StartingCdpID = 1
require.NotEqual(t, state1, state2)
require.False(t, state1.Equal(state2))
state2.StartingCdpID = 1
require.Equal(t, state1, state2)
require.True(t, state1.Equal(state2))
}

View File

@ -1,5 +1,12 @@
package types package types
import (
"bytes"
"encoding/binary"
sdk "github.com/cosmos/cosmos-sdk/types"
)
const ( const (
// ModuleName The name that will be used throughout the module // ModuleName The name that will be used throughout the module
ModuleName = "cdp" ModuleName = "cdp"
@ -10,6 +17,161 @@ const (
// RouterKey Top level router key // RouterKey Top level router key
RouterKey = ModuleName RouterKey = ModuleName
// QuerierRoute Top level query string
QuerierRoute = ModuleName
// DefaultParamspace default name for parameter store // DefaultParamspace default name for parameter store
DefaultParamspace = ModuleName DefaultParamspace = ModuleName
// LiquidatorMacc module account for liquidator
LiquidatorMacc = "liquidator"
) )
var sep = []byte(":")
// Keys for cdp store
// Items are stored with the following key: values
// - 0x00<cdpOwner_Bytes>: []cdpID
// - One cdp owner can control one cdp per collateral type
// - 0x01<collateralDenomPrefix>:<cdpID_Bytes>: CDP
// - cdps are prefix by denom prefix so we can iterate over cdps of one type
// - uses : as separator
// - 0x02<collateralDenomPrefix>:<collateralDebtRatio_Bytes>:<cdpID_Bytes>: cdpID
// - Ox03: nextCdpID
// - 0x04: debtDenom
// - 0x05<depositState>:<cdpID>:<depositorAddr_bytes>: Deposit
// - 0x06<denom>:totalPrincipal
// - 0x07<denom>:feeRate
// - 0x08:previousBlockTime
// KVStore key prefixes
var (
CdpIDKeyPrefix = []byte{0x00}
CdpKeyPrefix = []byte{0x01}
CollateralRatioIndexPrefix = []byte{0x02}
CdpIDKey = []byte{0x03}
DebtDenomKey = []byte{0x04}
DepositKeyPrefix = []byte{0x05}
PrincipalKeyPrefix = []byte{0x06}
AccumulatorKeyPrefix = []byte{0x07}
PreviousBlockTimeKey = []byte{0x08}
)
var lenPositiveDec = len(SortableDecBytes(sdk.OneDec()))
var lenNegativeDec = len(SortableDecBytes(sdk.OneDec().Neg()))
// GetCdpIDBytes returns the byte representation of the cdpID
func GetCdpIDBytes(cdpID uint64) (cdpIDBz []byte) {
cdpIDBz = make([]byte, 8)
binary.BigEndian.PutUint64(cdpIDBz, cdpID)
return
}
// GetCdpIDFromBytes returns cdpID in uint64 format from a byte array
func GetCdpIDFromBytes(bz []byte) (cdpID uint64) {
return binary.BigEndian.Uint64(bz)
}
// CdpKey key of a specific cdp in the store
func CdpKey(denomByte byte, cdpID uint64) []byte {
return createKey([]byte{denomByte}, sep, GetCdpIDBytes(cdpID))
}
// SplitCdpKey returns the component parts of a cdp key
func SplitCdpKey(key []byte) (byte, uint64) {
split := bytes.Split(key, sep)
return split[0][0], GetCdpIDFromBytes(split[1])
}
// DenomIterKey returns the key for iterating over cdps of a certain denom in the store
func DenomIterKey(denomByte byte) []byte {
return append([]byte{denomByte}, sep...)
}
// SplitDenomIterKey returns the component part of a key for iterating over cdps by denom
func SplitDenomIterKey(key []byte) byte {
split := bytes.Split(key, sep)
return split[0][0]
}
// 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)
}
// 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
}
// 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))
}
// 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
}
// CollateralRatioBytes returns the liquidation ratio as sortable bytes
func CollateralRatioBytes(ratio sdk.Dec) []byte {
ok := ValidSortableDec(ratio)
if !ok {
// set to max sortable if input is too large.
ratio = sdk.OneDec().Quo(sdk.SmallestDec())
}
return SortableDecBytes(ratio)
}
// CollateralRatioKey returns the key for querying a cdp by its liquidation ratio
func CollateralRatioKey(denomByte byte, cdpID uint64, ratio sdk.Dec) []byte {
ratioBytes := CollateralRatioBytes(ratio)
idBytes := GetCdpIDBytes(cdpID)
return createKey([]byte{denomByte}, sep, ratioBytes, sep, idBytes)
}
// SplitCollateralRatioKey split the collateral ratio key and return the denom, cdp id, and collateral:debt ratio
func SplitCollateralRatioKey(key []byte) (denom byte, cdpID uint64, ratio sdk.Dec) {
cdpID = GetCdpIDFromBytes(key[len(key)-8 : len(key)])
split := bytes.Split(key[:len(key)-8], sep)
denom = split[0][0]
ratio, err := ParseDecBytes(split[1])
if err != nil {
panic(err)
}
return
}
// CollateralRatioIterKey returns the key for iterating over cdps by denom and liquidation ratio
func CollateralRatioIterKey(denomByte byte, ratio sdk.Dec) []byte {
ratioBytes := CollateralRatioBytes(ratio)
return createKey([]byte{denomByte}, sep, ratioBytes)
}
// SplitCollateralRatioIterKey split the collateral ratio key and return the denom, cdp id, and collateral:debt ratio
func SplitCollateralRatioIterKey(key []byte) (denom byte, ratio sdk.Dec) {
split := bytes.Split(key, sep)
denom = split[0][0]
ratio, err := ParseDecBytes(split[1])
if err != nil {
panic(err)
}
return
}
func createKey(bytes ...[]byte) (r []byte) {
for _, b := range bytes {
r = append(r, b...)
}
return
}

65
x/cdp/types/keys_test.go Normal file
View File

@ -0,0 +1,65 @@
package types
import (
"testing"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/require"
"github.com/tendermint/tendermint/crypto/ed25519"
)
var addr = sdk.AccAddress(ed25519.GenPrivKey().PubKey().Address())
func TestKeys(t *testing.T) {
key := CdpKey(0x01, 2)
db, id := SplitCdpKey(key)
require.Equal(t, int(id), 2)
require.Equal(t, byte(0x01), db)
denomKey := DenomIterKey(0x01)
db = SplitDenomIterKey(denomKey)
require.Equal(t, byte(0x01), db)
depositKey := DepositKey(StatusNil, 2, addr)
status, 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)
require.Equal(t, 2, int(id))
require.Equal(t, StatusLiquidated, status)
require.Panics(t, func() { SplitDepositIterKey(append([]byte{0x03}, GetCdpIDBytes(2)...)) })
collateralKey := CollateralRatioKey(0x01, 2, sdk.MustNewDecFromStr("1.50"))
db, id, ratio := SplitCollateralRatioKey(collateralKey)
require.Equal(t, byte(0x01), db)
require.Equal(t, int(id), 2)
require.Equal(t, ratio, sdk.MustNewDecFromStr("1.50"))
bigRatio := sdk.OneDec().Quo(sdk.SmallestDec()).Mul(sdk.OneDec().Add(sdk.OneDec()))
collateralKey = CollateralRatioKey(0x01, 2, bigRatio)
db, id, ratio = SplitCollateralRatioKey(collateralKey)
require.Equal(t, ratio, MaxSortableDec)
collateralIterKey := CollateralRatioIterKey(0x01, sdk.MustNewDecFromStr("1.50"))
db, ratio = SplitCollateralRatioIterKey(collateralIterKey)
require.Equal(t, byte(0x01), db)
require.Equal(t, ratio, sdk.MustNewDecFromStr("1.50"))
require.Panics(t, func() { SplitCollateralRatioKey(badRatioKey()) })
require.Panics(t, func() { SplitCollateralRatioIterKey(badRatioIterKey()) })
}
func badRatioKey() []byte {
r := append(append(append(append([]byte{0x01}, sep...), []byte("nonsense")...), sep...), []byte{0xff}...)
return r
}
func badRatioIterKey() []byte {
r := append(append([]byte{0x01}, sep...), []byte("nonsense")...)
return r
}

View File

@ -1,54 +1,327 @@
package types package types
import ( import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
) )
// MsgCreateOrModifyCDP creates, adds/removes collateral/stable coin from a cdp // ensure Msg interface compliance at compile time
// TODO Make this more user friendly - maybe split into four functions. var (
type MsgCreateOrModifyCDP struct { _ sdk.Msg = &MsgCreateCDP{}
Sender sdk.AccAddress _ sdk.Msg = &MsgDeposit{}
CollateralDenom string _ sdk.Msg = &MsgWithdraw{}
CollateralChange sdk.Int _ sdk.Msg = &MsgDrawDebt{}
DebtChange sdk.Int _ sdk.Msg = &MsgRepayDebt{}
)
// MsgCreateCDP creates a cdp
type MsgCreateCDP struct {
Sender sdk.AccAddress `json:"sender" yaml:"sender"`
Collateral sdk.Coins `json:"collateral" yaml:"collateral"`
Principal sdk.Coins `json:"principal" yaml:"principal"`
} }
// NewMsgPlaceBid returns a new MsgPlaceBid. // NewMsgCreateCDP returns a new MsgPlaceBid.
func NewMsgCreateOrModifyCDP(sender sdk.AccAddress, collateralDenom string, collateralChange sdk.Int, debtChange sdk.Int) MsgCreateOrModifyCDP { func NewMsgCreateCDP(sender sdk.AccAddress, collateral sdk.Coins, principal sdk.Coins) MsgCreateCDP {
return MsgCreateOrModifyCDP{ return MsgCreateCDP{
Sender: sender, Sender: sender,
CollateralDenom: collateralDenom, Collateral: collateral,
CollateralChange: collateralChange, Principal: principal,
DebtChange: debtChange,
} }
} }
// Route return the message type used for routing the message. // Route return the message type used for routing the message.
func (msg MsgCreateOrModifyCDP) Route() string { return "cdp" } func (msg MsgCreateCDP) Route() string { return RouterKey }
// Type returns a human-readable string for the message, intended for utilization within tags. // Type returns a human-readable string for the message, intended for utilization within tags.
func (msg MsgCreateOrModifyCDP) Type() string { return "create_modify_cdp" } // TODO snake case? func (msg MsgCreateCDP) Type() string { return "create_cdp" }
// ValidateBasic does a simple validation check that doesn't require access to any other information. // ValidateBasic does a simple validation check that doesn't require access to any other information.
func (msg MsgCreateOrModifyCDP) ValidateBasic() sdk.Error { func (msg MsgCreateCDP) ValidateBasic() sdk.Error {
if msg.Sender.Empty() { if msg.Sender.Empty() {
return sdk.ErrInternal("invalid (empty) sender address") return sdk.ErrInternal("invalid (empty) sender address")
} }
// TODO check coin denoms if len(msg.Collateral) != 1 {
return sdk.ErrInvalidCoins(fmt.Sprintf("cdps do not support multiple collateral types: received %s", msg.Collateral))
}
if !msg.Collateral.IsValid() {
return sdk.ErrInvalidCoins(msg.Collateral.String())
}
if !msg.Collateral.IsAllPositive() {
return sdk.ErrInvalidCoins(msg.Collateral.String())
}
if !msg.Principal.IsValid() {
return sdk.ErrInvalidCoins(msg.Principal.String())
}
if !msg.Principal.IsAllPositive() {
return sdk.ErrInvalidCoins(msg.Collateral.String())
}
return nil return nil
} }
// GetSignBytes gets the canonical byte representation of the Msg. // GetSignBytes gets the canonical byte representation of the Msg.
func (msg MsgCreateOrModifyCDP) GetSignBytes() []byte { func (msg MsgCreateCDP) GetSignBytes() []byte {
bz := ModuleCdc.MustMarshalJSON(msg) bz := ModuleCdc.MustMarshalJSON(msg)
return sdk.MustSortJSON(bz) return sdk.MustSortJSON(bz)
} }
// GetSigners returns the addresses of signers that must sign. // GetSigners returns the addresses of signers that must sign.
func (msg MsgCreateOrModifyCDP) GetSigners() []sdk.AccAddress { func (msg MsgCreateCDP) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{msg.Sender} return []sdk.AccAddress{msg.Sender}
} }
// String implements the Stringer interface
func (msg MsgCreateCDP) String() string {
return fmt.Sprintf(`Create CDP Message:
Sender: %s
Collateral: %s
Principal: %s
`, msg.Sender, msg.Collateral, msg.Principal)
}
// MsgDeposit deposit collateral to an existing cdp.
type MsgDeposit struct {
Depositor sdk.AccAddress `json:"depositor" yaml:"depositor"`
Owner sdk.AccAddress `json:"owner" yaml:"owner"`
Collateral sdk.Coins `json:"collateral" yaml:"collateral"`
}
// NewMsgDeposit returns a new MsgDeposit
func NewMsgDeposit(owner sdk.AccAddress, depositor sdk.AccAddress, collateral sdk.Coins) MsgDeposit {
return MsgDeposit{
Owner: owner,
Depositor: depositor,
Collateral: collateral,
}
}
// Route return the message type used for routing the message.
func (msg MsgDeposit) Route() string { return RouterKey }
// Type returns a human-readable string for the message, intended for utilization within tags.
func (msg MsgDeposit) Type() string { return "deposit_cdp" }
// ValidateBasic does a simple validation check that doesn't require access to any other information.
func (msg MsgDeposit) ValidateBasic() sdk.Error {
if msg.Owner.Empty() {
return sdk.ErrInternal("invalid (empty) sender address")
}
if msg.Depositor.Empty() {
return sdk.ErrInternal("invalid (empty) owner address")
}
if len(msg.Collateral) != 1 {
return sdk.ErrInvalidCoins(fmt.Sprintf("cdps do not support multiple collateral types: received %s", msg.Collateral))
}
if !msg.Collateral.IsValid() {
return sdk.ErrInvalidCoins(msg.Collateral.String())
}
if !msg.Collateral.IsAllPositive() {
return sdk.ErrInvalidCoins(msg.Collateral.String())
}
return nil
}
// GetSignBytes gets the canonical byte representation of the Msg.
func (msg MsgDeposit) GetSignBytes() []byte {
bz := ModuleCdc.MustMarshalJSON(msg)
return sdk.MustSortJSON(bz)
}
// GetSigners returns the addresses of signers that must sign.
func (msg MsgDeposit) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{msg.Depositor}
}
// String implements the Stringer interface
func (msg MsgDeposit) String() string {
return fmt.Sprintf(`Deposit to CDP Message:
Sender: %s
Owner: %s
Collateral: %s
`, msg.Owner, msg.Owner, msg.Collateral)
}
// MsgWithdraw withdraw collateral from an existing cdp.
type MsgWithdraw struct {
Depositor sdk.AccAddress `json:"depositor" yaml:"depositor"`
Owner sdk.AccAddress `json:"owner" yaml:"owner"`
Collateral sdk.Coins `json:"collateral" yaml:"collateral"`
}
// NewMsgWithdraw returns a new MsgDeposit
func NewMsgWithdraw(owner sdk.AccAddress, depositor sdk.AccAddress, collateral sdk.Coins) MsgWithdraw {
return MsgWithdraw{
Owner: owner,
Depositor: depositor,
Collateral: collateral,
}
}
// Route return the message type used for routing the message.
func (msg MsgWithdraw) Route() string { return RouterKey }
// Type returns a human-readable string for the message, intended for utilization within tags.
func (msg MsgWithdraw) Type() string { return "withdraw_cdp" }
// ValidateBasic does a simple validation check that doesn't require access to any other information.
func (msg MsgWithdraw) ValidateBasic() sdk.Error {
if msg.Owner.Empty() {
return sdk.ErrInternal("invalid (empty) sender address")
}
if msg.Depositor.Empty() {
return sdk.ErrInternal("invalid (empty) owner address")
}
if len(msg.Collateral) != 1 {
return sdk.ErrInvalidCoins(fmt.Sprintf("cdps do not support multiple collateral types: received %s", msg.Collateral))
}
if !msg.Collateral.IsValid() {
return sdk.ErrInvalidCoins(msg.Collateral.String())
}
if !msg.Collateral.IsAllPositive() {
return sdk.ErrInvalidCoins(msg.Collateral.String())
}
return nil
}
// GetSignBytes gets the canonical byte representation of the Msg.
func (msg MsgWithdraw) GetSignBytes() []byte {
bz := ModuleCdc.MustMarshalJSON(msg)
return sdk.MustSortJSON(bz)
}
// GetSigners returns the addresses of signers that must sign.
func (msg MsgWithdraw) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{msg.Depositor}
}
// String implements the Stringer interface
func (msg MsgWithdraw) String() string {
return fmt.Sprintf(`Withdraw from CDP Message:
Owner: %s
Depositor: %s
Collateral: %s
`, msg.Owner, msg.Depositor, msg.Collateral)
}
// MsgDrawDebt draw coins off of collateral in cdp
type MsgDrawDebt struct {
Sender sdk.AccAddress `json:"sender" yaml:"sender"`
CdpDenom string `json:"cdp_denom" yaml:"cdp_denom"`
Principal sdk.Coins `json:"principal" yaml:"principal"`
}
// NewMsgDrawDebt returns a new MsgDrawDebt
func NewMsgDrawDebt(sender sdk.AccAddress, denom string, principal sdk.Coins) MsgDrawDebt {
return MsgDrawDebt{
Sender: sender,
CdpDenom: denom,
Principal: principal,
}
}
// Route return the message type used for routing the message.
func (msg MsgDrawDebt) Route() string { return RouterKey }
// Type returns a human-readable string for the message, intended for utilization within tags.
func (msg MsgDrawDebt) Type() string { return "draw_cdp" }
// ValidateBasic does a simple validation check that doesn't require access to any other information.
func (msg MsgDrawDebt) ValidateBasic() sdk.Error {
if msg.Sender.Empty() {
return sdk.ErrInternal("invalid (empty) sender address")
}
if msg.CdpDenom == "" {
return sdk.ErrInternal("invalid (empty) cdp denom")
}
if !msg.Principal.IsValid() {
return sdk.ErrInvalidCoins(msg.Principal.String())
}
if !msg.Principal.IsAllPositive() {
return sdk.ErrInvalidCoins(msg.Principal.String())
}
return nil
}
// GetSignBytes gets the canonical byte representation of the Msg.
func (msg MsgDrawDebt) GetSignBytes() []byte {
bz := ModuleCdc.MustMarshalJSON(msg)
return sdk.MustSortJSON(bz)
}
// GetSigners returns the addresses of signers that must sign.
func (msg MsgDrawDebt) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{msg.Sender}
}
// String implements the Stringer interface
func (msg MsgDrawDebt) String() string {
return fmt.Sprintf(`Draw debt from CDP Message:
Sender: %s
CDP Denom: %s
Principal: %s
`, msg.Sender, msg.CdpDenom, msg.Principal)
}
// MsgRepayDebt repay debt drawn off the collateral in a CDP
type MsgRepayDebt struct {
Sender sdk.AccAddress `json:"sender" yaml:"sender"`
CdpDenom string `json:"cdp_denom" yaml:"cdp_denom"`
Payment sdk.Coins `json:"payment" yaml:"payment"`
}
// NewMsgRepayDebt returns a new MsgRepayDebt
func NewMsgRepayDebt(sender sdk.AccAddress, denom string, payment sdk.Coins) MsgRepayDebt {
return MsgRepayDebt{
Sender: sender,
CdpDenom: denom,
Payment: payment,
}
}
// Route return the message type used for routing the message.
func (msg MsgRepayDebt) Route() string { return RouterKey }
// Type returns a human-readable string for the message, intended for utilization within tags.
func (msg MsgRepayDebt) Type() string { return "repay_cdp" }
// ValidateBasic does a simple validation check that doesn't require access to any other information.
func (msg MsgRepayDebt) ValidateBasic() sdk.Error {
if msg.Sender.Empty() {
return sdk.ErrInternal("invalid (empty) sender address")
}
if msg.CdpDenom == "" {
return sdk.ErrInternal("invalid (empty) cdp denom")
}
if !msg.Payment.IsValid() {
return sdk.ErrInvalidCoins(msg.Payment.String())
}
if !msg.Payment.IsAllPositive() {
return sdk.ErrInvalidCoins(msg.Payment.String())
}
return nil
}
// GetSignBytes gets the canonical byte representation of the Msg.
func (msg MsgRepayDebt) GetSignBytes() []byte {
bz := ModuleCdc.MustMarshalJSON(msg)
return sdk.MustSortJSON(bz)
}
// GetSigners returns the addresses of signers that must sign.
func (msg MsgRepayDebt) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{msg.Sender}
}
// String implements the Stringer interface
func (msg MsgRepayDebt) String() string {
return fmt.Sprintf(`Draw debt from CDP Message:
Sender: %s
CDP Denom: %s
Payment: %s
`, msg.Sender, msg.CdpDenom, msg.Payment)
}
// MsgTransferCDP changes the ownership of a cdp // MsgTransferCDP changes the ownership of a cdp
type MsgTransferCDP struct { type MsgTransferCDP struct {
// TODO // TODO

166
x/cdp/types/msg_test.go Normal file
View File

@ -0,0 +1,166 @@
package types
import (
"testing"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/require"
)
var (
coinsSingle = sdk.NewCoins(sdk.NewInt64Coin(sdk.DefaultBondDenom, 1000))
coinsZero = sdk.NewCoins()
coinsMulti = sdk.NewCoins(sdk.NewInt64Coin(sdk.DefaultBondDenom, 1000), sdk.NewInt64Coin("foo", 10000)).Sort()
addrs = []sdk.AccAddress{
sdk.AccAddress("test1"),
sdk.AccAddress("test2"),
}
)
func TestMsgCreateCDP(t *testing.T) {
tests := []struct {
description string
sender sdk.AccAddress
collateral sdk.Coins
principal sdk.Coins
expectPass bool
}{
{"create cdp", addrs[0], coinsSingle, coinsSingle, true},
{"create cdp multi debt", addrs[0], coinsSingle, coinsMulti, true},
{"create cdp no collateral", addrs[0], coinsZero, coinsSingle, false},
{"create cdp no debt", addrs[0], coinsSingle, coinsZero, false},
{"create cdp multi collateral", addrs[0], coinsMulti, coinsSingle, false},
{"create cdp empty owner", sdk.AccAddress{}, coinsSingle, coinsSingle, false},
}
for i, tc := range tests {
msg := NewMsgCreateCDP(
tc.sender,
tc.collateral,
tc.principal,
)
if tc.expectPass {
require.NoError(t, msg.ValidateBasic(), "test: %v", i)
} else {
require.Error(t, msg.ValidateBasic(), "test: %v", i)
}
}
}
func TestMsgDeposit(t *testing.T) {
tests := []struct {
description string
sender sdk.AccAddress
depositor sdk.AccAddress
collateral sdk.Coins
expectPass bool
}{
{"deposit", addrs[0], addrs[1], coinsSingle, true},
{"deposit", addrs[0], addrs[0], coinsSingle, true},
{"deposit no collateral", addrs[0], addrs[1], coinsZero, false},
{"deposit multi collateral", addrs[0], addrs[1], coinsMulti, false},
{"deposit empty owner", sdk.AccAddress{}, addrs[1], coinsSingle, false},
{"deposit empty depositor", addrs[0], sdk.AccAddress{}, coinsSingle, false},
}
for i, tc := range tests {
msg := NewMsgDeposit(
tc.sender,
tc.depositor,
tc.collateral,
)
if tc.expectPass {
require.NoError(t, msg.ValidateBasic(), "test: %v", i)
} else {
require.Error(t, msg.ValidateBasic(), "test: %v", i)
}
}
}
func TestMsgWithdraw(t *testing.T) {
tests := []struct {
description string
sender sdk.AccAddress
depositor sdk.AccAddress
collateral sdk.Coins
expectPass bool
}{
{"withdraw", addrs[0], addrs[1], coinsSingle, true},
{"withdraw", addrs[0], addrs[0], coinsSingle, true},
{"withdraw no collateral", addrs[0], addrs[1], coinsZero, false},
{"withdraw multi collateral", addrs[0], addrs[1], coinsMulti, false},
{"withdraw empty owner", sdk.AccAddress{}, addrs[1], coinsSingle, false},
{"withdraw empty depositor", addrs[0], sdk.AccAddress{}, coinsSingle, false},
}
for i, tc := range tests {
msg := NewMsgWithdraw(
tc.sender,
tc.depositor,
tc.collateral,
)
if tc.expectPass {
require.NoError(t, msg.ValidateBasic(), "test: %v", i)
} else {
require.Error(t, msg.ValidateBasic(), "test: %v", i)
}
}
}
func TestMsgDrawDebt(t *testing.T) {
tests := []struct {
description string
sender sdk.AccAddress
denom string
principal sdk.Coins
expectPass bool
}{
{"draw debt", addrs[0], sdk.DefaultBondDenom, coinsSingle, true},
{"draw debt no debt", addrs[0], sdk.DefaultBondDenom, coinsZero, false},
{"draw debt multi debt", addrs[0], sdk.DefaultBondDenom, coinsMulti, true},
{"draw debt empty owner", sdk.AccAddress{}, sdk.DefaultBondDenom, coinsSingle, false},
{"draw debt empty denom", sdk.AccAddress{}, "", coinsSingle, false},
}
for i, tc := range tests {
msg := NewMsgDrawDebt(
tc.sender,
tc.denom,
tc.principal,
)
if tc.expectPass {
require.NoError(t, msg.ValidateBasic(), "test: %v", i)
} else {
require.Error(t, msg.ValidateBasic(), "test: %v", i)
}
}
}
func TestMsgRepayDebt(t *testing.T) {
tests := []struct {
description string
sender sdk.AccAddress
denom string
payment sdk.Coins
expectPass bool
}{
{"repay debt", addrs[0], sdk.DefaultBondDenom, coinsSingle, true},
{"repay debt no payment", addrs[0], sdk.DefaultBondDenom, coinsZero, false},
{"repay debt multi payment", addrs[0], sdk.DefaultBondDenom, coinsMulti, true},
{"repay debt empty owner", sdk.AccAddress{}, sdk.DefaultBondDenom, coinsSingle, false},
{"repay debt empty denom", sdk.AccAddress{}, "", coinsSingle, false},
}
for i, tc := range tests {
msg := NewMsgRepayDebt(
tc.sender,
tc.denom,
tc.payment,
)
if tc.expectPass {
require.NoError(t, msg.ValidateBasic(), "test: %v", i)
} else {
require.Error(t, msg.ValidateBasic(), "test: %v", i)
}
}
}

View File

@ -2,137 +2,215 @@ package types
import ( import (
"fmt" "fmt"
"time"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/params/subspace" "github.com/cosmos/cosmos-sdk/x/params"
tmtime "github.com/tendermint/tendermint/types/time"
) )
/*
How this uses the sdk params module:
- Put all the params for this module in one struct `CDPModuleParams`
- Store this in the keeper's paramSubspace under one key
- Provide a function to load the param struct all at once `keeper.GetParams(ctx)`
It's possible to set individual key value pairs within a paramSubspace, but reading and setting them is awkward (an empty variable needs to be created, then Get writes the value into it)
This approach will be awkward if we ever need to write individual parameters (because they're stored all together). If this happens do as the sdk modules do - store parameters separately with custom get/set func for each.
*/
// CdpParams governance parameters for cdp module
type CdpParams struct {
GlobalDebtLimit sdk.Int
CollateralParams []CollateralParams
StableDenoms []string
}
// CollateralParams governance parameters for each collateral type within the cdp module
type CollateralParams struct {
Denom string // Coin name of collateral type
LiquidationRatio sdk.Dec // The ratio (Collateral (priced in stable coin) / Debt) under which a CDP will be liquidated
DebtLimit sdk.Int // Maximum amount of debt allowed to be drawn from this collateral type
//DebtFloor sdk.Int // used to prevent dust
}
// Parameter keys // Parameter keys
var ( var (
// ParamStoreKeyAuctionParams Param store key for auction params
KeyGlobalDebtLimit = []byte("GlobalDebtLimit") KeyGlobalDebtLimit = []byte("GlobalDebtLimit")
KeyCollateralParams = []byte("CollateralParams") KeyCollateralParams = []byte("CollateralParams")
KeyStableDenoms = []byte("StableDenoms") KeyDebtParams = []byte("DebtParams")
KeyCircuitBreaker = []byte("CircuitBreaker")
DefaultGlobalDebt = sdk.Coins{}
DefaultCircuitBreaker = false
DefaultCollateralParams = CollateralParams{}
DefaultDebtParams = DebtParams{}
DefaultCdpStartingID = uint64(1)
DefaultDebtDenom = "debt"
DefaultPreviousBlockTime = tmtime.Canonical(time.Unix(0, 0))
minCollateralPrefix = 0
maxCollateralPrefix = 255
) )
// 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"`
}
// String implements fmt.Stringer
func (p Params) String() string {
return fmt.Sprintf(`Params:
Global Debt Limit: %s
Collateral Params: %s
Debt Params: %s
Circuit Breaker: %t`,
p.GlobalDebtLimit, p.CollateralParams, p.DebtParams, p.CircuitBreaker,
)
}
// NewParams returns a new params object
func NewParams(debtLimit sdk.Coins, collateralParams CollateralParams, debtParams DebtParams, breaker bool) Params {
return Params{
GlobalDebtLimit: debtLimit,
CollateralParams: collateralParams,
DebtParams: debtParams,
CircuitBreaker: breaker,
}
}
// DefaultParams returns default params for cdp module
func DefaultParams() Params {
return NewParams(DefaultGlobalDebt, DefaultCollateralParams, DefaultDebtParams, 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
}
// String implements fmt.Stringer
func (cp CollateralParam) String() string {
return fmt.Sprintf(`Collateral:
Denom: %s
Liquidation Ratio: %s
Stability Fee: %s
Debt Limit: %s
Prefix: %b
Market ID: %s
Conversion Factor: %s`,
cp.Denom, cp.LiquidationRatio, cp.StabilityFee, cp.DebtLimit, cp.Prefix, cp.MarketID, cp.ConversionFactor)
}
// CollateralParams array of CollateralParam
type CollateralParams []CollateralParam
// String implements fmt.Stringer
func (cps CollateralParams) String() string {
out := "Collateral Params\n"
for _, cp := range cps {
out += fmt.Sprintf("%s\n", cp)
}
return out
}
// DebtParam governance params for debt assets
type DebtParam struct {
Denom string `json:"denom" yaml:"denom"`
ReferenceAsset string `json:"reference_asset" yaml:"reference_asset"`
DebtLimit sdk.Coins `json:"debt_limit" yaml:"debt_limit"`
ConversionFactor sdk.Int `json:"conversion_factor" yaml:"conversion_factor"`
DebtFloor sdk.Int `json:"debt_floor" yaml:"debt_floor"` // minimum active loan size, used to prevent dust
}
func (dp DebtParam) String() string {
return fmt.Sprintf(`Debt:
Denom: %s
Reference Asset: %s
Debt Limit: %s
Conversion Factor: %s
Debt Floot %s`, dp.Denom, dp.ReferenceAsset, dp.DebtLimit, dp.ConversionFactor, dp.DebtFloor)
}
// DebtParams array of DebtParam
type DebtParams []DebtParam
// String implements fmt.Stringer
func (dps DebtParams) String() string {
out := "Debt Params\n"
for _, dp := range dps {
out += fmt.Sprintf("%s\n", dp)
}
return out
}
// ParamKeyTable Key declaration for parameters // ParamKeyTable Key declaration for parameters
func ParamKeyTable() subspace.KeyTable { func ParamKeyTable() params.KeyTable {
return subspace.NewKeyTable().RegisterParamSet(&CdpParams{}) return params.NewKeyTable().RegisterParamSet(&Params{})
} }
// ParamSetPairs implements the ParamSet interface and returns all the key/value pairs // ParamSetPairs implements the ParamSet interface and returns all the key/value pairs
// pairs of auth module's parameters. // pairs of auth module's parameters.
// nolint // nolint
func (p *CdpParams) ParamSetPairs() subspace.ParamSetPairs { func (p *Params) ParamSetPairs() params.ParamSetPairs {
return subspace.ParamSetPairs{ return params.ParamSetPairs{
{KeyGlobalDebtLimit, &p.GlobalDebtLimit}, {Key: KeyGlobalDebtLimit, Value: &p.GlobalDebtLimit},
{KeyCollateralParams, &p.CollateralParams}, {Key: KeyCollateralParams, Value: &p.CollateralParams},
{KeyStableDenoms, &p.StableDenoms}, {Key: KeyDebtParams, Value: &p.DebtParams},
{Key: KeyCircuitBreaker, Value: &p.CircuitBreaker},
} }
} }
// String implements fmt.Stringer
func (p CdpParams) String() string {
out := fmt.Sprintf(`Params:
Global Debt Limit: %s
Collateral Params:`,
p.GlobalDebtLimit,
)
for _, cp := range p.CollateralParams {
out += fmt.Sprintf(`
%s
Liquidation Ratio: %s
Debt Limit: %s`,
cp.Denom,
cp.LiquidationRatio,
cp.DebtLimit,
)
}
return out
}
// GetCollateralParams returns params for a specific collateral denom
func (p CdpParams) GetCollateralParams(collateralDenom string) CollateralParams {
// search for matching denom, return
for _, cp := range p.CollateralParams {
if cp.Denom == collateralDenom {
return cp
}
}
// panic if not found, to be safe
panic("collateral params not found in module params")
}
// IsCollateralPresent returns true if the denom is among the collaterals in cdp module
func (p CdpParams) IsCollateralPresent(collateralDenom string) bool {
// search for matching denom, return
for _, cp := range p.CollateralParams {
if cp.Denom == collateralDenom {
return true
}
}
return false
}
// Validate checks that the parameters have valid values. // Validate checks that the parameters have valid values.
func (p CdpParams) Validate() error { func (p Params) Validate() error {
debtDenoms := make(map[string]int)
debtParamsDebtLimit := sdk.Coins{}
for _, dp := range p.DebtParams {
_, found := debtDenoms[dp.Denom]
if found {
return fmt.Errorf("duplicate debt denom: %s", dp.Denom)
}
debtDenoms[dp.Denom] = 1
if dp.DebtLimit.IsAnyNegative() {
return fmt.Errorf("debt limit for all debt tokens should be positive, is %s for %s", dp.DebtLimit, dp.Denom)
}
debtParamsDebtLimit = debtParamsDebtLimit.Add(dp.DebtLimit)
}
if !debtParamsDebtLimit.DenomsSubsetOf(p.GlobalDebtLimit) {
return fmt.Errorf("debt denom not found in global debt limit:\n\tglobal debt limit: %s\n\tdebt limits: %s",
p.GlobalDebtLimit, debtParamsDebtLimit)
}
if debtParamsDebtLimit.IsAnyGT(p.GlobalDebtLimit) {
return fmt.Errorf("debt limit exceeds global debt limit:\n\tglobal debt limit: %s\n\tdebt limits: %s",
p.GlobalDebtLimit, debtParamsDebtLimit)
}
collateralDupMap := make(map[string]int) collateralDupMap := make(map[string]int)
denomDupMap := make(map[string]int) prefixDupMap := make(map[int]int)
for _, collateral := range p.CollateralParams { collateralParamsDebtLimit := sdk.Coins{}
_, found := collateralDupMap[collateral.Denom] for _, cp := range p.CollateralParams {
prefix := int(cp.Prefix)
if prefix < minCollateralPrefix || prefix > maxCollateralPrefix {
return fmt.Errorf("invalid prefix for collateral denom %s: %b", cp.Denom, cp.Prefix)
}
_, found := prefixDupMap[prefix]
if found { if found {
return fmt.Errorf("duplicate denom: %s", collateral.Denom) return fmt.Errorf("duplicate prefix for collateral denom %s: %v", cp.Denom, []byte{cp.Prefix})
}
collateralDupMap[collateral.Denom] = 1
if collateral.DebtLimit.IsNegative() {
return fmt.Errorf("debt limit should be positive, is %s for %s", collateral.DebtLimit, collateral.Denom)
} }
// TODO do we want to enforce overcollateralization at this level? -- probably not, as it's technically a governance thing (kevin) prefixDupMap[prefix] = 1
} _, found = collateralDupMap[cp.Denom]
if p.GlobalDebtLimit.IsNegative() {
return fmt.Errorf("global debt limit should be positive, is %s", p.GlobalDebtLimit)
}
for _, denom := range p.StableDenoms {
_, found := denomDupMap[denom]
if found { if found {
return fmt.Errorf("duplicate stable denom: %s", denom) return fmt.Errorf("duplicate collateral denom: %s", cp.Denom)
} }
denomDupMap[denom] = 1 collateralDupMap[cp.Denom] = 1
if cp.DebtLimit.IsAnyNegative() {
return fmt.Errorf("debt limit for all collaterals should be positive, is %s for %s", cp.DebtLimit, cp.Denom)
}
collateralParamsDebtLimit = collateralParamsDebtLimit.Add(cp.DebtLimit)
for _, dc := range cp.DebtLimit {
_, found := debtDenoms[dc.Denom]
if !found {
return fmt.Errorf("debt limit for collateral %s contains invalid debt denom %s", cp.Denom, dc.Denom)
}
}
if cp.DebtLimit.IsAnyGT(p.GlobalDebtLimit) {
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 collateralParamsDebtLimit.IsAnyGT(p.GlobalDebtLimit) {
return fmt.Errorf("collateral debt limit exceeds global debt limit:\n\tglobal debt limit: %s\n\tcollateral debt limits: %s",
p.GlobalDebtLimit, collateralParamsDebtLimit)
}
if p.GlobalDebtLimit.IsAnyNegative() {
return fmt.Errorf("global debt limit should be positive for all debt tokens, is %s", p.GlobalDebtLimit)
} }
return nil return nil
} }
func DefaultParams() CdpParams {
return CdpParams{
GlobalDebtLimit: sdk.NewInt(0),
CollateralParams: []CollateralParams{},
StableDenoms: []string{"usdx"},
}
}

View File

@ -2,24 +2,55 @@ package types
import ( import (
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/rest"
) )
// Querier routes for the cdp module
const ( const (
QueryGetCdp = "cdp"
QueryGetCdps = "cdps" QueryGetCdps = "cdps"
QueryGetCdpsByCollateralization = "ratio"
QueryGetParams = "params" QueryGetParams = "params"
RestOwner = "owner" RestOwner = "owner"
RestCollateralDenom = "collateralDenom" RestCollateralDenom = "collateral-denom"
RestUnderCollateralizedAt = "underCollateralizedAt" RestRatio = "ratio"
) )
// QueryCdpsParams params for query /cdp/cdps
type QueryCdpsParams struct { type QueryCdpsParams struct {
CollateralDenom string // get CDPs with this collateral denom CollateralDenom string // get CDPs with this collateral denom
Owner sdk.AccAddress // get CDPs belonging to this owner
UnderCollateralizedAt sdk.Dec // get CDPs that will be below the liquidation ratio when the collateral is at this price.
} }
type ModifyCdpRequestBody struct { // NewQueryCdpsParams returns QueryCdpsParams
BaseReq rest.BaseReq `json:"base_req"` func NewQueryCdpsParams(denom string) QueryCdpsParams {
Cdp CDP `json:"cdp"` return QueryCdpsParams{
CollateralDenom: denom,
}
}
// NewQueryCdpParams returns QueryCdpParams
func NewQueryCdpParams(owner sdk.AccAddress, denom string) QueryCdpParams {
return QueryCdpParams{
Owner: owner,
CollateralDenom: denom,
}
}
// QueryCdpParams params for query /cdp/cdp
type QueryCdpParams struct {
CollateralDenom string // get CDPs with this collateral denom
Owner sdk.AccAddress // get CDPs belonging to this owner
}
// QueryCdpsByRatioParams params for query /cdp/cdps/ratio
type QueryCdpsByRatioParams struct {
CollateralDenom string // get CDPs with this collateral denom
Ratio sdk.Dec // get CDPs below this collateral:debt ratio
}
// NewQueryCdpsByRatioParams returns QueryCdpsByRatioParams
func NewQueryCdpsByRatioParams(denom string, ratio sdk.Dec) QueryCdpsByRatioParams {
return QueryCdpsByRatioParams{
CollateralDenom: denom,
Ratio: ratio,
}
} }

View File

@ -1,77 +0,0 @@
package types
import (
"fmt"
"strings"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// GovDenom asset code of the governance coin
const GovDenom = "kava"
// CDP is the state of a single Collateralized Debt Position.
type CDP struct {
//ID []byte // removing IDs for now to make things simpler
Owner sdk.AccAddress `json:"owner" yaml:"owner"` // Account that authorizes changes to the CDP
CollateralDenom string `json:"collateral_denom" yaml:"collateral_denom"` // Type of collateral stored in this CDP
CollateralAmount sdk.Int `json:"collateral_amount" yaml:"collateral_amount"` // Amount of collateral stored in this CDP
Debt sdk.Int `json:"debt" yaml:"debt"` // Amount of stable coin drawn from this CDP
}
func (cdp CDP) IsUnderCollateralized(price sdk.Dec, liquidationRatio sdk.Dec) bool {
collateralValue := sdk.NewDecFromInt(cdp.CollateralAmount).Mul(price)
minCollateralValue := liquidationRatio.Mul(sdk.NewDecFromInt(cdp.Debt))
return collateralValue.LT(minCollateralValue) // TODO LT or LTE?
}
func (cdp CDP) String() string {
return strings.TrimSpace(fmt.Sprintf(`CDP:
Owner: %s
Collateral: %s
Debt: %s`,
cdp.Owner,
sdk.NewCoin(cdp.CollateralDenom, cdp.CollateralAmount),
sdk.NewCoin("usdx", cdp.Debt),
))
}
type CDPs []CDP
// String implements stringer
func (cdps CDPs) String() string {
out := ""
for _, cdp := range cdps {
out += cdp.String() + "\n"
}
return out
}
// ByCollateralRatio is used to sort CDPs
type ByCollateralRatio CDPs
func (cdps ByCollateralRatio) Len() int { return len(cdps) }
func (cdps ByCollateralRatio) Swap(i, j int) { cdps[i], cdps[j] = cdps[j], cdps[i] }
func (cdps ByCollateralRatio) Less(i, j int) bool {
// Sort by "collateral ratio" ie collateralAmount/Debt
// The comparison is: collat_i/debt_i < collat_j/debt_j
// But to avoid division this can be rearranged to: collat_i*debt_j < collat_j*debt_i
// Provided the values are positive, so check for positive values.
if cdps[i].CollateralAmount.IsNegative() ||
cdps[i].Debt.IsNegative() ||
cdps[j].CollateralAmount.IsNegative() ||
cdps[j].Debt.IsNegative() {
panic("negative collateral and debt not supported in CDPs")
}
// TODO overflows could cause panics
left := cdps[i].CollateralAmount.Mul(cdps[j].Debt)
right := cdps[j].CollateralAmount.Mul(cdps[i].Debt)
return left.LT(right)
}
// CollateralState stores global information tied to a particular collateral type.
type CollateralState struct {
Denom string `json:"denom" yaml:"denom"` // Type of collateral
TotalDebt sdk.Int `json:"total_debt" yaml:"total_debt"` // total debt collateralized by a this coin type
//AccumulatedFees sdk.Int // Ignoring fees for now
}

99
x/cdp/types/utils.go Normal file
View File

@ -0,0 +1,99 @@
package types
import (
"bytes"
"fmt"
"strings"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// MaxSortableDec largest sortable sdk.Dec
var MaxSortableDec = sdk.OneDec().Quo(sdk.SmallestDec())
// ValidSortableDec sdk.Dec can't have precision of less than 10^-18
func ValidSortableDec(dec sdk.Dec) bool {
return dec.Abs().LTE(MaxSortableDec)
}
// SortableDecBytes returns a byte slice representation of a Dec that can be sorted.
// Left and right pads with 0s so there are 18 digits to left and right of the decimal point.
// For this reason, there is a maximum and minimum value for this, enforced by ValidSortableDec.
func SortableDecBytes(dec sdk.Dec) []byte {
if !ValidSortableDec(dec) {
panic("dec must be within bounds")
}
// Instead of adding an extra byte to all sortable decs in order to handle max sortable, we just
// makes its bytes be "max" which comes after all numbers in ASCIIbetical order
if dec.Equal(MaxSortableDec) {
return []byte("max")
}
// For the same reason, we make the bytes of minimum sortable dec be --, which comes before all numbers.
if dec.Equal(MaxSortableDec.Neg()) {
return []byte("--")
}
// We move the negative sign to the front of all the left padded 0s, to make negative numbers come before positive numbers
if dec.IsNegative() {
return append([]byte("-"), []byte(fmt.Sprintf(fmt.Sprintf("%%0%ds", sdk.Precision*2+1), dec.Abs().String()))...)
}
return []byte(fmt.Sprintf(fmt.Sprintf("%%0%ds", sdk.Precision*2+1), dec.String()))
}
// ParseDecBytes parses a []byte encoded using SortableDecBytes back to sdk.Dec
func ParseDecBytes(db []byte) (sdk.Dec, error) {
strFromDecBytes := strings.Trim(string(db[:]), "0")
if string(strFromDecBytes[0]) == "." {
strFromDecBytes = "0" + strFromDecBytes
}
if string(strFromDecBytes[len(strFromDecBytes)-1]) == "." {
strFromDecBytes = strFromDecBytes + "0"
}
if bytes.Equal(db, []byte("max")) {
return MaxSortableDec, nil
}
if bytes.Equal(db, []byte("--")) {
return MaxSortableDec.Neg(), nil
}
dec, err := sdk.NewDecFromStr(strFromDecBytes)
if err != nil {
return sdk.Dec{}, err
}
return dec, nil
}
// RelativePow raises x to the power of n, where x (and the result, z) are scaled by factor b.
// For example, RelativePow(210, 2, 100) = 441 (2.1^2 = 4.41)
// Only defined for positive ints.
func RelativePow(x sdk.Int, n sdk.Int, b sdk.Int) (z sdk.Int) {
if x.IsZero() {
if n.IsZero() {
z = b // 0^0 = 1
return
}
z = sdk.ZeroInt() // otherwise 0^a = 0
return
}
z = x
if n.Mod(sdk.NewInt(2)).Equal(sdk.ZeroInt()) {
z = b
}
halfOfB := b.Quo(sdk.NewInt(2))
n = n.Quo(sdk.NewInt(2))
for n.GT(sdk.ZeroInt()) {
xSquared := x.Mul(x)
xSquaredRounded := xSquared.Add(halfOfB)
x = xSquaredRounded.Quo(b)
if n.Mod(sdk.NewInt(2)).Equal(sdk.OneInt()) {
zx := z.Mul(x)
zxRounded := zx.Add(halfOfB)
z = zxRounded.Quo(b)
}
n = n.Quo(sdk.NewInt(2))
}
return
}

83
x/cdp/types/utils_test.go Normal file
View File

@ -0,0 +1,83 @@
package types
import (
"testing"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSortableDecBytes(t *testing.T) {
tests := []struct {
d sdk.Dec
want []byte
}{
{sdk.NewDec(0), []byte("000000000000000000.000000000000000000")},
{sdk.NewDec(1), []byte("000000000000000001.000000000000000000")},
{sdk.MustNewDecFromStr("2.0"), []byte("000000000000000002.000000000000000000")},
{sdk.MustNewDecFromStr("-2.0"), []byte("-000000000000000002.000000000000000000")},
{sdk.NewDec(10), []byte("000000000000000010.000000000000000000")},
{sdk.NewDec(12340), []byte("000000000000012340.000000000000000000")},
{sdk.NewDecWithPrec(12340, 4), []byte("000000000000000001.234000000000000000")},
{sdk.NewDecWithPrec(12340, 5), []byte("000000000000000000.123400000000000000")},
{sdk.NewDecWithPrec(12340, 8), []byte("000000000000000000.000123400000000000")},
{sdk.NewDecWithPrec(1009009009009009009, 17), []byte("000000000000000010.090090090090090090")},
{sdk.NewDecWithPrec(-1009009009009009009, 17), []byte("-000000000000000010.090090090090090090")},
{sdk.NewDec(1000000000000000000), []byte("max")},
{sdk.NewDec(-1000000000000000000), []byte("--")},
}
for tcIndex, tc := range tests {
assert.Equal(t, tc.want, SortableDecBytes(tc.d), "bad String(), index: %v", tcIndex)
}
assert.Panics(t, func() { SortableDecBytes(sdk.NewDec(1000000000000000001)) })
assert.Panics(t, func() { SortableDecBytes(sdk.NewDec(-1000000000000000001)) })
}
func TestParseSortableDecBytes(t *testing.T) {
tests := []struct {
d sdk.Dec
want []byte
}{
{sdk.NewDec(0), []byte("000000000000000000.000000000000000000")},
{sdk.NewDec(1), []byte("000000000000000001.000000000000000000")},
{sdk.MustNewDecFromStr("2.0"), []byte("000000000000000002.000000000000000000")},
{sdk.MustNewDecFromStr("-2.0"), []byte("-000000000000000002.000000000000000000")},
{sdk.NewDec(10), []byte("000000000000000010.000000000000000000")},
{sdk.NewDec(12340), []byte("000000000000012340.000000000000000000")},
{sdk.NewDecWithPrec(12340, 4), []byte("000000000000000001.234000000000000000")},
{sdk.NewDecWithPrec(12340, 5), []byte("000000000000000000.123400000000000000")},
{sdk.NewDecWithPrec(12340, 8), []byte("000000000000000000.000123400000000000")},
{sdk.NewDecWithPrec(1009009009009009009, 17), []byte("000000000000000010.090090090090090090")},
{sdk.NewDecWithPrec(-1009009009009009009, 17), []byte("-000000000000000010.090090090090090090")},
{sdk.NewDec(1000000000000000000), []byte("max")},
{sdk.NewDec(-1000000000000000000), []byte("--")},
}
for tcIndex, tc := range tests {
b := SortableDecBytes(tc.d)
r, err := ParseDecBytes(b)
assert.NoError(t, err)
assert.Equal(t, tc.d, r, "bad Dec(), index: %v", tcIndex)
}
}
func TestRelativePow(t *testing.T) {
tests := []struct {
args []sdk.Int
want sdk.Int
}{
{[]sdk.Int{sdk.ZeroInt(), sdk.ZeroInt(), sdk.OneInt()}, sdk.OneInt()},
{[]sdk.Int{sdk.ZeroInt(), sdk.ZeroInt(), sdk.NewInt(10)}, sdk.NewInt(10)},
{[]sdk.Int{sdk.ZeroInt(), sdk.OneInt(), sdk.NewInt(10)}, sdk.ZeroInt()},
{[]sdk.Int{sdk.NewInt(10), sdk.NewInt(2), sdk.OneInt()}, sdk.NewInt(100)},
{[]sdk.Int{sdk.NewInt(210), sdk.NewInt(2), sdk.NewInt(100)}, sdk.NewInt(441)},
{[]sdk.Int{sdk.NewInt(2100), sdk.NewInt(2), sdk.NewInt(1000)}, sdk.NewInt(4410)},
{[]sdk.Int{sdk.NewInt(1000000001547125958), sdk.NewInt(600), sdk.NewInt(1000000000000000000)}, sdk.NewInt(1000000928276004850)},
}
for i, tc := range tests {
res := RelativePow(tc.args[0], tc.args[1], tc.args[2])
require.Equal(t, tc.want, res, "unexpected result for test case %d, input: %v, got: %v", i, tc.args, res)
}
}

View File

@ -1,52 +0,0 @@
// nolint
// autogenerated code using github.com/rigelrozanski/multitool
// aliases generated for the following subdirectories:
// ALIASGEN: github.com/kava-labs/kava/x/liquidator/types/
// ALIASGEN: github.com/kava-labs/kava/x/liquidator/keeper/
package liquidator
import (
"github.com/kava-labs/kava/x/liquidator/keeper"
"github.com/kava-labs/kava/x/liquidator/types"
)
const (
ModuleName = types.ModuleName
StoreKey = types.StoreKey
RouterKey = types.RouterKey
QuerierRoute = types.QuerierRoute
DefaultParamspace = types.DefaultParamspace
QueryGetOutstandingDebt = types.QueryGetOutstandingDebt
)
var (
// functions aliases
RegisterCodec = types.RegisterCodec
DefaultGenesisState = types.DefaultGenesisState
ValidateGenesis = types.ValidateGenesis
NewLiquidatorParams = types.NewLiquidatorParams
ParamKeyTable = types.ParamKeyTable
DefaultParams = types.DefaultParams
NewKeeper = keeper.NewKeeper
NewQuerier = keeper.NewQuerier
// variable aliases
ModuleCdc = types.ModuleCdc
KeyDebtAuctionSize = types.KeyDebtAuctionSize
KeyCollateralParams = types.KeyCollateralParams
)
type (
CdpKeeper = types.CdpKeeper
BankKeeper = types.BankKeeper
AuctionKeeper = types.AuctionKeeper
GenesisState = types.GenesisState
MsgSeizeAndStartCollateralAuction = types.MsgSeizeAndStartCollateralAuction
MsgStartDebtAuction = types.MsgStartDebtAuction
LiquidatorParams = types.LiquidatorParams
CollateralParams = types.CollateralParams
SeizeAndStartCollateralAuctionRequest = types.SeizeAndStartCollateralAuctionRequest
StartDebtAuctionRequest = types.StartDebtAuctionRequest
SeizedDebt = types.SeizedDebt
Keeper = keeper.Keeper
)

View File

@ -1,49 +0,0 @@
package cli
import (
"fmt"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/spf13/cobra"
"github.com/kava-labs/kava/x/liquidator/types"
)
// GetQueryCmd returns the cli query commands for this module
func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
// Group nameservice queries under a subcommand
queryCmd := &cobra.Command{
Use: "liquidator",
Short: "Querying commands for the cdp liquidator",
}
queryCmd.AddCommand(client.GetCommands(
GetCmdGetOutstandingDebt(queryRoute, cdc),
)...)
return queryCmd
}
// GetCmdGetOutstandingDebt queries for the remaining available debt in the liquidator module after settlement with the module's stablecoin balance.
func GetCmdGetOutstandingDebt(queryRoute string, cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "debt",
Short: "get the outstanding seized debt",
Long: "Get the remaining available debt after settlement with the liquidator's stable coin balance.",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetOutstandingDebt), nil)
if err != nil {
return err
}
var outstandingDebt sdk.Int
cdc.MustUnmarshalJSON(res, &outstandingDebt)
return cliCtx.PrintOutput(outstandingDebt)
},
}
}

View File

@ -1,98 +0,0 @@
package cli
import (
"github.com/spf13/cobra"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth"
"github.com/cosmos/cosmos-sdk/x/auth/client/utils"
"github.com/kava-labs/kava/x/liquidator/types"
)
// GetTxCmd returns the transaction commands for this module
func GetTxCmd(cdc *codec.Codec) *cobra.Command {
txCmd := &cobra.Command{
Use: "liquidator",
Short: "liquidator transactions subcommands",
}
txCmd.AddCommand(client.PostCommands(
GetCmdSeizeAndStartCollateralAuction(cdc),
GetCmdStartDebtAuction(cdc),
)...)
return txCmd
}
// GetCmdSeizeAndStartCollateralAuction seize funds from a CDP and send to auction
func GetCmdSeizeAndStartCollateralAuction(cdc *codec.Codec) *cobra.Command {
cmd := &cobra.Command{
Use: "seize [cdp-owner] [collateral-denom]",
Short: "",
Long: `Seize a fixed amount of collateral and debt from a CDP then start an auction with the collateral.
The amount of collateral seized is given by the 'AuctionSize' module parameter or, if there isn't enough collateral in the CDP, all the CDP's collateral is seized.
Debt is seized in proportion to the collateral seized so that the CDP stays at the same collateral to debt ratio.
A 'forward-reverse' auction is started selling the seized collateral for some stable coin, with a maximum bid of stable coin set to equal the debt seized.
As this is a forward-reverse auction type, if the max stable coin is bid then bidding continues by bidding down the amount of collateral taken by the bidder. At the end, extra collateral is returned to the original CDP owner.`,
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc))
// Validate inputs
sender := cliCtx.GetFromAddress()
cdpOwner, err := sdk.AccAddressFromBech32(args[0])
if err != nil {
return err
}
denom := args[1]
// TODO validate denom?
// Prepare and send message
msg := types.MsgSeizeAndStartCollateralAuction{
Sender: sender,
CdpOwner: cdpOwner,
CollateralDenom: denom,
}
err = msg.ValidateBasic()
if err != nil {
return err
}
return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
},
}
return cmd
}
func GetCmdStartDebtAuction(cdc *codec.Codec) *cobra.Command {
cmd := &cobra.Command{
Use: "mint",
Short: "start a debt auction, minting gov coin to cover debt",
Long: "Start a reverse auction, selling off minted gov coin to raise a fixed amount of stable coin.",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc))
sender := cliCtx.GetFromAddress()
// Prepare and send message
msg := types.MsgStartDebtAuction{
Sender: sender,
}
err := msg.ValidateBasic()
if err != nil {
return err
}
// TODO print out results like auction ID?
return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
},
}
return cmd
}

View File

@ -1,30 +0,0 @@
package rest
import (
"fmt"
"net/http"
"github.com/gorilla/mux"
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/types/rest"
"github.com/kava-labs/kava/x/liquidator/types"
)
// RegisterRoutes - Central function to define routes that get registered by the main application
func registerQueryRoutes(cliCtx context.CLIContext, r *mux.Router) {
r.HandleFunc("/liquidator/outstandingdebt", queryDebtHandlerFn(cliCtx)).Methods("GET")
// r.HandleFunc("liquidator/burn", surplusAuctionHandlerFn(cdc, cliCtx).Methods("POST"))
}
func queryDebtHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/liquidator/%s", types.QueryGetOutstandingDebt), nil)
cliCtx = cliCtx.WithHeight(height)
if err != nil {
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
rest.PostProcessResponse(w, cliCtx, res) // write JSON to response writer
}
}

View File

@ -1,13 +0,0 @@
package rest
import (
"github.com/gorilla/mux"
"github.com/cosmos/cosmos-sdk/client/context"
)
// RegisterRoutes - Central function to define routes that get registered by the main application
func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router) {
registerQueryRoutes(cliCtx, r)
registerTxRoutes(cliCtx, r)
}

View File

@ -1,73 +0,0 @@
package rest
import (
"net/http"
"github.com/gorilla/mux"
"github.com/cosmos/cosmos-sdk/client/context"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/rest"
"github.com/cosmos/cosmos-sdk/x/auth/client/utils"
"github.com/kava-labs/kava/x/liquidator/types"
)
func registerTxRoutes(cliCtx context.CLIContext, r *mux.Router) {
r.HandleFunc("/liquidator/seize", seizeCdpHandlerFn(cliCtx)).Methods("POST")
r.HandleFunc("/liquidator/mint", debtAuctionHandlerFn(cliCtx)).Methods("POST")
}
func seizeCdpHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Get args from post body
var req types.SeizeAndStartCollateralAuctionRequest
if !rest.ReadRESTReq(w, r, cliCtx.Codec, &req) { // This function writes a response on error
return
}
req.BaseReq = req.BaseReq.Sanitize()
if !req.BaseReq.ValidateBasic(w) { // This function writes a response on error
return
}
// Create msg
msg := types.MsgSeizeAndStartCollateralAuction{
req.Sender,
req.CdpOwner,
req.CollateralDenom,
}
if err := msg.ValidateBasic(); err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
// Generate tx and write response
utils.WriteGenerateStdTxResponse(w, cliCtx, req.BaseReq, []sdk.Msg{msg})
}
}
func debtAuctionHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Get args from post body
var req types.StartDebtAuctionRequest
if !rest.ReadRESTReq(w, r, cliCtx.Codec, &req) {
return
}
req.BaseReq = req.BaseReq.Sanitize()
if !req.BaseReq.ValidateBasic(w) {
return
}
// Create msg
msg := types.MsgStartDebtAuction{
req.Sender,
}
if err := msg.ValidateBasic(); err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
// Generate tx and write response
utils.WriteGenerateStdTxResponse(w, cliCtx, req.BaseReq, []sdk.Msg{msg})
}
}

View File

@ -1,21 +0,0 @@
/*
Package Liquidator settles bad debt from undercollateralized CDPs by seizing them and raising funds through auctions.
Notes
- Missing the debt queue thing from Vow
- seized collateral and usdx are stored in the module account, but debt (aka Sin) is stored in keeper
- The boundary between the liquidator and the cdp modules is messy.
- The CDP type is used in liquidator
- cdp knows about seizing
- seizing of a CDP is split across each module
- recording of debt is split across modules
- liquidator needs get access to stable and gov denoms from the cdp module
TODO
- Is returning unsold collateral to the CDP owner rather than the CDP a problem? It could prevent the CDP from becoming safe again.
- Add some kind of more complete test
- Add constants for the module and route names
- tags
- custom error types, codespace
*/
package liquidator

View File

@ -1,19 +0,0 @@
package liquidator
import (
sdk "github.com/cosmos/cosmos-sdk/types"
)
// InitGenesis sets the genesis state in the keeper.
func InitGenesis(ctx sdk.Context, keeper Keeper, data GenesisState) {
keeper.SetParams(ctx, data.Params)
}
// ExportGenesis returns a GenesisState for a given context and keeper.
func ExportGenesis(ctx sdk.Context, keeper Keeper) GenesisState {
params := keeper.GetParams(ctx)
return GenesisState{
Params: params,
}
}

View File

@ -1,54 +0,0 @@
package liquidator
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// Handle all liquidator messages.
func NewHandler(keeper Keeper) sdk.Handler {
return func(ctx sdk.Context, msg sdk.Msg) sdk.Result {
switch msg := msg.(type) {
case MsgSeizeAndStartCollateralAuction:
return handleMsgSeizeAndStartCollateralAuction(ctx, keeper, msg)
case MsgStartDebtAuction:
return handleMsgStartDebtAuction(ctx, keeper)
// case MsgStartSurplusAuction:
// return handleMsgStartSurplusAuction(ctx, keeper)
default:
errMsg := fmt.Sprintf("Unrecognized liquidator msg type: %T", msg)
return sdk.ErrUnknownRequest(errMsg).Result()
}
}
}
func handleMsgSeizeAndStartCollateralAuction(ctx sdk.Context, keeper Keeper, msg MsgSeizeAndStartCollateralAuction) sdk.Result {
_, err := keeper.SeizeAndStartCollateralAuction(ctx, msg.CdpOwner, msg.CollateralDenom)
if err != nil {
return err.Result()
}
return sdk.Result{} // TODO tags, return auction ID
}
func handleMsgStartDebtAuction(ctx sdk.Context, keeper Keeper) sdk.Result {
// cancel out any debt and stable coins before trying to start auction
keeper.SettleDebt(ctx)
// start an auction
_, err := keeper.StartDebtAuction(ctx)
if err != nil {
return err.Result()
}
return sdk.Result{} // TODO tags, return auction ID
}
// With no stability and liquidation fees, surplus auctions can never be run.
// func handleMsgStartSurplusAuction(ctx sdk.Context, keeper Keeper) sdk.Result {
// // cancel out any debt and stable coins before trying to start auction
// keeper.settleDebt(ctx)
// _, err := keeper.StartSurplusAuction(ctx)
// if err != nil {
// return err.Result()
// }
// return sdk.Result{} // TODO tags
// }

View File

@ -1,72 +0,0 @@
package keeper_test
import (
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/cdp"
"github.com/kava-labs/kava/x/liquidator"
"github.com/kava-labs/kava/x/liquidator/types"
"github.com/kava-labs/kava/x/pricefeed"
)
// Avoid cluttering test cases with long function name
func i(in int64) sdk.Int { return sdk.NewInt(in) }
func d(str string) sdk.Dec { return sdk.MustNewDecFromStr(str) }
func c(denom string, amount int64) sdk.Coin { return sdk.NewInt64Coin(denom, amount) }
func cs(coins ...sdk.Coin) sdk.Coins { return sdk.NewCoins(coins...) }
// Genesis states to initialize test apps
func NewPricefeedGenState(asset string, price sdk.Dec) app.GenesisState {
pfGenesis := pricefeed.GenesisState{
Params: pricefeed.Params{
Markets: []pricefeed.Market{
pricefeed.Market{MarketID: asset, BaseAsset: asset, QuoteAsset: "usd", Oracles: pricefeed.Oracles{}, Active: true}},
},
PostedPrices: []pricefeed.PostedPrice{
pricefeed.PostedPrice{
MarketID: asset,
OracleAddress: sdk.AccAddress{},
Price: price,
Expiry: time.Now().Add(1 * time.Hour),
},
},
}
return app.GenesisState{pricefeed.ModuleName: pricefeed.ModuleCdc.MustMarshalJSON(pfGenesis)}
}
func NewCDPGenState() app.GenesisState {
cdpGenesis := cdp.GenesisState{
Params: cdp.CdpParams{
GlobalDebtLimit: sdk.NewInt(1000000),
CollateralParams: []cdp.CollateralParams{
{
Denom: "btc",
LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
DebtLimit: sdk.NewInt(500000),
},
},
},
GlobalDebt: sdk.ZeroInt(),
CDPs: cdp.CDPs{},
}
return app.GenesisState{cdp.ModuleName: cdp.ModuleCdc.MustMarshalJSON(cdpGenesis)}
}
func NewLiquidatorGenState() app.GenesisState {
liquidatorGenesis := types.GenesisState{
Params: types.LiquidatorParams{
DebtAuctionSize: sdk.NewInt(1000),
CollateralParams: []types.CollateralParams{
{
Denom: "btc",
AuctionSize: sdk.NewInt(1),
},
},
},
}
return app.GenesisState{liquidator.ModuleName: liquidator.ModuleCdc.MustMarshalJSON(liquidatorGenesis)}
}

View File

@ -1,203 +0,0 @@
package keeper
import (
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/params/subspace"
"github.com/kava-labs/kava/x/liquidator/types"
)
type Keeper struct {
cdc *codec.Codec
paramSubspace subspace.Subspace
key sdk.StoreKey
cdpKeeper types.CdpKeeper
auctionKeeper types.AuctionKeeper
bankKeeper types.BankKeeper
}
func NewKeeper(cdc *codec.Codec, storeKey sdk.StoreKey, paramstore subspace.Subspace, cdpKeeper types.CdpKeeper, auctionKeeper types.AuctionKeeper, bankKeeper types.BankKeeper) Keeper {
subspace := paramstore.WithKeyTable(types.ParamKeyTable())
return Keeper{
cdc: cdc,
paramSubspace: subspace,
key: storeKey,
cdpKeeper: cdpKeeper,
auctionKeeper: auctionKeeper,
bankKeeper: bankKeeper,
}
}
// SeizeAndStartCollateralAuction pulls collateral out of a CDP and sells it in an auction for stable coin. Excess collateral goes to the original CDP owner.
// Known as Cat.bite in maker
// result: stable coin is transferred to module account, collateral is transferred from module account to buyer, (and any excess collateral is transferred to original CDP owner)
func (k Keeper) SeizeAndStartCollateralAuction(ctx sdk.Context, owner sdk.AccAddress, collateralDenom string) (uint64, sdk.Error) {
// Get CDP
cdp, found := k.cdpKeeper.GetCDP(ctx, owner, collateralDenom)
if !found {
return 0, sdk.ErrInternal("CDP not found")
}
// Calculate amount of collateral to sell in this auction
paramsMap := make(map[string]types.CollateralParams)
params := k.GetParams(ctx).CollateralParams
for _, cp := range params {
paramsMap[cp.Denom] = cp
}
collateralParams, found := paramsMap[collateralDenom]
if !found {
return 0, sdk.ErrInternal("collateral denom not found")
}
collateralToSell := sdk.MinInt(cdp.CollateralAmount, collateralParams.AuctionSize)
// Calculate the corresponding maximum amount of stable coin to raise TODO test maths
stableToRaise := sdk.NewDecFromInt(collateralToSell).Quo(sdk.NewDecFromInt(cdp.CollateralAmount)).Mul(sdk.NewDecFromInt(cdp.Debt)).RoundInt()
// Seize the collateral and debt from the CDP
err := k.PartialSeizeCDP(ctx, owner, collateralDenom, collateralToSell, stableToRaise)
if err != nil {
return 0, err
}
// Start "forward reverse" auction type
lot := sdk.NewCoin(cdp.CollateralDenom, collateralToSell)
maxBid := sdk.NewCoin(k.cdpKeeper.GetStableDenom(), stableToRaise)
auctionID, err := k.auctionKeeper.StartForwardReverseAuction(ctx, k.cdpKeeper.GetLiquidatorAccountAddress(), lot, maxBid, owner)
if err != nil {
panic(err) // TODO how can errors here be handled to be safe with the state update in PartialSeizeCDP?
}
return auctionID, nil
}
// StartDebtAuction sells off minted gov coin to raise set amounts of stable coin.
// Known as Vow.flop in maker
// result: minted gov coin moved to highest bidder, stable coin moved to moduleAccount
func (k Keeper) StartDebtAuction(ctx sdk.Context) (uint64, sdk.Error) {
// Ensure amount of seized stable coin is 0 (ie Joy = 0)
stableCoins := k.bankKeeper.GetCoins(ctx, k.cdpKeeper.GetLiquidatorAccountAddress()).AmountOf(k.cdpKeeper.GetStableDenom())
if !stableCoins.IsZero() {
return 0, sdk.ErrInternal("debt auction cannot be started as there is outstanding stable coins")
}
// check the seized debt is above a threshold
params := k.GetParams(ctx)
seizedDebt := k.GetSeizedDebt(ctx)
if seizedDebt.Available().LT(params.DebtAuctionSize) {
return 0, sdk.ErrInternal("not enough seized debt to start an auction")
}
// start reverse auction, selling minted gov coin for stable coin
auctionID, err := k.auctionKeeper.StartReverseAuction(
ctx,
k.cdpKeeper.GetLiquidatorAccountAddress(),
sdk.NewCoin(k.cdpKeeper.GetStableDenom(), params.DebtAuctionSize),
sdk.NewInt64Coin(k.cdpKeeper.GetGovDenom(), 2^255-1), // TODO is there a way to avoid potentially minting infinite gov coin?
)
if err != nil {
return 0, err
}
// Record amount of debt sent for auction. Debt can only be reduced in lock step with reducing stable coin
seizedDebt.SentToAuction = seizedDebt.SentToAuction.Add(params.DebtAuctionSize)
k.SetSeizedDebt(ctx, seizedDebt)
return auctionID, nil
}
// With no stability and liquidation fees, surplus auctions can never be run.
// StartSurplusAuction sells off excess stable coin in exchange for gov coin, which is burned
// Known as Vow.flap in maker
// result: stable coin removed from module account (eventually to buyer), gov coin transferred to module account
// func (k Keeper) StartSurplusAuction(ctx sdk.Context) (uint64, sdk.Error) {
// // TODO ensure seized debt is 0
// // check there is enough surplus to be sold
// surplus := k.bankKeeper.GetCoins(ctx, k.cdpKeeper.GetLiquidatorAccountAddress()).AmountOf(k.cdpKeeper.GetStableDenom())
// if surplus.LT(SurplusAuctionSize) {
// return 0, sdk.ErrInternal("not enough surplus stable coin to start an auction")
// }
// // start normal auction, selling stable coin
// auctionID, err := k.auctionKeeper.StartForwardAuction(
// ctx,
// k.cdpKeeper.GetLiquidatorAccountAddress(),
// sdk.NewCoin(k.cdpKeeper.GetStableDenom(), SurplusAuctionSize),
// sdk.NewInt64Coin(k.cdpKeeper.GetGovDenom(), 0),
// )
// if err != nil {
// return 0, err
// }
// // Starting the auction will remove coins from the account, so they don't need modified here.
// return auctionID, nil
// }
// PartialSeizeCDP seizes some collateral and debt from an under-collateralized CDP.
func (k Keeper) PartialSeizeCDP(ctx sdk.Context, owner sdk.AccAddress, collateralDenom string, collateralToSeize sdk.Int, debtToSeize sdk.Int) sdk.Error { // aka Cat.bite
// Seize debt and collateral in the cdp module. This also validates the inputs.
err := k.cdpKeeper.PartialSeizeCDP(ctx, owner, collateralDenom, collateralToSeize, debtToSeize)
if err != nil {
return err // cdp could be not found, or not under collateralized, or inputs invalid
}
// increment the total seized debt (Awe) by cdp.debt
seizedDebt := k.GetSeizedDebt(ctx)
seizedDebt.Total = seizedDebt.Total.Add(debtToSeize)
k.SetSeizedDebt(ctx, seizedDebt)
// add cdp.collateral amount of coins to the moduleAccount (so they can be transferred to the auction later)
coins := sdk.NewCoins(sdk.NewCoin(collateralDenom, collateralToSeize))
_, err = k.bankKeeper.AddCoins(ctx, k.cdpKeeper.GetLiquidatorAccountAddress(), coins)
if err != nil {
panic(err) // TODO this shouldn't happen?
}
return nil
}
// SettleDebt removes equal amounts of debt and stable coin from the liquidator's reserves (and also updates the global debt in the cdp module).
// This is called in the handler when a debt or surplus auction is started
// TODO Should this be called with an amount, rather than annihilating the maximum?
func (k Keeper) SettleDebt(ctx sdk.Context) sdk.Error {
// Calculate max amount of debt and stable coins that can be settled (ie annihilated)
debt := k.GetSeizedDebt(ctx)
stableCoins := k.bankKeeper.GetCoins(ctx, k.cdpKeeper.GetLiquidatorAccountAddress()).AmountOf(k.cdpKeeper.GetStableDenom())
settleAmount := sdk.MinInt(debt.Total, stableCoins)
// Call cdp module to reduce GlobalDebt. This can fail if genesis not set
err := k.cdpKeeper.ReduceGlobalDebt(ctx, settleAmount)
if err != nil {
return err
}
// Decrement total seized debt (also decrement from SentToAuction debt)
updatedDebt, err := debt.Settle(settleAmount)
if err != nil {
return err // this should not error in this context
}
k.SetSeizedDebt(ctx, updatedDebt)
// Subtract stable coin from moduleAccout
k.bankKeeper.SubtractCoins(ctx, k.cdpKeeper.GetLiquidatorAccountAddress(), sdk.Coins{sdk.NewCoin(k.cdpKeeper.GetStableDenom(), settleAmount)})
return nil
}
// ---------- Store Wrappers ----------
func (k Keeper) getSeizedDebtKey() []byte {
return []byte("seizedDebt")
}
func (k Keeper) GetSeizedDebt(ctx sdk.Context) types.SeizedDebt {
store := ctx.KVStore(k.key)
bz := store.Get(k.getSeizedDebtKey())
if bz == nil {
// TODO make initial seized debt and CDPs configurable at genesis, then panic here if not found
bz = k.cdc.MustMarshalBinaryLengthPrefixed(types.SeizedDebt{
Total: sdk.ZeroInt(),
SentToAuction: sdk.ZeroInt()})
}
var seizedDebt types.SeizedDebt
k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &seizedDebt)
return seizedDebt
}
func (k Keeper) SetSeizedDebt(ctx sdk.Context, debt types.SeizedDebt) {
store := ctx.KVStore(k.key)
bz := k.cdc.MustMarshalBinaryLengthPrefixed(debt)
store.Set(k.getSeizedDebtKey(), bz)
}

View File

@ -1,118 +0,0 @@
package keeper_test
import (
"testing"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/require"
abci "github.com/tendermint/tendermint/abci/types"
tmtime "github.com/tendermint/tendermint/types/time"
"github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/liquidator/types"
)
func TestKeeper_SeizeAndStartCollateralAuction(t *testing.T) {
// Setup
_, addrs := app.GeneratePrivKeyAddressPairs(1)
tApp := app.NewTestApp()
tApp.InitializeFromGenesisStates(
app.NewAuthGenState(addrs, []sdk.Coins{cs(c("btc", 100))}),
NewPricefeedGenState("btc", d("8000.00")),
NewCDPGenState(),
NewLiquidatorGenState(),
)
ctx := tApp.NewContext(false, abci.Header{})
require.NoError(t, tApp.GetCDPKeeper().ModifyCDP(ctx, addrs[0], "btc", i(3), i(16000)))
_, err := tApp.GetPriceFeedKeeper().SetPrice(ctx, addrs[0], "btc", d("7999.99"), time.Now().Add(1*time.Hour))
require.NoError(t, err)
require.NoError(t, tApp.GetPriceFeedKeeper().SetCurrentPrices(ctx, "btc"))
// Run test function
auctionID, err := tApp.GetLiquidatorKeeper().SeizeAndStartCollateralAuction(ctx, addrs[0], "btc")
// Check CDP
require.NoError(t, err)
cdp, found := tApp.GetCDPKeeper().GetCDP(ctx, addrs[0], "btc")
require.True(t, found)
require.Equal(t, cdp.CollateralAmount, i(2)) // original amount - params.CollateralAuctionSize
require.Equal(t, cdp.Debt, i(10667)) // original debt scaled by amount of collateral removed
// Check auction exists
_, found = tApp.GetAuctionKeeper().GetAuction(ctx, auctionID)
require.True(t, found)
}
func TestKeeper_StartDebtAuction(t *testing.T) {
// Setup
tApp := app.NewTestApp()
tApp.InitializeFromGenesisStates(
NewLiquidatorGenState(),
)
keeper := tApp.GetLiquidatorKeeper()
ctx := tApp.NewContext(false, abci.Header{})
initSDebt := types.SeizedDebt{i(2000), i(0)}
keeper.SetSeizedDebt(ctx, initSDebt)
// Execute
auctionID, err := keeper.StartDebtAuction(ctx)
// Check
require.NoError(t, err)
require.Equal(t,
types.SeizedDebt{
initSDebt.Total,
initSDebt.SentToAuction.Add(keeper.GetParams(ctx).DebtAuctionSize),
},
keeper.GetSeizedDebt(ctx),
)
_, found := tApp.GetAuctionKeeper().GetAuction(ctx, auctionID)
require.True(t, found)
}
func TestKeeper_partialSeizeCDP(t *testing.T) {
// Setup
_, addrs := app.GeneratePrivKeyAddressPairs(1)
tApp := app.NewTestApp()
tApp.InitializeFromGenesisStates(
app.NewAuthGenState(addrs, []sdk.Coins{cs(c("btc", 100))}),
NewPricefeedGenState("btc", d("8000.00")),
NewCDPGenState(),
NewLiquidatorGenState(),
)
ctx := tApp.NewContext(false, abci.Header{})
tApp.GetCDPKeeper().ModifyCDP(ctx, addrs[0], "btc", i(3), i(16000))
tApp.GetPriceFeedKeeper().SetPrice(ctx, addrs[0], "btc", d("7999.99"), tmtime.Now().Add(time.Hour*1))
tApp.GetPriceFeedKeeper().SetCurrentPrices(ctx, "btc")
// Run test function
err := tApp.GetLiquidatorKeeper().PartialSeizeCDP(ctx, addrs[0], "btc", i(2), i(10000))
// Check
require.NoError(t, err)
cdp, found := tApp.GetCDPKeeper().GetCDP(ctx, addrs[0], "btc")
require.True(t, found)
require.Equal(t, i(1), cdp.CollateralAmount)
require.Equal(t, i(6000), cdp.Debt)
}
func TestKeeper_GetSetSeizedDebt(t *testing.T) {
// Setup
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{})
debt := types.SeizedDebt{i(234247645), i(2343)}
// Run test function
tApp.GetLiquidatorKeeper().SetSeizedDebt(ctx, debt)
readDebt := tApp.GetLiquidatorKeeper().GetSeizedDebt(ctx)
// Check
require.Equal(t, debt, readDebt)
}

View File

@ -1,18 +0,0 @@
package keeper
import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/liquidator/types"
)
// GetParams returns the params for liquidator module
func (k Keeper) GetParams(ctx sdk.Context) types.LiquidatorParams {
var params types.LiquidatorParams
k.paramSubspace.GetParamSet(ctx, &params)
return params
}
// SetParams sets params for the liquidator module
func (k Keeper) SetParams(ctx sdk.Context, params types.LiquidatorParams) {
k.paramSubspace.SetParamSet(ctx, &params)
}

View File

@ -1,45 +0,0 @@
package keeper
import (
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/liquidator/types"
abci "github.com/tendermint/tendermint/abci/types"
)
func NewQuerier(keeper Keeper) sdk.Querier {
return func(ctx sdk.Context, path []string, req abci.RequestQuery) (res []byte, err sdk.Error) {
switch path[0] {
case types.QueryGetOutstandingDebt:
return queryGetOutstandingDebt(ctx, path[1:], req, keeper)
// case QueryGetSurplus:
// return queryGetSurplus()
default:
return nil, sdk.ErrUnknownRequest("unknown liquidator query endpoint")
}
}
}
func queryGetOutstandingDebt(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) {
// Calculate the remaining seized debt after settling with the liquidator's stable coins.
stableCoins := keeper.bankKeeper.GetCoins(
ctx,
keeper.cdpKeeper.GetLiquidatorAccountAddress(),
).AmountOf(keeper.cdpKeeper.GetStableDenom())
seizedDebt := keeper.GetSeizedDebt(ctx)
settleAmount := sdk.MinInt(seizedDebt.Total, stableCoins)
seizedDebt, err := seizedDebt.Settle(settleAmount)
if err != nil {
return nil, err // this shouldn't error in this context
}
// Get the available debt after settling
oustandingDebt := seizedDebt.Available()
// Encode and return
bz, err2 := codec.MarshalJSONIndent(keeper.cdc, oustandingDebt)
if err2 != nil {
return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error()))
}
return bz, nil
}

View File

@ -1,130 +0,0 @@
package liquidator
import (
"encoding/json"
"github.com/gorilla/mux"
"github.com/spf13/cobra"
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/module"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/kava-labs/kava/x/liquidator/client/cli"
"github.com/kava-labs/kava/x/liquidator/client/rest"
)
var (
_ module.AppModule = AppModule{}
_ module.AppModuleBasic = AppModuleBasic{}
)
// AppModuleBasic app module basics object
type AppModuleBasic struct{}
// Name get module name
func (AppModuleBasic) Name() string {
return ModuleName
}
// RegisterCodec register module codec
func (AppModuleBasic) RegisterCodec(cdc *codec.Codec) {
RegisterCodec(cdc)
}
// DefaultGenesis default genesis state
func (AppModuleBasic) DefaultGenesis() json.RawMessage {
return ModuleCdc.MustMarshalJSON(DefaultGenesisState())
}
// ValidateGenesis module validate genesis
func (AppModuleBasic) ValidateGenesis(bz json.RawMessage) error {
var data GenesisState
err := ModuleCdc.UnmarshalJSON(bz, &data)
if err != nil {
return err
}
return ValidateGenesis(data)
}
// RegisterRESTRoutes registers the REST routes for the liquidator module.
func (AppModuleBasic) RegisterRESTRoutes(ctx context.CLIContext, rtr *mux.Router) {
rest.RegisterRoutes(ctx, rtr)
}
// GetTxCmd returns the root tx command for the liquidator module.
func (AppModuleBasic) GetTxCmd(cdc *codec.Codec) *cobra.Command {
return cli.GetTxCmd(cdc)
}
// GetQueryCmd returns the root query command for the auction module.
func (AppModuleBasic) GetQueryCmd(cdc *codec.Codec) *cobra.Command {
return cli.GetQueryCmd(StoreKey, cdc)
}
// AppModule app module type
type AppModule struct {
AppModuleBasic
keeper Keeper
}
// NewAppModule creates a new AppModule object
func NewAppModule(keeper Keeper) AppModule {
return AppModule{
AppModuleBasic: AppModuleBasic{},
keeper: keeper,
}
}
// Name module name
func (AppModule) Name() string {
return ModuleName
}
// RegisterInvariants register module invariants
func (AppModule) RegisterInvariants(_ sdk.InvariantRegistry) {}
// Route module message route name
func (AppModule) Route() string {
return ModuleName
}
// NewHandler module handler
func (am AppModule) NewHandler() sdk.Handler {
return NewHandler(am.keeper)
}
// QuerierRoute module querier route name
func (AppModule) QuerierRoute() string {
return ModuleName
}
// NewQuerierHandler module querier
func (am AppModule) NewQuerierHandler() sdk.Querier {
return NewQuerier(am.keeper)
}
// InitGenesis module init-genesis
func (am AppModule) InitGenesis(ctx sdk.Context, data json.RawMessage) []abci.ValidatorUpdate {
var genesisState GenesisState
ModuleCdc.MustUnmarshalJSON(data, &genesisState)
InitGenesis(ctx, am.keeper, genesisState)
return []abci.ValidatorUpdate{}
}
// ExportGenesis module export genesis
func (am AppModule) ExportGenesis(ctx sdk.Context) json.RawMessage {
gs := ExportGenesis(ctx, am.keeper)
return ModuleCdc.MustMarshalJSON(gs)
}
// BeginBlock module begin-block
func (AppModule) BeginBlock(_ sdk.Context, _ abci.RequestBeginBlock) {}
// EndBlock module end-block
func (am AppModule) EndBlock(ctx sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate {
return []abci.ValidatorUpdate{}
}

View File

@ -1,20 +0,0 @@
package types
import "github.com/cosmos/cosmos-sdk/codec"
// ModuleCdc module level codec
var ModuleCdc = codec.New()
func init() {
cdc := codec.New()
RegisterCodec(cdc)
codec.RegisterCrypto(cdc)
ModuleCdc = cdc.Seal()
}
// RegisterCodec registers concrete types on the codec.
func RegisterCodec(cdc *codec.Codec) {
cdc.RegisterConcrete(MsgSeizeAndStartCollateralAuction{}, "liquidator/MsgSeizeAndStartCollateralAuction", nil)
cdc.RegisterConcrete(MsgStartDebtAuction{}, "liquidator/MsgStartDebtAuction", nil)
// cdc.RegisterConcrete(MsgStartSurplusAuction{}, "liquidator/MsgStartSurplusAuction", nil)
}

View File

@ -1,31 +0,0 @@
package types
import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/cdp"
)
// CdpKeeper expected interface for the cdp keeper
type CdpKeeper interface {
GetCDP(sdk.Context, sdk.AccAddress, string) (cdp.CDP, bool)
PartialSeizeCDP(sdk.Context, sdk.AccAddress, string, sdk.Int, sdk.Int) sdk.Error
ReduceGlobalDebt(sdk.Context, sdk.Int) sdk.Error
GetStableDenom() string // TODO can this be removed somehow?
GetGovDenom() string
GetLiquidatorAccountAddress() sdk.AccAddress // This won't need to exist once the module account is defined in this module (instead of in the cdp module)
}
// BankKeeper expected interface for the bank keeper
type BankKeeper interface {
GetCoins(sdk.Context, sdk.AccAddress) sdk.Coins
AddCoins(sdk.Context, sdk.AccAddress, sdk.Coins) (sdk.Coins, sdk.Error)
SubtractCoins(sdk.Context, sdk.AccAddress, sdk.Coins) (sdk.Coins, sdk.Error)
}
// AuctionKeeper expected interface for the auction keeper
type AuctionKeeper interface {
StartForwardAuction(sdk.Context, sdk.AccAddress, sdk.Coin, sdk.Coin) (uint64, sdk.Error)
StartReverseAuction(sdk.Context, sdk.AccAddress, sdk.Coin, sdk.Coin) (uint64, sdk.Error)
StartForwardReverseAuction(sdk.Context, sdk.AccAddress, sdk.Coin, sdk.Coin, sdk.AccAddress) (uint64, sdk.Error)
}

View File

@ -1,22 +0,0 @@
package types
// GenesisState is the state that must be provided at genesis.
type GenesisState struct {
Params LiquidatorParams `json:"liquidator_params" yaml:"liquidator_params"`
}
// DefaultGenesisState returns a default genesis state
// TODO pick better values
func DefaultGenesisState() GenesisState {
return GenesisState{
DefaultParams(),
}
}
// ValidateGenesis performs basic validation of genesis data returning an error for any failed validation criteria.
func ValidateGenesis(data GenesisState) error {
if err := data.Params.Validate(); err != nil {
return err
}
return nil
}

View File

@ -1,18 +0,0 @@
package types
const (
// ModuleName is the name of the module
ModuleName = "liquidator"
// StoreKey is the store key string for liquidator
StoreKey = ModuleName
// RouterKey is the message route for liquidator
RouterKey = ModuleName
// QuerierRoute is the querier route for liquidator
QuerierRoute = ModuleName
// DefaultParamspace default name for parameter store
DefaultParamspace = ModuleName
)

View File

@ -1,97 +0,0 @@
package types
import sdk "github.com/cosmos/cosmos-sdk/types"
/*
Message types for starting various auctions.
Note: these message types are not final and will likely change.
Design options and problems:
- msgs that only start auctions
- senders have to pay fees
- these msgs cannot be bundled into a tx with a PlaceBid msg because PlaceBid requires an auction ID
- msgs that start auctions and place an initial bid
- place bid can fail, leaving auction without bids which is similar to first case
- no msgs, auctions started automatically
- running this as an endblocker adds complexity and potential vulnerabilities
*/
// MsgSeizeAndStartCollateralAuction siezes a cdp that is below liquidation ratio and starts an auction for the collateral
type MsgSeizeAndStartCollateralAuction struct {
Sender sdk.AccAddress // only needed to pay the tx fees
CdpOwner sdk.AccAddress
CollateralDenom string
}
// Route return the message type used for routing the message.
func (msg MsgSeizeAndStartCollateralAuction) Route() string { return "liquidator" }
// Type returns a human-readable string for the message, intended for utilization within tags.
func (msg MsgSeizeAndStartCollateralAuction) Type() string { return "seize_and_start_auction" } // TODO snake case?
// ValidateBasic does a simple validation check that doesn't require access to any other information.
func (msg MsgSeizeAndStartCollateralAuction) ValidateBasic() sdk.Error {
if msg.Sender.Empty() {
return sdk.ErrInternal("invalid (empty) sender address")
}
if msg.CdpOwner.Empty() {
return sdk.ErrInternal("invalid (empty) CDP owner address")
}
// TODO check coin denoms
return nil
}
// GetSignBytes gets the canonical byte representation of the Msg.
func (msg MsgSeizeAndStartCollateralAuction) GetSignBytes() []byte {
bz := ModuleCdc.MustMarshalJSON(msg)
return sdk.MustSortJSON(bz)
}
// GetSigners returns the addresses of signers that must sign.
func (msg MsgSeizeAndStartCollateralAuction) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{msg.Sender}
}
// MsgStartDebtAuction starts an auction of gov tokens for stable tokens
type MsgStartDebtAuction struct {
Sender sdk.AccAddress // only needed to pay the tx fees
}
// Route returns the route for this message
func (msg MsgStartDebtAuction) Route() string { return "liquidator" }
// Type returns the type for this message
func (msg MsgStartDebtAuction) Type() string { return "start_debt_auction" }
// ValidateBasic simple validation check
func (msg MsgStartDebtAuction) ValidateBasic() sdk.Error {
if msg.Sender.Empty() {
return sdk.ErrInternal("invalid (empty) sender address")
}
return nil
}
// GetSignBytes returns canonical byte representation of the message
func (msg MsgStartDebtAuction) GetSignBytes() []byte {
return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg))
}
// GetSigners returns the addresses of the signers of the message
func (msg MsgStartDebtAuction) GetSigners() []sdk.AccAddress { return []sdk.AccAddress{msg.Sender} }
// With no stability and liquidation fees, surplus auctions can never be run.
// type MsgStartSurplusAuction struct {
// Sender sdk.AccAddress // only needed to pay the tx fees
// }
// func (msg MsgStartSurplusAuction) Route() string { return "liquidator" }
// func (msg MsgStartSurplusAuction) Type() string { return "start_surplus_auction" } // TODO snake case?
// func (msg MsgStartSurplusAuction) ValidateBasic() sdk.Error {
// if msg.Sender.Empty() {
// return sdk.ErrInternal("invalid (empty) sender address")
// }
// return nil
// }
// func (msg MsgStartSurplusAuction) GetSignBytes() []byte {
// return sdk.MustSortJSON(msgCdc.MustMarshalJSON(msg))
// }
// func (msg MsgStartSurplusAuction) GetSigners() []sdk.AccAddress { return []sdk.AccAddress{msg.Sender} }

View File

@ -1,100 +0,0 @@
package types
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/params/subspace"
)
// Parameter keys
var (
KeyDebtAuctionSize = []byte("DebtAuctionSize")
KeyCollateralParams = []byte("CollateralParams")
)
// LiquidatorParams store params for the liquidator module
type LiquidatorParams struct {
DebtAuctionSize sdk.Int
//SurplusAuctionSize sdk.Int
CollateralParams []CollateralParams
}
// NewLiquidatorParams returns a new params object for the liquidator module
func NewLiquidatorParams(debtAuctionSize sdk.Int, collateralParams []CollateralParams) LiquidatorParams {
return LiquidatorParams{
DebtAuctionSize: debtAuctionSize,
CollateralParams: collateralParams,
}
}
// String implements fmt.Stringer
func (p LiquidatorParams) String() string {
out := fmt.Sprintf(`Params:
Debt Auction Size: %s
Collateral Params: `,
p.DebtAuctionSize,
)
for _, cp := range p.CollateralParams {
out += fmt.Sprintf(`
%s`, cp.String())
}
return out
}
// CollateralParams params storing information about each collateral for the liquidator module
type CollateralParams struct {
Denom string // Coin name of collateral type
AuctionSize sdk.Int // Max amount of collateral to sell off in any one auction. Known as lump in Maker.
// LiquidationPenalty
}
// String implements stringer interface
func (cp CollateralParams) String() string {
return fmt.Sprintf(`
Denom: %s
AuctionSize: %s`, cp.Denom, cp.AuctionSize)
}
// ParamKeyTable for the liquidator module
func ParamKeyTable() subspace.KeyTable {
return subspace.NewKeyTable().RegisterParamSet(&LiquidatorParams{})
}
// ParamSetPairs implements the ParamSet interface and returns all the key/value pairs
// pairs of liquidator module's parameters.
// nolint
func (p *LiquidatorParams) ParamSetPairs() subspace.ParamSetPairs {
return subspace.ParamSetPairs{
subspace.NewParamSetPair(KeyDebtAuctionSize, &p.DebtAuctionSize),
subspace.NewParamSetPair(KeyCollateralParams, &p.CollateralParams),
}
}
// DefaultParams for the liquidator module
func DefaultParams() LiquidatorParams {
return LiquidatorParams{
DebtAuctionSize: sdk.NewInt(1000),
CollateralParams: []CollateralParams{},
}
}
func (p LiquidatorParams) Validate() error {
if p.DebtAuctionSize.IsNegative() {
return fmt.Errorf("debt auction size should be positive, is %s", p.DebtAuctionSize)
}
denomDupMap := make(map[string]int)
for _, cp := range p.CollateralParams {
_, found := denomDupMap[cp.Denom]
if found {
return fmt.Errorf("duplicate denom: %s", cp.Denom)
}
denomDupMap[cp.Denom] = 1
if cp.AuctionSize.IsNegative() {
return fmt.Errorf(
"auction size for each collateral should be positive, is %s for %s", cp.AuctionSize, cp.Denom,
)
}
}
return nil
}

View File

@ -1,22 +0,0 @@
package types
import(
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/rest"
)
const (
QueryGetOutstandingDebt = "outstanding_debt" // Get the outstanding seized debt
)
type SeizeAndStartCollateralAuctionRequest struct {
BaseReq rest.BaseReq `json:"base_req"`
Sender sdk.AccAddress `json:"sender"`
CdpOwner sdk.AccAddress `json:"cdp_owner"`
CollateralDenom string `json:"collateral_denom"`
}
type StartDebtAuctionRequest struct {
BaseReq rest.BaseReq `json:"base_req"`
Sender sdk.AccAddress `json:"sender"` // TODO use baseReq.From instead?
}

View File

@ -1,30 +0,0 @@
package types
import (
sdk "github.com/cosmos/cosmos-sdk/types"
)
// SeizedDebt tracks debt seized from liquidated CDPs.
type SeizedDebt struct {
Total sdk.Int // Total debt seized from CDPs. Known as Awe in maker.
SentToAuction sdk.Int // Portion of seized debt that has had a (reverse) auction was started for it. Known as Ash in maker.
// SentToAuction should always be < Total
}
// Available gets the seized debt that has not been sent for auction. Known as Woe in maker.
func (sd SeizedDebt) Available() sdk.Int {
return sd.Total.Sub(sd.SentToAuction)
}
// Settle reduces the amount of debt
func (sd SeizedDebt) Settle(amount sdk.Int) (SeizedDebt, sdk.Error) {
if amount.IsNegative() {
return sd, sdk.ErrInternal("tried to settle a negative amount")
}
if amount.GT(sd.Total) {
return sd, sdk.ErrInternal("tried to settle more debt than exists")
}
sd.Total = sd.Total.Sub(amount)
sd.SentToAuction = sdk.MaxInt(sd.SentToAuction.Sub(amount), sdk.ZeroInt())
return sd, nil
}

View File

@ -95,10 +95,14 @@ func (k Keeper) SetCurrentPrices(ctx sdk.Context, marketID string) sdk.Error {
}) })
} }
} }
medianPrice, err := k.CalculateMedianPrice(ctx, notExpiredPrices) if len(notExpiredPrices) == 0 {
if err != nil { store := ctx.KVStore(k.storeKey)
return err store.Set(
[]byte(types.CurrentPricePrefix+marketID), k.cdc.MustMarshalBinaryBare(types.CurrentPrice{}),
)
return types.ErrNoValidPrice(k.codespace)
} }
medianPrice := k.CalculateMedianPrice(ctx, notExpiredPrices)
store := ctx.KVStore(k.storeKey) store := ctx.KVStore(k.storeKey)
currentPrice := types.CurrentPrice{ currentPrice := types.CurrentPrice{
@ -113,16 +117,13 @@ func (k Keeper) SetCurrentPrices(ctx sdk.Context, marketID string) sdk.Error {
} }
// CalculateMedianPrice calculates the median prices for the input prices. // CalculateMedianPrice calculates the median prices for the input prices.
func (k Keeper) CalculateMedianPrice(ctx sdk.Context, prices []types.CurrentPrice) (sdk.Dec, sdk.Error) { func (k Keeper) CalculateMedianPrice(ctx sdk.Context, prices []types.CurrentPrice) sdk.Dec {
l := len(prices) l := len(prices)
if l == 0 { if l == 1 {
// Error if there are no valid prices in the raw pricefeed
return sdk.Dec{}, types.ErrNoValidPrice(k.codespace)
} else if l == 1 {
// Return immediately if there's only one price // Return immediately if there's only one price
return prices[0].Price, nil return prices[0].Price
} else { }
// sort the prices // sort the prices
sort.Slice(prices, func(i, j int) bool { sort.Slice(prices, func(i, j int) bool {
return prices[i].Price.LT(prices[j].Price) return prices[i].Price.LT(prices[j].Price)
@ -130,11 +131,11 @@ func (k Keeper) CalculateMedianPrice(ctx sdk.Context, prices []types.CurrentPric
// for even numbers of prices, the median is calculated as the mean of the two middle prices // for even numbers of prices, the median is calculated as the mean of the two middle prices
if l%2 == 0 { if l%2 == 0 {
median := k.calculateMeanPrice(ctx, prices[l/2-1:l/2+1]) median := k.calculateMeanPrice(ctx, prices[l/2-1:l/2+1])
return median, nil return median
} }
// for odd numbers of prices, return the middle element // for odd numbers of prices, return the middle element
return prices[l/2].Price, nil return prices[l/2].Price
}
} }
func (k Keeper) calculateMeanPrice(ctx sdk.Context, prices []types.CurrentPrice) sdk.Dec { func (k Keeper) calculateMeanPrice(ctx sdk.Context, prices []types.CurrentPrice) sdk.Dec {
@ -144,13 +145,19 @@ func (k Keeper) calculateMeanPrice(ctx sdk.Context, prices []types.CurrentPrice)
} }
// GetCurrentPrice fetches the current median price of all oracles for a specific asset // GetCurrentPrice fetches the current median price of all oracles for a specific asset
func (k Keeper) GetCurrentPrice(ctx sdk.Context, marketID string) types.CurrentPrice { func (k Keeper) GetCurrentPrice(ctx sdk.Context, marketID string) (types.CurrentPrice, sdk.Error) {
store := ctx.KVStore(k.storeKey) store := ctx.KVStore(k.storeKey)
bz := store.Get([]byte(types.CurrentPricePrefix + marketID)) bz := store.Get([]byte(types.CurrentPricePrefix + marketID))
// TODO panic or return error if not found
if bz == nil {
return types.CurrentPrice{}, types.ErrNoValidPrice(k.codespace)
}
var price types.CurrentPrice var price types.CurrentPrice
k.cdc.MustUnmarshalBinaryBare(bz, &price) k.cdc.MustUnmarshalBinaryBare(bz, &price)
return price if price.Price.Equal(sdk.ZeroDec()) {
return types.CurrentPrice{}, types.ErrNoValidPrice(k.codespace)
}
return price, nil
} }
// GetRawPrices fetches the set of all prices posted by oracles for an asset // GetRawPrices fetches the set of all prices posted by oracles for an asset

View File

@ -120,7 +120,8 @@ func TestKeeper_GetSetCurrentPrice(t *testing.T) {
err := keeper.SetCurrentPrices(ctx, "tstusd") err := keeper.SetCurrentPrices(ctx, "tstusd")
require.NoError(t, err) require.NoError(t, err)
// Get Current price // Get Current price
price := keeper.GetCurrentPrice(ctx, "tstusd") price, err := keeper.GetCurrentPrice(ctx, "tstusd")
require.Nil(t, err)
require.Equal(t, price.Price.Equal(sdk.MustNewDecFromStr("0.34")), true) require.Equal(t, price.Price.Equal(sdk.MustNewDecFromStr("0.34")), true)
// Even number of oracles // Even number of oracles
@ -130,6 +131,7 @@ func TestKeeper_GetSetCurrentPrice(t *testing.T) {
time.Now().Add(time.Hour*1)) time.Now().Add(time.Hour*1))
err = keeper.SetCurrentPrices(ctx, "tstusd") err = keeper.SetCurrentPrices(ctx, "tstusd")
require.NoError(t, err) require.NoError(t, err)
price = keeper.GetCurrentPrice(ctx, "tstusd") price, err = keeper.GetCurrentPrice(ctx, "tstusd")
require.Nil(t, err)
require.Equal(t, price.Price.Equal(sdk.MustNewDecFromStr("0.345")), true) require.Equal(t, price.Price.Equal(sdk.MustNewDecFromStr("0.345")), true)
} }

View File

@ -32,8 +32,10 @@ func queryCurrentPrice(ctx sdk.Context, path []string, req abci.RequestQuery, ke
if !found { if !found {
return []byte{}, sdk.ErrUnknownRequest("asset not found") return []byte{}, sdk.ErrUnknownRequest("asset not found")
} }
currentPrice := keeper.GetCurrentPrice(ctx, marketID) currentPrice, err := keeper.GetCurrentPrice(ctx, marketID)
if err != nil {
return nil, err
}
bz, err2 := codec.MarshalJSONIndent(keeper.cdc, currentPrice) bz, err2 := codec.MarshalJSONIndent(keeper.cdc, currentPrice)
if err2 != nil { if err2 != nil {
panic("could not marshal result to JSON") panic("could not marshal result to JSON")