mirror of
https://github.com/0glabs/0g-chain.git
synced 2024-12-24 23:35:19 +00:00
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:
parent
e1c11d411a
commit
d849d690e5
2
.gitignore
vendored
2
.gitignore
vendored
@ -8,7 +8,7 @@
|
||||
# Test binary, build with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
# Output of the go coverage tool
|
||||
*.out
|
||||
|
||||
# Exclude build files
|
||||
|
23
app/app.go
23
app/app.go
@ -6,7 +6,6 @@ import (
|
||||
|
||||
"github.com/kava-labs/kava/x/auction"
|
||||
"github.com/kava-labs/kava/x/cdp"
|
||||
"github.com/kava-labs/kava/x/liquidator"
|
||||
"github.com/kava-labs/kava/x/pricefeed"
|
||||
validatorvesting "github.com/kava-labs/kava/x/validator-vesting"
|
||||
|
||||
@ -61,7 +60,6 @@ var (
|
||||
supply.AppModuleBasic{},
|
||||
auction.AppModuleBasic{},
|
||||
cdp.AppModuleBasic{},
|
||||
//liquidator.AppModuleBasic{},
|
||||
pricefeed.AppModuleBasic{},
|
||||
)
|
||||
|
||||
@ -75,7 +73,8 @@ var (
|
||||
gov.ModuleName: {supply.Burner},
|
||||
validatorvesting.ModuleName: {supply.Burner},
|
||||
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
|
||||
auctionKeeper auction.Keeper
|
||||
cdpKeeper cdp.Keeper
|
||||
liquidatorKeeper liquidator.Keeper
|
||||
pricefeedKeeper pricefeed.Keeper
|
||||
|
||||
// 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,
|
||||
supply.StoreKey, mint.StoreKey, distr.StoreKey, slashing.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)
|
||||
|
||||
@ -153,7 +151,6 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
|
||||
crisisSubspace := app.paramsKeeper.Subspace(crisis.DefaultParamspace)
|
||||
auctionSubspace := app.paramsKeeper.Subspace(auction.DefaultParamspace)
|
||||
cdpSubspace := app.paramsKeeper.Subspace(cdp.DefaultParamspace)
|
||||
//liquidatorSubspace := app.paramsKeeper.Subspace(liquidator.DefaultParamspace)
|
||||
pricefeedSubspace := app.paramsKeeper.Subspace(pricefeed.DefaultParamspace)
|
||||
|
||||
// add keepers
|
||||
@ -231,24 +228,19 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
|
||||
keys[pricefeed.StoreKey],
|
||||
pricefeedSubspace,
|
||||
pricefeed.DefaultCodespace)
|
||||
// NewKeeper(cdc *codec.Codec, key sdk.StoreKey, paramstore subspace.Subspace, pfk types.PricefeedKeeper, sk types.SupplyKeeper, codespace sdk.CodespaceType)
|
||||
app.cdpKeeper = cdp.NewKeeper(
|
||||
app.cdc,
|
||||
keys[cdp.StoreKey],
|
||||
cdpSubspace,
|
||||
app.pricefeedKeeper,
|
||||
app.bankKeeper)
|
||||
app.supplyKeeper,
|
||||
cdp.DefaultCodespace)
|
||||
app.auctionKeeper = auction.NewKeeper(
|
||||
app.cdc,
|
||||
keys[auction.StoreKey],
|
||||
app.supplyKeeper,
|
||||
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
|
||||
// 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),
|
||||
auction.NewAppModule(app.auctionKeeper),
|
||||
cdp.NewAppModule(app.cdpKeeper, app.pricefeedKeeper),
|
||||
//liquidator.NewAppModule(app.liquidatorKeeper),
|
||||
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,
|
||||
staking.ModuleName, bank.ModuleName, slashing.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)
|
||||
|
@ -29,7 +29,6 @@ import (
|
||||
|
||||
"github.com/kava-labs/kava/x/auction"
|
||||
"github.com/kava-labs/kava/x/cdp"
|
||||
"github.com/kava-labs/kava/x/liquidator"
|
||||
"github.com/kava-labs/kava/x/pricefeed"
|
||||
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) GetAuctionKeeper() auction.Keeper { return tApp.auctionKeeper }
|
||||
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 }
|
||||
|
||||
// This calls InitChain on the app using the default genesis state, overwitten with any passed in genesis states
|
||||
|
@ -12,7 +12,7 @@ import (
|
||||
|
||||
"github.com/kava-labs/kava/app"
|
||||
"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) {
|
||||
@ -21,7 +21,7 @@ func TestKeeper_EndBlocker(t *testing.T) {
|
||||
buyer := addrs[0]
|
||||
returnAddrs := addrs[1:]
|
||||
returnWeights := []sdk.Int{sdk.NewInt(1)}
|
||||
sellerModName := liquidator.ModuleName
|
||||
sellerModName := cdp.LiquidatorMacc
|
||||
|
||||
tApp := app.NewTestApp()
|
||||
sellerAcc := supply.NewEmptyModuleAccount(sellerModName)
|
||||
|
@ -13,14 +13,14 @@ import (
|
||||
|
||||
"github.com/kava-labs/kava/app"
|
||||
"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) {
|
||||
// Setup
|
||||
_, addrs := app.GeneratePrivKeyAddressPairs(1)
|
||||
buyer := addrs[0]
|
||||
sellerModName := liquidator.ModuleName
|
||||
sellerModName := cdp.LiquidatorMacc
|
||||
sellerAddr := supply.NewModuleAddress(sellerModName)
|
||||
|
||||
tApp := app.NewTestApp()
|
||||
@ -64,7 +64,7 @@ func TestDebtAuctionBasic(t *testing.T) {
|
||||
// Setup
|
||||
_, addrs := app.GeneratePrivKeyAddressPairs(1)
|
||||
seller := addrs[0]
|
||||
buyerModName := liquidator.ModuleName
|
||||
buyerModName := cdp.LiquidatorMacc
|
||||
buyerAddr := supply.NewModuleAddress(buyerModName)
|
||||
|
||||
tApp := app.NewTestApp()
|
||||
@ -104,7 +104,7 @@ func TestCollateralAuctionBasic(t *testing.T) {
|
||||
buyer := addrs[0]
|
||||
returnAddrs := addrs[1:]
|
||||
returnWeights := is(30, 20, 10)
|
||||
sellerModName := liquidator.ModuleName
|
||||
sellerModName := cdp.LiquidatorMacc
|
||||
sellerAddr := supply.NewModuleAddress(sellerModName)
|
||||
|
||||
tApp := app.NewTestApp()
|
||||
@ -174,7 +174,7 @@ func TestStartSurplusAuction(t *testing.T) {
|
||||
{
|
||||
"normal",
|
||||
someTime,
|
||||
args{liquidator.ModuleName, c("stable", 10), "gov"},
|
||||
args{cdp.LiquidatorMacc, c("stable", 10), "gov"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
@ -186,13 +186,13 @@ func TestStartSurplusAuction(t *testing.T) {
|
||||
{
|
||||
"not enough coins",
|
||||
someTime,
|
||||
args{liquidator.ModuleName, c("stable", 101), "gov"},
|
||||
args{cdp.LiquidatorMacc, c("stable", 101), "gov"},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"incorrect denom",
|
||||
someTime,
|
||||
args{liquidator.ModuleName, c("notacoin", 10), "gov"},
|
||||
args{cdp.LiquidatorMacc, c("notacoin", 10), "gov"},
|
||||
false,
|
||||
},
|
||||
}
|
||||
@ -202,7 +202,7 @@ func TestStartSurplusAuction(t *testing.T) {
|
||||
initialLiquidatorCoins := cs(c("stable", 100))
|
||||
tApp := app.NewTestApp()
|
||||
|
||||
liqAcc := supply.NewEmptyModuleAccount(liquidator.ModuleName, supply.Burner)
|
||||
liqAcc := supply.NewEmptyModuleAccount(cdp.LiquidatorMacc, supply.Burner)
|
||||
require.NoError(t, liqAcc.SetCoins(initialLiquidatorCoins))
|
||||
tApp.InitializeFromGenesisStates(
|
||||
NewAuthGenStateFromAccs(authexported.GenesisAccounts{liqAcc}),
|
||||
@ -215,7 +215,7 @@ func TestStartSurplusAuction(t *testing.T) {
|
||||
|
||||
// check
|
||||
sk := tApp.GetSupplyKeeper()
|
||||
liquidatorCoins := sk.GetModuleAccount(ctx, liquidator.ModuleName).GetCoins()
|
||||
liquidatorCoins := sk.GetModuleAccount(ctx, cdp.LiquidatorMacc).GetCoins()
|
||||
actualAuc, found := keeper.GetAuction(ctx, id)
|
||||
|
||||
if tc.expectPass {
|
||||
|
25
x/cdp/abci.go
Normal file
25
x/cdp/abci.go
Normal 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
126
x/cdp/abci_test.go
Normal 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))
|
||||
}
|
138
x/cdp/alias.go
138
x/cdp/alias.go
@ -11,51 +11,151 @@ import (
|
||||
)
|
||||
|
||||
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
|
||||
StoreKey = types.StoreKey
|
||||
RouterKey = types.RouterKey
|
||||
QuerierRoute = types.QuerierRoute
|
||||
DefaultParamspace = types.DefaultParamspace
|
||||
QueryGetCdp = types.QueryGetCdp
|
||||
QueryGetCdps = types.QueryGetCdps
|
||||
QueryGetCdpsByCollateralization = types.QueryGetCdpsByCollateralization
|
||||
QueryGetParams = types.QueryGetParams
|
||||
RestOwner = types.RestOwner
|
||||
RestCollateralDenom = types.RestCollateralDenom
|
||||
RestUnderCollateralizedAt = types.RestUnderCollateralizedAt
|
||||
GovDenom = types.GovDenom
|
||||
RestRatio = types.RestRatio
|
||||
)
|
||||
|
||||
var (
|
||||
// functions aliases
|
||||
NewCDP = types.NewCDP
|
||||
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
|
||||
ValidateGenesis = types.ValidateGenesis
|
||||
NewMsgCreateOrModifyCDP = types.NewMsgCreateOrModifyCDP
|
||||
ParamKeyTable = types.ParamKeyTable
|
||||
GetCdpIDBytes = types.GetCdpIDBytes
|
||||
GetCdpIDFromBytes = types.GetCdpIDFromBytes
|
||||
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
|
||||
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
|
||||
NewQuerier = keeper.NewQuerier
|
||||
|
||||
// variable aliases
|
||||
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
|
||||
KeyCollateralParams = types.KeyCollateralParams
|
||||
KeyStableDenoms = types.KeyStableDenoms
|
||||
LiquidatorAccountAddress = keeper.LiquidatorAccountAddress
|
||||
KeyDebtParams = types.KeyDebtParams
|
||||
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 (
|
||||
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
|
||||
CDPs = types.CDPs
|
||||
ByCollateralRatio = types.ByCollateralRatio
|
||||
CollateralState = types.CollateralState
|
||||
Deposit = types.Deposit
|
||||
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
|
||||
LiquidatorModuleAccount = keeper.LiquidatorModuleAccount
|
||||
)
|
||||
|
@ -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)))
|
||||
}
|
@ -2,6 +2,7 @@ package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
@ -22,17 +23,17 @@ func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
|
||||
}
|
||||
|
||||
cdpQueryCmd.AddCommand(client.GetCommands(
|
||||
GetCmdGetCdp(queryRoute, cdc),
|
||||
GetCmdGetCdps(queryRoute, cdc),
|
||||
GetCmdGetUnderCollateralizedCdps(queryRoute, cdc),
|
||||
GetCmdGetParams(queryRoute, cdc),
|
||||
QueryCdpCmd(queryRoute, cdc),
|
||||
QueryCdpsByDenomCmd(queryRoute, cdc),
|
||||
QueryCdpsByDenomAndRatioCmd(queryRoute, cdc),
|
||||
QueryParamsCmd(queryRoute, cdc),
|
||||
)...)
|
||||
|
||||
return cdpQueryCmd
|
||||
}
|
||||
|
||||
// GetCmdGetCdp queries the latest info about a particular cdp
|
||||
func GetCmdGetCdp(queryRoute string, cdc *codec.Codec) *cobra.Command {
|
||||
// QueryCdpCmd returns the command handler for querying a particular cdp
|
||||
func QueryCdpCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "cdp [ownerAddress] [collateralType]",
|
||||
Short: "get info about a cdp",
|
||||
@ -47,7 +48,6 @@ func GetCmdGetCdp(queryRoute string, cdc *codec.Codec) *cobra.Command {
|
||||
}
|
||||
collateralType := args[1] // TODO validation?
|
||||
bz, err := cdc.MarshalJSON(types.QueryCdpsParams{
|
||||
Owner: ownerAddress,
|
||||
CollateralDenom: collateralType,
|
||||
})
|
||||
if err != nil {
|
||||
@ -55,7 +55,7 @@ func GetCmdGetCdp(queryRoute string, cdc *codec.Codec) *cobra.Command {
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
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
|
||||
var cdps types.CDPs
|
||||
cdc.MustUnmarshalJSON(res, &cdps)
|
||||
if len(cdps) != 1 {
|
||||
panic("Unexpected number of CDPs returned from querier. This shouldn't happen.")
|
||||
}
|
||||
return cliCtx.PrintOutput(cdps[0])
|
||||
var cdp types.CDP
|
||||
cdc.MustUnmarshalJSON(res, &cdp)
|
||||
return cliCtx.PrintOutput(cdp)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// GetCmdGetCdps queries the store for all cdps for a collateral type
|
||||
func GetCmdGetCdps(queryRoute string, cdc *codec.Codec) *cobra.Command {
|
||||
// QueryCdpsByDenomCmd returns the command handler for querying cdps for a collateral type
|
||||
func QueryCdpsByDenomCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "cdps [collateralType]",
|
||||
Short: "get info about many cdps",
|
||||
Long: "Get all CDPs. Specify a collateral type to get only CDPs with that collateral type.",
|
||||
Short: "Query cdps by collateral type",
|
||||
Long: strings.TrimSpace(`Query cdps by a specific collateral type, or query all cdps if none is specifed:
|
||||
|
||||
$ <appcli> query cdp cdps atom
|
||||
`),
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cliCtx := context.NewCLIContext().WithCodec(cdc)
|
||||
|
||||
// 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 {
|
||||
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{
|
||||
Use: "bad-cdps [collateralType] [price]",
|
||||
Short: "get under collateralized CDPs",
|
||||
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.",
|
||||
Use: "cdps [collateralType] [ratio]",
|
||||
Short: "get cdps with matching collateral type and below the specified ratio",
|
||||
Long: strings.TrimSpace(`Get all CDPS of a particular collateral type with collateralization
|
||||
ratio below the specified input.`),
|
||||
Args: cobra.ExactArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cliCtx := context.NewCLIContext().WithCodec(cdc)
|
||||
@ -117,11 +120,11 @@ func GetCmdGetUnderCollateralizedCdps(queryRoute string, cdc *codec.Codec) *cobr
|
||||
// Prepare params for querier
|
||||
price, errSdk := sdk.NewDecFromStr(args[1])
|
||||
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],
|
||||
UnderCollateralizedAt: price,
|
||||
Ratio: price,
|
||||
})
|
||||
if err != nil {
|
||||
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{
|
||||
Use: "params",
|
||||
Short: "get the cdp module parameters",
|
||||
@ -153,13 +157,13 @@ func GetCmdGetParams(queryRoute string, cdc *codec.Codec) *cobra.Command {
|
||||
|
||||
// Query
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
||||
// Decode and print results
|
||||
var out types.CdpParams
|
||||
var out types.QueryCdpParams
|
||||
cdc.MustUnmarshalJSON(res, &out)
|
||||
return cliCtx.PrintOutput(out)
|
||||
},
|
||||
|
@ -1,8 +1,6 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client"
|
||||
@ -13,11 +11,9 @@ import (
|
||||
"github.com/cosmos/cosmos-sdk/x/auth/client/utils"
|
||||
|
||||
"github.com/kava-labs/kava/x/cdp/types"
|
||||
|
||||
)
|
||||
|
||||
// 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 {
|
||||
cdpTxCmd := &cobra.Command{
|
||||
Use: "cdp",
|
||||
@ -25,33 +21,151 @@ func GetTxCmd(cdc *codec.Codec) *cobra.Command {
|
||||
}
|
||||
|
||||
cdpTxCmd.AddCommand(client.PostCommands(
|
||||
GetCmdModifyCdp(cdc),
|
||||
GetCmdCreateCdp(cdc),
|
||||
GetCmdDeposit(cdc),
|
||||
GetCmdWithdraw(cdc),
|
||||
GetCmdDraw(cdc),
|
||||
GetCmdRepay(cdc),
|
||||
)...)
|
||||
|
||||
return cdpTxCmd
|
||||
}
|
||||
|
||||
// GetCmdModifyCdp cli command for creating and modifying cdps.
|
||||
func GetCmdModifyCdp(cdc *codec.Codec) *cobra.Command {
|
||||
// GetCmdCreateCdp returns the command handler for creating a cdp
|
||||
func GetCmdCreateCdp(cdc *codec.Codec) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "modifycdp [ownerAddress] [collateralType] [collateralChange] [debtChange]",
|
||||
Short: "create or modify a cdp",
|
||||
Args: cobra.ExactArgs(4),
|
||||
Use: "create [ownerAddress] [collateralChange] [debtChange]",
|
||||
Short: "create a new 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))
|
||||
collateralChange, ok := sdk.NewIntFromString(args[2])
|
||||
if !ok {
|
||||
fmt.Printf("invalid collateral amount - %s \n", string(args[2]))
|
||||
return nil
|
||||
collateral, err := sdk.ParseCoins(args[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
debtChange, ok := sdk.NewIntFromString(args[3])
|
||||
if !ok {
|
||||
fmt.Printf("invalid debt amount - %s \n", string(args[3]))
|
||||
return nil
|
||||
debt, err := sdk.ParseCoins(args[2])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
msg := types.NewMsgCreateOrModifyCDP(cliCtx.GetFromAddress(), args[1], collateralChange, debtChange)
|
||||
err := msg.ValidateBasic()
|
||||
msg := types.NewMsgCreateCDP(cliCtx.GetFromAddress(), collateral, debt)
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -14,66 +14,91 @@ import (
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
)
|
||||
|
||||
/*
|
||||
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
|
||||
// define routes that get registered by the main application
|
||||
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")
|
||||
}
|
||||
|
||||
func getCdpsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
|
||||
func queryCdpHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// get parameters from the URL
|
||||
ownerBech32 := r.URL.Query().Get(types.RestOwner)
|
||||
collateralDenom := r.URL.Query().Get(types.RestCollateralDenom)
|
||||
priceString := r.URL.Query().Get(types.RestUnderCollateralizedAt)
|
||||
vars := mux.Vars(r)
|
||||
ownerBech32 := vars[types.RestOwner]
|
||||
collateralDenom := vars[types.RestCollateralDenom]
|
||||
|
||||
// Construct querier params
|
||||
querierParams := types.QueryCdpsParams{}
|
||||
|
||||
if len(ownerBech32) != 0 {
|
||||
owner, err := sdk.AccAddressFromBech32(ownerBech32)
|
||||
if err != nil {
|
||||
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
querierParams.Owner = owner
|
||||
}
|
||||
|
||||
if len(collateralDenom) != 0 {
|
||||
// TODO validate denom
|
||||
querierParams.CollateralDenom = collateralDenom
|
||||
}
|
||||
params := types.NewQueryCdpParams(owner, collateralDenom)
|
||||
|
||||
if len(priceString) != 0 {
|
||||
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)
|
||||
bz, err := cliCtx.Codec.MarshalJSON(params)
|
||||
if err != nil {
|
||||
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Get the CDPs
|
||||
res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/cdp/%s", types.QueryGetCdps), querierParamsBz)
|
||||
res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s", types.QueryGetCdp), bz)
|
||||
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 {
|
||||
rest.WriteErrorResponse(w, http.StatusNotFound, err.Error())
|
||||
return
|
||||
|
@ -4,6 +4,8 @@ import (
|
||||
"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"
|
||||
)
|
||||
|
||||
// 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)
|
||||
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"`
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
@ -15,13 +14,18 @@ import (
|
||||
)
|
||||
|
||||
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) {
|
||||
// Decode PUT request body
|
||||
var requestBody types.ModifyCdpRequestBody
|
||||
var requestBody PostCdpReq
|
||||
if !rest.ReadRESTReq(w, r, cliCtx.Codec, &requestBody) {
|
||||
return
|
||||
}
|
||||
@ -30,39 +34,99 @@ func modifyCdpHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
|
||||
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
|
||||
msg := types.NewMsgCreateOrModifyCDP(
|
||||
requestBody.Cdp.Owner,
|
||||
requestBody.Cdp.CollateralDenom,
|
||||
collateralDelta,
|
||||
debtDelta,
|
||||
msg := types.NewMsgCreateCDP(
|
||||
requestBody.Owner,
|
||||
requestBody.Collateral,
|
||||
requestBody.Principal,
|
||||
)
|
||||
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})
|
||||
}
|
||||
|
@ -6,42 +6,77 @@ import (
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
)
|
||||
|
||||
func InitGenesis(ctx sdk.Context, k Keeper, pk PricefeedKeeper, data GenesisState) {
|
||||
// validate denoms - check that any collaterals in the CdpParams are in the pricefeed, pricefeed needs to initgenesis before cdp
|
||||
collateralMap := make(map[string]int)
|
||||
ap := pk.GetParams(ctx)
|
||||
for _, m := range ap.Markets {
|
||||
collateralMap[m.MarketID] = 1
|
||||
// InitGenesis sets initial genesis state for cdp module
|
||||
func InitGenesis(ctx sdk.Context, k Keeper, pk PricefeedKeeper, gs GenesisState) {
|
||||
if err := gs.Validate(); err != nil {
|
||||
panic(fmt.Sprintf("failed to validate %s genesis state: %s", ModuleName, err))
|
||||
}
|
||||
|
||||
for _, col := range data.Params.CollateralParams {
|
||||
_, found := collateralMap[col.Denom]
|
||||
// validate denoms - check that any collaterals in the params are in the pricefeed,
|
||||
// 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 {
|
||||
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 {
|
||||
k.SetCDP(ctx, cdp)
|
||||
// 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)
|
||||
}
|
||||
|
||||
k.SetGlobalDebt(ctx, data.GlobalDebt)
|
||||
// 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.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.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 {
|
||||
params := k.GetParams(ctx)
|
||||
cdps, err := k.GetCDPs(ctx, "", sdk.Dec{})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
cdps := k.GetAllCdps(ctx)
|
||||
cdpID := k.GetNextCdpID(ctx)
|
||||
previousBlockTime, found := k.GetPreviousBlockTime(ctx)
|
||||
if !found {
|
||||
previousBlockTime = DefaultPreviousBlockTime
|
||||
}
|
||||
debt := k.GetGlobalDebt(ctx)
|
||||
debtDenom := k.GetDebtDenom(ctx)
|
||||
|
||||
return GenesisState{
|
||||
Params: params,
|
||||
GlobalDebt: debt,
|
||||
StartingCdpID: cdpID,
|
||||
CDPs: cdps,
|
||||
PreviousBlockTime: previousBlockTime,
|
||||
DebtDenom: debtDenom,
|
||||
}
|
||||
}
|
||||
|
61
x/cdp/genesis_test.go
Normal file
61
x/cdp/genesis_test.go
Normal 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))
|
||||
}
|
101
x/cdp/handler.go
101
x/cdp/handler.go
@ -6,25 +6,108 @@ import (
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
)
|
||||
|
||||
// Handle all cdp messages.
|
||||
func NewHandler(keeper Keeper) sdk.Handler {
|
||||
// NewHandler creates an sdk.Handler for cdp messages
|
||||
func NewHandler(k Keeper) sdk.Handler {
|
||||
return func(ctx sdk.Context, msg sdk.Msg) sdk.Result {
|
||||
switch msg := msg.(type) {
|
||||
case MsgCreateOrModifyCDP:
|
||||
return handleMsgCreateOrModifyCDP(ctx, keeper, msg)
|
||||
case MsgCreateCDP:
|
||||
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:
|
||||
errMsg := fmt.Sprintf("Unrecognized cdp msg type: %T", msg)
|
||||
errMsg := fmt.Sprintf("unrecognized cdp msg type: %T", msg)
|
||||
return sdk.ErrUnknownRequest(errMsg).Result()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleMsgCreateOrModifyCDP(ctx sdk.Context, keeper Keeper, msg MsgCreateOrModifyCDP) sdk.Result {
|
||||
|
||||
err := keeper.ModifyCDP(ctx, msg.Sender, msg.CollateralDenom, msg.CollateralChange, msg.DebtChange)
|
||||
func handleMsgCreateCDP(ctx sdk.Context, k Keeper, msg MsgCreateCDP) sdk.Result {
|
||||
err := k.AddCdp(ctx, msg.Sender, msg.Collateral, msg.Principal)
|
||||
if err != nil {
|
||||
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
64
x/cdp/handler_test.go
Normal 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))
|
||||
}
|
@ -8,26 +8,25 @@ import (
|
||||
"github.com/kava-labs/kava/app"
|
||||
"github.com/kava-labs/kava/x/cdp"
|
||||
"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 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...) }
|
||||
|
||||
func NewPFGenState(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},
|
||||
},
|
||||
}
|
||||
func NewPricefeedGenState(asset string, price sdk.Dec) app.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{
|
||||
pricefeed.PostedPrice{
|
||||
MarketID: asset,
|
||||
MarketID: asset + ":usd",
|
||||
OracleAddress: sdk.AccAddress{},
|
||||
Price: price,
|
||||
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 {
|
||||
cdpGenesis := cdp.GenesisState{
|
||||
Params: cdp.CdpParams{
|
||||
GlobalDebtLimit: sdk.NewInt(1000000),
|
||||
CollateralParams: []cdp.CollateralParams{
|
||||
Params: cdp.Params{
|
||||
GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000)),
|
||||
CollateralParams: cdp.CollateralParams{
|
||||
{
|
||||
Denom: asset,
|
||||
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{},
|
||||
PreviousBlockTime: cdp.DefaultPreviousBlockTime,
|
||||
}
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
@ -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
421
x/cdp/keeper/cdp.go
Normal 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
299
x/cdp/keeper/cdp_test.go
Normal 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
223
x/cdp/keeper/deposit.go
Normal 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
|
||||
}
|
154
x/cdp/keeper/deposit_test.go
Normal file
154
x/cdp/keeper/deposit_test.go
Normal 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
216
x/cdp/keeper/draw.go
Normal 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
198
x/cdp/keeper/draw_test.go
Normal 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
102
x/cdp/keeper/fees.go
Normal 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
95
x/cdp/keeper/fees_test.go
Normal 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))
|
||||
}
|
@ -7,11 +7,11 @@ import (
|
||||
|
||||
"github.com/kava-labs/kava/app"
|
||||
"github.com/kava-labs/kava/x/cdp"
|
||||
"github.com/kava-labs/kava/x/cdp/types"
|
||||
"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 d(str string) sdk.Dec { return sdk.MustNewDecFromStr(str) }
|
||||
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{
|
||||
Params: pricefeed.Params{
|
||||
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{
|
||||
pricefeed.PostedPrice{
|
||||
MarketID: asset,
|
||||
MarketID: asset + ":usd",
|
||||
OracleAddress: sdk.AccAddress{},
|
||||
Price: price,
|
||||
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 {
|
||||
cdpGenesis := cdp.GenesisState{
|
||||
Params: cdp.CdpParams{
|
||||
GlobalDebtLimit: sdk.NewInt(1000000),
|
||||
CollateralParams: []cdp.CollateralParams{
|
||||
Params: cdp.Params{
|
||||
GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000)),
|
||||
CollateralParams: cdp.CollateralParams{
|
||||
{
|
||||
Denom: asset,
|
||||
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{},
|
||||
PreviousBlockTime: cdp.DefaultPreviousBlockTime,
|
||||
}
|
||||
return app.GenesisState{cdp.ModuleName: cdp.ModuleCdc.MustMarshalJSON(cdpGenesis)}
|
||||
}
|
||||
@ -58,19 +73,19 @@ func NewPricefeedGenStateMulti() app.GenesisState {
|
||||
pfGenesis := pricefeed.GenesisState{
|
||||
Params: pricefeed.Params{
|
||||
Markets: []pricefeed.Market{
|
||||
pricefeed.Market{MarketID: "btc", 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: "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",
|
||||
MarketID: "btc:usd",
|
||||
OracleAddress: sdk.AccAddress{},
|
||||
Price: sdk.MustNewDecFromStr("8000.00"),
|
||||
Expiry: time.Now().Add(1 * time.Hour),
|
||||
},
|
||||
pricefeed.PostedPrice{
|
||||
MarketID: "xrp",
|
||||
MarketID: "xrp:usd",
|
||||
OracleAddress: sdk.AccAddress{},
|
||||
Price: sdk.MustNewDecFromStr("0.25"),
|
||||
Expiry: time.Now().Add(1 * time.Hour),
|
||||
@ -81,23 +96,59 @@ func NewPricefeedGenStateMulti() app.GenesisState {
|
||||
}
|
||||
func NewCDPGenStateMulti() app.GenesisState {
|
||||
cdpGenesis := cdp.GenesisState{
|
||||
Params: cdp.CdpParams{
|
||||
GlobalDebtLimit: sdk.NewInt(1000000),
|
||||
CollateralParams: []types.CollateralParams{
|
||||
{
|
||||
Denom: "btc",
|
||||
LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
|
||||
DebtLimit: sdk.NewInt(500000),
|
||||
},
|
||||
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.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{},
|
||||
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
|
||||
}
|
||||
|
@ -1,499 +1,103 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/codec"
|
||||
"github.com/cosmos/cosmos-sdk/store/prefix"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/x/params/subspace"
|
||||
"github.com/kava-labs/kava/x/cdp/types"
|
||||
)
|
||||
|
||||
// Keeper cdp Keeper
|
||||
// Keeper keeper for the cdp module
|
||||
type Keeper struct {
|
||||
key sdk.StoreKey
|
||||
cdc *codec.Codec
|
||||
paramSubspace subspace.Subspace
|
||||
pricefeedKeeper types.PricefeedKeeper
|
||||
bankKeeper types.BankKeeper
|
||||
supplyKeeper types.SupplyKeeper
|
||||
codespace sdk.CodespaceType
|
||||
}
|
||||
|
||||
// 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{
|
||||
key: key,
|
||||
cdc: cdc,
|
||||
paramSubspace: paramstore.WithKeyTable(types.ParamKeyTable()),
|
||||
pricefeedKeeper: pfk,
|
||||
bankKeeper: bk,
|
||||
supplyKeeper: sk,
|
||||
codespace: codespace,
|
||||
}
|
||||
}
|
||||
|
||||
// ModifyCDP creates, changes, or deletes a CDP
|
||||
// TODO can/should this function be split up?
|
||||
func (k Keeper) ModifyCDP(ctx sdk.Context, owner sdk.AccAddress, collateralDenom string, changeInCollateral sdk.Int, changeInDebt sdk.Int) sdk.Error {
|
||||
|
||||
// Phase 1: Get state, make changes in memory and check if they're ok.
|
||||
|
||||
// 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
|
||||
if changeInCollateral.IsPositive() { // adding collateral to CDP
|
||||
ok := k.bankKeeper.HasCoins(ctx, owner, sdk.NewCoins(sdk.NewCoin(collateralDenom, changeInCollateral)))
|
||||
if !ok {
|
||||
return sdk.ErrInsufficientCoins("not enough collateral in sender's account")
|
||||
}
|
||||
}
|
||||
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
|
||||
// Get CDP (or create if not exists)
|
||||
cdp, found := k.GetCDP(ctx, owner, collateralDenom)
|
||||
if !found {
|
||||
cdp = types.CDP{Owner: owner, CollateralDenom: collateralDenom, CollateralAmount: sdk.ZeroInt(), Debt: sdk.ZeroInt()}
|
||||
}
|
||||
// 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
|
||||
// CdpDenomIndexIterator returns an sdk.Iterator for all cdps with matching collateral denom
|
||||
func (k Keeper) CdpDenomIndexIterator(ctx sdk.Context, denom string) sdk.Iterator {
|
||||
store := prefix.NewStore(ctx.KVStore(k.key), types.CdpKeyPrefix)
|
||||
db, _ := k.GetDenomPrefix(ctx, denom)
|
||||
return sdk.KVStorePrefixIterator(store, types.DenomIterKey(db))
|
||||
}
|
||||
|
||||
// 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
|
||||
// CdpCollateralRatioIndexIterator returns an sdk.Iterator for all cdps that have collateral denom
|
||||
// matching denom and collateral:debt ratio LESS THAN targetRatio
|
||||
func (k Keeper) CdpCollateralRatioIndexIterator(ctx sdk.Context, denom string, targetRatio sdk.Dec) sdk.Iterator {
|
||||
store := prefix.NewStore(ctx.KVStore(k.key), types.CollateralRatioIndexPrefix)
|
||||
db, _ := k.GetDenomPrefix(ctx, denom)
|
||||
return store.Iterator(types.CollateralRatioIterKey(db, sdk.ZeroDec()), types.CollateralRatioIterKey(db, targetRatio))
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
// IterateAllCdps iterates over all cdps and performs a callback function
|
||||
func (k Keeper) IterateAllCdps(ctx sdk.Context, cb func(cdp types.CDP) (stop bool)) {
|
||||
store := prefix.NewStore(ctx.KVStore(k.key), types.CdpKeyPrefix)
|
||||
iterator := sdk.KVStorePrefixIterator(store, []byte{})
|
||||
defer iterator.Close()
|
||||
for ; iterator.Valid(); iterator.Next() {
|
||||
var cdp types.CDP
|
||||
k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &cdp)
|
||||
return cdp, true
|
||||
}
|
||||
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))
|
||||
}
|
||||
k.cdc.MustUnmarshalBinaryLengthPrefixed(iterator.Value(), &cdp)
|
||||
|
||||
// GetCDPs returns all CDPs, optionally filtered by collateral type and liquidation price.
|
||||
// `price` filters for CDPs that will be below the liquidation ratio when the collateral is at that specified price.
|
||||
func (k Keeper) GetCDPs(ctx sdk.Context, collateralDenom string, price sdk.Dec) (types.CDPs, sdk.Error) {
|
||||
// Validate inputs
|
||||
params := k.GetParams(ctx)
|
||||
if len(collateralDenom) != 0 && !params.IsCollateralPresent(collateralDenom) {
|
||||
return nil, sdk.ErrInternal("collateral denom not authorized")
|
||||
if cb(cdp) {
|
||||
break
|
||||
}
|
||||
if len(collateralDenom) == 0 && !(price.IsNil() || price.IsNegative()) {
|
||||
return nil, sdk.ErrInternal("cannot specify price without collateral denom")
|
||||
}
|
||||
}
|
||||
|
||||
// Get an iterator over CDPs
|
||||
store := ctx.KVStore(k.key)
|
||||
iter := sdk.KVStorePrefixIterator(store, k.getCDPKeyPrefix(collateralDenom)) // could be all CDPs is collateralDenom is ""
|
||||
// IterateCdpsByDenom iterates over cdps with matching denom and performs a callback function
|
||||
func (k Keeper) IterateCdpsByDenom(ctx sdk.Context, denom string, cb func(cdp types.CDP) (stop bool)) {
|
||||
iterator := k.CdpDenomIndexIterator(ctx, denom)
|
||||
|
||||
// Decode CDPs into slice
|
||||
var cdps types.CDPs
|
||||
for ; iter.Valid(); iter.Next() {
|
||||
defer iterator.Close()
|
||||
for ; iterator.Valid(); iterator.Next() {
|
||||
var cdp types.CDP
|
||||
k.cdc.MustUnmarshalBinaryLengthPrefixed(iter.Value(), &cdp)
|
||||
cdps = append(cdps, cdp)
|
||||
k.cdc.MustUnmarshalBinaryLengthPrefixed(iterator.Value(), &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
|
||||
func (k Keeper) SubtractCoins(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)
|
||||
// subtract coins from module account
|
||||
lma := k.getLiquidatorModuleAccount(ctx)
|
||||
updatedCoins, isNegative := lma.Coins.SafeSub(filteredCoins)
|
||||
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)
|
||||
}
|
||||
}
|
||||
// IterateCdpsByCollateralRatio iterate over cdps with collateral denom equal to denom and
|
||||
// collateral:debt ratio LESS THAN targetRatio and performs a callback function.
|
||||
func (k Keeper) IterateCdpsByCollateralRatio(ctx sdk.Context, denom string, targetRatio sdk.Dec, cb func(cdp types.CDP) (stop bool)) {
|
||||
iterator := k.CdpCollateralRatioIndexIterator(ctx, denom, targetRatio)
|
||||
|
||||
// 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)
|
||||
defer iterator.Close()
|
||||
for ; iterator.Valid(); iterator.Next() {
|
||||
db, id, _ := types.SplitCollateralRatioKey(iterator.Key())
|
||||
d := k.getDenomFromByte(ctx, db)
|
||||
cdp, found := k.GetCDP(ctx, d, id)
|
||||
if !found {
|
||||
panic(fmt.Sprintf("cdp %d does not exist", id))
|
||||
}
|
||||
if cb(cdp) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
@ -1,19 +1,81 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/kava-labs/kava/x/cdp/types"
|
||||
)
|
||||
|
||||
// ---------- Module Parameters ----------
|
||||
// GetParams returns the params from the store
|
||||
func (k Keeper) GetParams(ctx sdk.Context) types.CdpParams {
|
||||
var p types.CdpParams
|
||||
func (k Keeper) GetParams(ctx sdk.Context) types.Params {
|
||||
var p types.Params
|
||||
k.paramSubspace.GetParamSet(ctx, &p)
|
||||
return p
|
||||
}
|
||||
|
||||
// SetParams sets params on the store
|
||||
func (k Keeper) SetParams(ctx sdk.Context, cdpParams types.CdpParams) {
|
||||
k.paramSubspace.SetParamSet(ctx, &cdpParams)
|
||||
func (k Keeper) SetParams(ctx sdk.Context, params types.Params) {
|
||||
k.paramSubspace.SetParamSet(ctx, ¶ms)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
@ -10,13 +10,16 @@ import (
|
||||
"github.com/kava-labs/kava/x/cdp/types"
|
||||
)
|
||||
|
||||
|
||||
|
||||
// NewQuerier returns a new querier function
|
||||
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.QueryGetCdp:
|
||||
return queryGetCdp(ctx, req, keeper)
|
||||
case types.QueryGetCdps:
|
||||
return queryGetCdps(ctx, req, keeper)
|
||||
return queryGetCdpsByDenom(ctx, req, keeper)
|
||||
case types.QueryGetCdpsByCollateralization:
|
||||
return queryGetCdpsByRatio(ctx, req, keeper)
|
||||
case types.QueryGetParams:
|
||||
return queryGetParams(ctx, req, keeper)
|
||||
default:
|
||||
@ -25,42 +28,45 @@ func NewQuerier(keeper Keeper) sdk.Querier {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// queryGetCdps fetches CDPs, optionally filtering by any of the query params (in QueryCdpsParams).
|
||||
// While CDPs do not have an ID, this method can be used to get one CDP by specifying the collateral and owner.
|
||||
func queryGetCdps(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) {
|
||||
// Decode request
|
||||
var requestParams types.QueryCdpsParams
|
||||
// query a specific cdp
|
||||
func queryGetCdp(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) {
|
||||
var requestParams types.QueryCdpParams
|
||||
err := keeper.cdc.UnmarshalJSON(req.Data, &requestParams)
|
||||
if err != nil {
|
||||
return nil, sdk.ErrInternal(fmt.Sprintf("failed to parse params: %s", err))
|
||||
}
|
||||
|
||||
// Get CDPs
|
||||
var cdps types.CDPs
|
||||
if len(requestParams.Owner) != 0 {
|
||||
if len(requestParams.CollateralDenom) != 0 {
|
||||
// owner and collateral specified - get a single CDP
|
||||
cdp, found := keeper.GetCDP(ctx, requestParams.Owner, requestParams.CollateralDenom)
|
||||
_, valid := keeper.GetDenomPrefix(ctx, requestParams.CollateralDenom)
|
||||
if !valid {
|
||||
return nil, types.ErrInvalidCollateralDenom(keeper.codespace, requestParams.CollateralDenom)
|
||||
}
|
||||
|
||||
cdp, found := keeper.GetCdpByOwnerAndDenom(ctx, requestParams.Owner, requestParams.CollateralDenom)
|
||||
if !found {
|
||||
cdp = types.CDP{Owner: requestParams.Owner, CollateralDenom: requestParams.CollateralDenom, CollateralAmount: sdk.ZeroInt(), Debt: sdk.ZeroInt()}
|
||||
}
|
||||
cdps = types.CDPs{cdp}
|
||||
} else {
|
||||
// owner, but no collateral specified - get all CDPs for one address
|
||||
return nil, sdk.ErrInternal("getting all CDPs belonging to one owner not implemented")
|
||||
}
|
||||
} 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 nil, types.ErrCdpNotFound(keeper.codespace, requestParams.Owner, requestParams.CollateralDenom)
|
||||
}
|
||||
|
||||
bz, err := codec.MarshalJSONIndent(keeper.cdc, cdp)
|
||||
if err != nil {
|
||||
return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error()))
|
||||
}
|
||||
return bz, nil
|
||||
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// Encode results
|
||||
cdps := keeper.GetAllCdpsByDenomAndRatio(ctx, requestParams.CollateralDenom, requestParams.Ratio)
|
||||
bz, err := codec.MarshalJSONIndent(keeper.cdc, cdps)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
// queryGetParams fetches the cdp module parameters
|
||||
// TODO does this need to exist? Can you use cliCtx.QueryStore instead?
|
||||
// query all cdps with matching collateral denom
|
||||
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) {
|
||||
// Get params
|
||||
params := keeper.GetParams(ctx)
|
||||
|
226
x/cdp/keeper/querier_test.go
Normal file
226
x/cdp/keeper/querier_test.go
Normal 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
102
x/cdp/keeper/seize.go
Normal 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
141
x/cdp/keeper/seize_test.go
Normal 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))
|
||||
}
|
@ -41,12 +41,12 @@ func (AppModuleBasic) DefaultGenesis() json.RawMessage {
|
||||
|
||||
// ValidateGenesis module validate genesis
|
||||
func (AppModuleBasic) ValidateGenesis(bz json.RawMessage) error {
|
||||
var data GenesisState
|
||||
err := ModuleCdc.UnmarshalJSON(bz, &data)
|
||||
var gs GenesisState
|
||||
err := ModuleCdc.UnmarshalJSON(bz, &gs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ValidateGenesis(data)
|
||||
return gs.Validate()
|
||||
}
|
||||
|
||||
// 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
|
||||
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
|
||||
func (am AppModule) EndBlock(ctx sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate {
|
||||
func (am AppModule) EndBlock(_ sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate {
|
||||
return []abci.ValidatorUpdate{}
|
||||
}
|
||||
|
64
x/cdp/types/cdp.go
Normal file
64
x/cdp/types/cdp.go
Normal 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
|
||||
}
|
@ -12,8 +12,12 @@ func init() {
|
||||
ModuleCdc = cdc.Seal()
|
||||
}
|
||||
|
||||
// RegisterCodec registers concrete types on the codec.
|
||||
// RegisterCodec registers the necessary types for cdp module
|
||||
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)
|
||||
}
|
||||
|
83
x/cdp/types/deposit.go
Normal file
83
x/cdp/types/deposit.go
Normal 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
103
x/cdp/types/errors.go
Normal 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
16
x/cdp/types/events.go
Normal 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"
|
||||
)
|
@ -4,18 +4,28 @@ import (
|
||||
"time"
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
type BankKeeper interface {
|
||||
GetCoins(sdk.Context, sdk.AccAddress) sdk.Coins
|
||||
HasCoins(sdk.Context, sdk.AccAddress, sdk.Coins) bool
|
||||
AddCoins(sdk.Context, sdk.AccAddress, sdk.Coins) (sdk.Coins, sdk.Error)
|
||||
SubtractCoins(sdk.Context, sdk.AccAddress, sdk.Coins) (sdk.Coins, sdk.Error)
|
||||
// SupplyKeeper defines the expected supply keeper for module accounts
|
||||
type SupplyKeeper interface {
|
||||
GetModuleAddress(name string) sdk.AccAddress
|
||||
GetModuleAccount(ctx sdk.Context, name string) supplyexported.ModuleAccountI
|
||||
|
||||
// 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 {
|
||||
GetCurrentPrice(sdk.Context, string) pftypes.CurrentPrice
|
||||
GetCurrentPrice(sdk.Context, string) (pftypes.CurrentPrice, sdk.Error)
|
||||
GetParams(sdk.Context) pftypes.Params
|
||||
// These are used for testing TODO replace mockApp with keeper in tests to remove these
|
||||
SetParams(sdk.Context, pftypes.Params)
|
||||
|
@ -1,34 +1,61 @@
|
||||
package types
|
||||
|
||||
import sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
Params CdpParams `json:"params" yaml:"params"`
|
||||
GlobalDebt sdk.Int `json:"global_debt" yaml:"global_debt"`
|
||||
Params Params `json:"params" yaml:"params"`
|
||||
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
|
||||
// TODO make this empty, load test values independent
|
||||
func DefaultGenesisState() GenesisState {
|
||||
return GenesisState{
|
||||
Params: DefaultParams(),
|
||||
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.
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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{})
|
||||
}
|
||||
|
22
x/cdp/types/genesis_test.go
Normal file
22
x/cdp/types/genesis_test.go
Normal 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))
|
||||
}
|
@ -1,5 +1,12 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
)
|
||||
|
||||
const (
|
||||
// ModuleName The name that will be used throughout the module
|
||||
ModuleName = "cdp"
|
||||
@ -10,6 +17,161 @@ const (
|
||||
// RouterKey Top level router key
|
||||
RouterKey = ModuleName
|
||||
|
||||
// QuerierRoute Top level query string
|
||||
QuerierRoute = ModuleName
|
||||
|
||||
// DefaultParamspace default name for parameter store
|
||||
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
65
x/cdp/types/keys_test.go
Normal 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
|
||||
}
|
@ -1,54 +1,327 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
)
|
||||
|
||||
// MsgCreateOrModifyCDP creates, adds/removes collateral/stable coin from a cdp
|
||||
// TODO Make this more user friendly - maybe split into four functions.
|
||||
type MsgCreateOrModifyCDP struct {
|
||||
Sender sdk.AccAddress
|
||||
CollateralDenom string
|
||||
CollateralChange sdk.Int
|
||||
DebtChange sdk.Int
|
||||
// ensure Msg interface compliance at compile time
|
||||
var (
|
||||
_ sdk.Msg = &MsgCreateCDP{}
|
||||
_ sdk.Msg = &MsgDeposit{}
|
||||
_ sdk.Msg = &MsgWithdraw{}
|
||||
_ sdk.Msg = &MsgDrawDebt{}
|
||||
_ 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.
|
||||
func NewMsgCreateOrModifyCDP(sender sdk.AccAddress, collateralDenom string, collateralChange sdk.Int, debtChange sdk.Int) MsgCreateOrModifyCDP {
|
||||
return MsgCreateOrModifyCDP{
|
||||
// NewMsgCreateCDP returns a new MsgPlaceBid.
|
||||
func NewMsgCreateCDP(sender sdk.AccAddress, collateral sdk.Coins, principal sdk.Coins) MsgCreateCDP {
|
||||
return MsgCreateCDP{
|
||||
Sender: sender,
|
||||
CollateralDenom: collateralDenom,
|
||||
CollateralChange: collateralChange,
|
||||
DebtChange: debtChange,
|
||||
Collateral: collateral,
|
||||
Principal: principal,
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
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.
|
||||
func (msg MsgCreateOrModifyCDP) ValidateBasic() sdk.Error {
|
||||
func (msg MsgCreateCDP) ValidateBasic() sdk.Error {
|
||||
if msg.Sender.Empty() {
|
||||
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
|
||||
}
|
||||
|
||||
// GetSignBytes gets the canonical byte representation of the Msg.
|
||||
func (msg MsgCreateOrModifyCDP) GetSignBytes() []byte {
|
||||
func (msg MsgCreateCDP) GetSignBytes() []byte {
|
||||
bz := ModuleCdc.MustMarshalJSON(msg)
|
||||
return sdk.MustSortJSON(bz)
|
||||
}
|
||||
|
||||
// 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}
|
||||
}
|
||||
|
||||
// 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
|
||||
type MsgTransferCDP struct {
|
||||
// TODO
|
||||
|
166
x/cdp/types/msg_test.go
Normal file
166
x/cdp/types/msg_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
@ -2,137 +2,215 @@ package types
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
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
|
||||
var (
|
||||
// ParamStoreKeyAuctionParams Param store key for auction params
|
||||
KeyGlobalDebtLimit = []byte("GlobalDebtLimit")
|
||||
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
|
||||
func ParamKeyTable() subspace.KeyTable {
|
||||
return subspace.NewKeyTable().RegisterParamSet(&CdpParams{})
|
||||
func ParamKeyTable() params.KeyTable {
|
||||
return params.NewKeyTable().RegisterParamSet(&Params{})
|
||||
}
|
||||
|
||||
// ParamSetPairs implements the ParamSet interface and returns all the key/value pairs
|
||||
// pairs of auth module's parameters.
|
||||
// nolint
|
||||
func (p *CdpParams) ParamSetPairs() subspace.ParamSetPairs {
|
||||
return subspace.ParamSetPairs{
|
||||
{KeyGlobalDebtLimit, &p.GlobalDebtLimit},
|
||||
{KeyCollateralParams, &p.CollateralParams},
|
||||
{KeyStableDenoms, &p.StableDenoms},
|
||||
func (p *Params) ParamSetPairs() params.ParamSetPairs {
|
||||
return params.ParamSetPairs{
|
||||
{Key: KeyGlobalDebtLimit, Value: &p.GlobalDebtLimit},
|
||||
{Key: KeyCollateralParams, Value: &p.CollateralParams},
|
||||
{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.
|
||||
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)
|
||||
denomDupMap := make(map[string]int)
|
||||
for _, collateral := range p.CollateralParams {
|
||||
_, found := collateralDupMap[collateral.Denom]
|
||||
prefixDupMap := make(map[int]int)
|
||||
collateralParamsDebtLimit := sdk.Coins{}
|
||||
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 {
|
||||
return fmt.Errorf("duplicate denom: %s", collateral.Denom)
|
||||
}
|
||||
collateralDupMap[collateral.Denom] = 1
|
||||
|
||||
if collateral.DebtLimit.IsNegative() {
|
||||
return fmt.Errorf("debt limit should be positive, is %s for %s", collateral.DebtLimit, collateral.Denom)
|
||||
return fmt.Errorf("duplicate prefix for collateral denom %s: %v", cp.Denom, []byte{cp.Prefix})
|
||||
}
|
||||
|
||||
// TODO do we want to enforce overcollateralization at this level? -- probably not, as it's technically a governance thing (kevin)
|
||||
}
|
||||
if p.GlobalDebtLimit.IsNegative() {
|
||||
return fmt.Errorf("global debt limit should be positive, is %s", p.GlobalDebtLimit)
|
||||
}
|
||||
prefixDupMap[prefix] = 1
|
||||
_, found = collateralDupMap[cp.Denom]
|
||||
|
||||
for _, denom := range p.StableDenoms {
|
||||
_, found := denomDupMap[denom]
|
||||
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
|
||||
}
|
||||
|
||||
func DefaultParams() CdpParams {
|
||||
return CdpParams{
|
||||
GlobalDebtLimit: sdk.NewInt(0),
|
||||
CollateralParams: []CollateralParams{},
|
||||
StableDenoms: []string{"usdx"},
|
||||
}
|
||||
}
|
||||
|
@ -2,24 +2,55 @@ package types
|
||||
|
||||
import (
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/types/rest"
|
||||
)
|
||||
|
||||
// Querier routes for the cdp module
|
||||
const (
|
||||
QueryGetCdp = "cdp"
|
||||
QueryGetCdps = "cdps"
|
||||
QueryGetCdpsByCollateralization = "ratio"
|
||||
QueryGetParams = "params"
|
||||
RestOwner = "owner"
|
||||
RestCollateralDenom = "collateralDenom"
|
||||
RestUnderCollateralizedAt = "underCollateralizedAt"
|
||||
RestCollateralDenom = "collateral-denom"
|
||||
RestRatio = "ratio"
|
||||
)
|
||||
|
||||
// QueryCdpsParams params for query /cdp/cdps
|
||||
type QueryCdpsParams struct {
|
||||
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 {
|
||||
BaseReq rest.BaseReq `json:"base_req"`
|
||||
Cdp CDP `json:"cdp"`
|
||||
// NewQueryCdpsParams returns QueryCdpsParams
|
||||
func NewQueryCdpsParams(denom string) QueryCdpsParams {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
@ -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
99
x/cdp/types/utils.go
Normal 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
83
x/cdp/types/utils_test.go
Normal 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)
|
||||
}
|
||||
}
|
@ -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
|
||||
)
|
@ -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)
|
||||
},
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
@ -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})
|
||||
}
|
||||
}
|
@ -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
|
@ -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,
|
||||
}
|
||||
}
|
@ -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
|
||||
// }
|
@ -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)}
|
||||
}
|
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
@ -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, ¶ms)
|
||||
return params
|
||||
}
|
||||
|
||||
// SetParams sets params for the liquidator module
|
||||
func (k Keeper) SetParams(ctx sdk.Context, params types.LiquidatorParams) {
|
||||
k.paramSubspace.SetParamSet(ctx, ¶ms)
|
||||
}
|
@ -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
|
||||
}
|
@ -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{}
|
||||
}
|
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
)
|
@ -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} }
|
@ -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
|
||||
}
|
@ -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?
|
||||
}
|
@ -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
|
||||
}
|
@ -95,10 +95,14 @@ func (k Keeper) SetCurrentPrices(ctx sdk.Context, marketID string) sdk.Error {
|
||||
})
|
||||
}
|
||||
}
|
||||
medianPrice, err := k.CalculateMedianPrice(ctx, notExpiredPrices)
|
||||
if err != nil {
|
||||
return err
|
||||
if len(notExpiredPrices) == 0 {
|
||||
store := ctx.KVStore(k.storeKey)
|
||||
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)
|
||||
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.
|
||||
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)
|
||||
|
||||
if l == 0 {
|
||||
// Error if there are no valid prices in the raw pricefeed
|
||||
return sdk.Dec{}, types.ErrNoValidPrice(k.codespace)
|
||||
} else if l == 1 {
|
||||
if l == 1 {
|
||||
// Return immediately if there's only one price
|
||||
return prices[0].Price, nil
|
||||
} else {
|
||||
return prices[0].Price
|
||||
}
|
||||
// sort the prices
|
||||
sort.Slice(prices, func(i, j int) bool {
|
||||
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
|
||||
if l%2 == 0 {
|
||||
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
|
||||
return prices[l/2].Price, nil
|
||||
}
|
||||
return prices[l/2].Price
|
||||
|
||||
}
|
||||
|
||||
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
|
||||
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)
|
||||
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
|
||||
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
|
||||
|
@ -120,7 +120,8 @@ func TestKeeper_GetSetCurrentPrice(t *testing.T) {
|
||||
err := keeper.SetCurrentPrices(ctx, "tstusd")
|
||||
require.NoError(t, err)
|
||||
// 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)
|
||||
|
||||
// Even number of oracles
|
||||
@ -130,6 +131,7 @@ func TestKeeper_GetSetCurrentPrice(t *testing.T) {
|
||||
time.Now().Add(time.Hour*1))
|
||||
err = keeper.SetCurrentPrices(ctx, "tstusd")
|
||||
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)
|
||||
}
|
||||
|
@ -32,8 +32,10 @@ func queryCurrentPrice(ctx sdk.Context, path []string, req abci.RequestQuery, ke
|
||||
if !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)
|
||||
if err2 != nil {
|
||||
panic("could not marshal result to JSON")
|
||||
|
Loading…
Reference in New Issue
Block a user